show date last updated for posts with multiple commits
8 files changed, 121 insertions(+), 18 deletions(-)
M internal/builder/builder.go → internal/builder/builder.go
@@ -2,7 +2,6 @@ package builder import ( "context" - "database/sql" "fmt" "io" "os"@@ -17,6 +16,7 @@ "go.alanpearce.eu/website/internal/content" "go.alanpearce.eu/website/internal/sitemap" "go.alanpearce.eu/website/internal/storage" "go.alanpearce.eu/website/internal/storage/files" + "go.alanpearce.eu/website/internal/vcs" "go.alanpearce.eu/website/templates" "go.alanpearce.eu/x/log"@@ -25,10 +25,10 @@ "gitlab.com/tozd/go/errors" ) type Options struct { - Source string `conf:"default:.,short:s,flag:src"` - Destination string `conf:"default:public,short:d,flag:dest"` - Development bool `conf:"default:false,flag:dev"` - DB *sql.DB + Source string `conf:"default:.,short:s,flag:src"` + Destination string `conf:"default:public,short:d,flag:dest"` + Development bool `conf:"default:false,flag:dev"` + Repo *vcs.Repository `conf:"-"` } type Result struct {@@ -78,6 +78,7 @@ log.Debug("reading posts", "source", options.Source) cc, err := content.NewContentCollection(&content.Config{ Root: options.Source, PostDir: "post", + Repo: options.Repo, }, log.Named("content")) if err != nil { return nil, err
M internal/content/posts.go → internal/content/posts.go
@@ -13,6 +13,7 @@ "strings" "time" "go.alanpearce.eu/website/internal/markdown" + "go.alanpearce.eu/website/internal/vcs" "go.alanpearce.eu/x/log" "github.com/adrg/frontmatter"@@ -34,6 +35,7 @@ Input string Output string Basename string URL string + Commits []*vcs.Commit *PostMatter content []byte@@ -42,6 +44,7 @@ type Config struct { Root string PostDir string + Repo *vcs.Repository } type Collection struct {@@ -66,15 +69,20 @@ func (cc *Collection) GetPost(filename string) (*Post, error) { fp := filepath.Join(cc.config.Root, filename) url := path.Join("/", postURLReplacer.Replace(filename)) + "/" + cs, err := cc.config.Repo.GetFileLog(filename) + if err != nil { + return nil, errors.WithMessagef(err, "could not get commit log for file %s", filename) + } post := &Post{ Input: fp, Output: postOutputReplacer.Replace(filename), Basename: filepath.Base(url), URL: url, PostMatter: &PostMatter{}, + Commits: cs, } - err := parse(fp, post) + err = parse(fp, post) if err != nil { return nil, err }
A internal/vcs/filelog.go
@@ -0,0 +1,63 @@ +package vcs + +import ( + "net/url" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "gitlab.com/tozd/go/errors" +) + +type Author struct { + Name string + Email string +} + +type Commit struct { + Hash string + Message string + Author Author + Date time.Time + Link *url.URL +} + +func (r *Repository) makeCommitURL(hash string) *url.URL { + u := r.remoteURL.JoinPath("commit") + q := u.Query() + q.Add("id", hash) + u.RawQuery = q.Encode() + + return u +} + +func (r *Repository) GetFileLog(filename string) (cs []*Commit, err error) { + fl, err := r.repo.Log(&git.LogOptions{ + Order: git.LogOrderCommitterTime, + FileName: &filename, + }) + if err != nil { + return nil, errors.WithMessagef(err, "could not get git log for file %s", filename) + } + + defer fl.Close() + err = fl.ForEach(func(c *object.Commit) error { + cs = append(cs, &Commit{ + Hash: c.Hash.String(), + Message: c.Message, + Author: Author{ + Name: c.Author.Name, + Email: c.Author.Email, + }, + Date: c.Author.When, + Link: r.makeCommitURL(c.Hash.String()), + }) + + return nil + }) + if err != nil { + return nil, errors.WithMessagef(err, "could not iterate over commits for file %s", filename) + } + + return cs, nil +}
M internal/vcs/repository.go → internal/vcs/repository.go
@@ -19,8 +19,9 @@ Branch string } type Repository struct { - repo *git.Repository - log *log.Logger + repo *git.Repository + log *log.Logger + remoteURL config.URL } func CloneOrOpen(cfg *Options, log *log.Logger) (repo *Repository, exists bool, err error) {@@ -30,7 +31,8 @@ if err != nil && !errors.Is(err, fs.ErrNotExist) { return } repo = &Repository{ - log: log, + log: log, + remoteURL: cfg.RemoteURL, } if stat == nil { repo.repo, err = git.PlainClone(cfg.LocalPath, false, &git.CloneOptions{
M internal/website/mux.go → internal/website/mux.go
@@ -71,6 +71,7 @@ }, log.Named("vcs")) if err != nil { return nil, errors.WithMessage(err, "could not open repository") } + builderOptions.Repo = repo if exists && !opts.Development { _, err := repo.Update()
M templates/list.templ → templates/list.templ
@@ -42,7 +42,7 @@ <ul class="h-feed"> for _, post := range posts { <li class="h-entry"> <span> - @postDate(post.Date) + @postDate(post.Date, "dt-published") </span> <a class="p-name u-url" href={ templ.SafeURL(post.URL) }>{ post.Title }</a> </li>
M templates/post.templ → templates/post.templ
@@ -16,8 +16,8 @@ return }) } -templ postDate(d time.Time) { - <time class="dt-published" datetime={ d.UTC().Format(time.RFC3339) }> +templ postDate(d time.Time, class string) { + <time class={ class } datetime={ d.UTC().Format(time.RFC3339) }> { d.Format("2006-01-02") } </time> }@@ -35,12 +35,26 @@ }, Path: post.URL, }) { <article> - <h1 class="p-name">{ post.Title }</h1> - <p> - <a class="u-url" href={ templ.SafeURL(post.URL) }> - @postDate(post.Date) - </a> - </p> + <header> + <h1 class="p-name">{ post.Title }</h1> + <p class="meta"> + <span class="date"> + Published: + <a class="u-url" href={ templ.SafeURL(post.URL) }> + @postDate(post.Date, "dt-published") + </a> + </span> + // one commit: not updated + if len(post.Commits) > 1 { + <span class="date last-updated"> + Last updated: + <a href={ templ.URL(post.Commits[0].Link.String()) }> + @postDate(post.Commits[0].Date, "dt-updated") + </a> + </span> + } + </p> + </header> <div class="e-content"> @post </div>
M templates/style.css → templates/style.css
@@ -186,6 +186,20 @@ .skip:focus { top: 0; } +article > header > h1 { + margin-bottom: 0; +} + +.meta { + margin-top: 0; +} + +.date { + display: inline-block; + margin-right: 1ex; + font-size: small; +} + .tags { font-size: small; display: inline-block;