Browse Source

Link the About page's commit hash to the commit in Gogs

The About page's build hash now links to <repoURL>/commit/<full hash>.
repoURL is a new optional site.json setting, defaulting to this repo,
so forks can point at their own. Dirty builds, which match no commit,
show -dirty with no link. Version 0.5.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude 10 hours ago
parent
commit
a756b16dc9
7 changed files with 88 additions and 26 deletions
  1. 3 1
      README.md
  2. 4 0
      config.go
  3. 11 6
      main.go
  4. 46 0
      main_test.go
  5. 4 2
      render.go
  6. 1 1
      templates/about.html
  7. 19 16
      version.go

+ 3 - 1
README.md

@@ -63,7 +63,8 @@ and take effect without a restart.
 ```json
 {
   "homeText": "Home",
-  "defaultAuthor": "Claude"
+  "defaultAuthor": "Claude",
+  "repoURL": "https://git.monoaf.com/bajdev/posts"
 }
 ```
 
@@ -71,6 +72,7 @@ and take effect without a restart.
 |---|---|---|
 | `homeText` | `Home` | Text of the header link to the index page |
 | `defaultAuthor` | `Claude` | Author shown for posts without an `author` line |
+| `repoURL` | `https://git.monoaf.com/bajdev/posts` | Source repo; the About page links the running commit to `<repoURL>/commit/<hash>` |
 
 A missing file, a missing setting or a blank value uses the default. If the
 file is invalid JSON or has an unknown setting name, the error is logged and

+ 4 - 0
config.go

@@ -15,6 +15,7 @@ import (
 type siteConfig struct {
 	HomeText      string `json:"homeText"`      // text of the header link to the index
 	DefaultAuthor string `json:"defaultAuthor"` // author of posts that don't name one
+	RepoURL       string `json:"repoURL"`       // web address of the source repo, for commit links
 }
 
 // withDefaults fills in any setting that is missing or blank.
@@ -22,6 +23,9 @@ func (c siteConfig) withDefaults() siteConfig {
 	if strings.TrimSpace(c.HomeText) == "" {
 		c.HomeText = "Home"
 	}
+	if strings.TrimSpace(c.RepoURL) == "" {
+		c.RepoURL = "https://git.monoaf.com/bajdev/posts"
+	}
 	if strings.TrimSpace(c.DefaultAuthor) == "" {
 		c.DefaultAuthor = "Claude"
 	}

+ 11 - 6
main.go

@@ -22,7 +22,7 @@ type app struct {
 	about    template.HTML
 	siteURL  string // absolute base URL, no trailing slash
 	siteName string
-	commit   string
+	commit   commitInfo
 
 	releaseFS   fs.FS // pull-releases/
 	releases    []*Release
@@ -141,12 +141,17 @@ func (a *app) aboutPage(w http.ResponseWriter, r *http.Request) {
 		a.serverError(w, err)
 		return
 	}
+	var commitURL string
+	if a.commit.Full != "" && !a.commit.Dirty {
+		commitURL = strings.TrimRight(a.config.Get().RepoURL, "/") + "/commit/" + a.commit.Full
+	}
 	a.render(w, http.StatusOK, "about", pageData{
-		Posts:    idx.posts,
-		Releases: a.releases,
-		Version:  version,
-		Commit:   a.commit,
-		About:    a.about,
+		Posts:     idx.posts,
+		Releases:  a.releases,
+		Version:   version,
+		Commit:    a.commit,
+		CommitURL: commitURL,
+		About:     a.about,
 	})
 }
 

+ 46 - 0
main_test.go

@@ -304,6 +304,52 @@ func TestHeaderAndFooter(t *testing.T) {
 	}
 }
 
+func TestAboutCommitLink(t *testing.T) {
+	const full = "d2a387e186c8caf21a35349b8c3d60f68fb87b1f"
+	about := func(t *testing.T, c commitInfo, siteJSON string) string {
+		t.Helper()
+		dir := t.TempDir()
+		if siteJSON != "" {
+			writeFile(t, dir, "site.json", siteJSON)
+		}
+		a, err := newApp(dir, "https://posts.example.com")
+		if err != nil {
+			t.Fatal(err)
+		}
+		a.commit = c // go test binaries carry no VCS stamp; set one
+		srv := httptest.NewServer(a.routes())
+		t.Cleanup(srv.Close)
+		_, _, body := get(t, srv, "/about")
+		return body
+	}
+
+	t.Run("clean build links the full hash", func(t *testing.T) {
+		body := about(t, commitInfo{Full: full, Short: full[:7]}, "")
+		want := `<span class="muted">(<a href="https://git.monoaf.com/bajdev/posts/commit/` + full + `">d2a387e</a>)</span>`
+		if !strings.Contains(body, want) {
+			t.Errorf("missing %s in:\n%s", want, body)
+		}
+	})
+	t.Run("repoURL from site.json", func(t *testing.T) {
+		body := about(t, commitInfo{Full: full, Short: full[:7]}, `{"repoURL": "https://example.org/me/fork/"}`)
+		if !strings.Contains(body, `href="https://example.org/me/fork/commit/`+full+`"`) {
+			t.Errorf("site.json repoURL not used:\n%s", body)
+		}
+	})
+	t.Run("dirty build is not linked", func(t *testing.T) {
+		body := about(t, commitInfo{Full: full, Short: full[:7], Dirty: true}, "")
+		if !strings.Contains(body, `(d2a387e-dirty)`) || strings.Contains(body, "/commit/") {
+			t.Errorf("dirty build should show -dirty with no link:\n%s", body)
+		}
+	})
+	t.Run("no commit shows version only", func(t *testing.T) {
+		body := about(t, commitInfo{}, "")
+		if !strings.Contains(body, "<dd>"+version+"</dd>") {
+			t.Errorf("want bare version:\n%s", body)
+		}
+	})
+}
+
 func TestAboutPage(t *testing.T) {
 	srv, _ := newTestServer(t, sample)
 	status, _, body := get(t, srv, "/about")

+ 4 - 2
render.go

@@ -67,8 +67,10 @@ type pageData struct {
 	Message      string
 
 	// About page only.
-	Version, Commit string
-	About           template.HTML
+	Version   string
+	Commit    commitInfo
+	CommitURL string // empty when the build doesn't match a commit exactly
+	About     template.HTML
 
 	// Release notes pages (and the About page's link to them).
 	Releases []*Release

+ 1 - 1
templates/about.html

@@ -4,7 +4,7 @@
   <h1>About</h1>
   <dl class="facts">
     <dt>Version</dt>
-    <dd>{{.Version}}{{with .Commit}} <span class="muted">({{.}})</span>{{end}}</dd>
+    <dd>{{.Version}}{{with .Commit.Short}} <span class="muted">({{if $.CommitURL}}<a href="{{$.CommitURL}}">{{.}}</a>{{else}}{{.}}{{if $.Commit.Dirty}}-dirty{{end}}{{end}})</span>{{end}}</dd>
     <dt>Release notes</dt>
     <dd><a href="/releases">{{len .Releases}} releases</a></dd>
     <dt>Posts</dt>

+ 19 - 16
version.go

@@ -4,31 +4,34 @@ import "runtime/debug"
 
 // version is the release number shown on the About page. Bump it in the PR
 // that makes a user-visible change.
-const version = "0.5.2"
+const version = "0.5.3"
 
-// buildCommit returns the short git revision that `go build` stamped into the
-// binary, with "-dirty" if the tree had uncommitted changes, or "" if the
-// binary was built outside a git checkout.
-func buildCommit() string {
+// commitInfo is the git revision `go build` stamped into the binary. Full is
+// empty when the binary was built outside a git checkout (for example by
+// `go test`); Dirty means the tree had uncommitted changes, so the build
+// doesn't match any commit exactly.
+type commitInfo struct {
+	Full, Short string
+	Dirty       bool
+}
+
+func buildCommit() commitInfo {
 	info, ok := debug.ReadBuildInfo()
 	if !ok {
-		return ""
+		return commitInfo{}
 	}
-	var rev string
-	var dirty bool
+	var c commitInfo
 	for _, s := range info.Settings {
 		switch s.Key {
 		case "vcs.revision":
-			rev = s.Value
+			c.Full = s.Value
 		case "vcs.modified":
-			dirty = s.Value == "true"
+			c.Dirty = s.Value == "true"
 		}
 	}
-	if len(rev) > 7 {
-		rev = rev[:7]
-	}
-	if rev != "" && dirty {
-		rev += "-dirty"
+	c.Short = c.Full
+	if len(c.Short) > 7 {
+		c.Short = c.Short[:7]
 	}
-	return rev
+	return c
 }