web: link resolved author names to their profiles !70

merged merged by cmc on 2026-08-26 03:33 UTC · krz/gitbay:author-links into main

9 files changed, +60 −26

Layout: unified · split

README.org +2
@@ -1,6 +1,8 @@
11#+title: gitbay
22#+author: Christian Cleberg
33
4[[https://gitbay.org/krz/gitbay/builds][file:https://gitbay.org/krz/gitbay/badge/build.svg]]
5
46A CLI-first git forge. One binary, SQLite, and the system =git= — designed
57so the command line is the product and the web UI is a rendering of state
68the CLI already manages. Runs at [[https://gitbay.org]].
e2e/design_test.go +15
@@ -279,6 +279,10 @@ func TestAuthorNamesResolve(t *testing.T) {
279279 os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b\n"), 0o644)
280280 mustGit(t, dir, stranger, "add", ".")
281281 mustGit(t, dir, stranger, "commit", "-q", "-m", "from a stranger")
282 // The tip is the account's, so the bar above the listing shows a link.
283 os.WriteFile(filepath.Join(dir, "c.txt"), []byte("c\n"), 0o644)
284 mustGit(t, dir, known, "add", ".")
285 mustGit(t, dir, known, "commit", "-q", "-m", "back to the account")
282286 mustGit(t, dir, known, "push", "-q", "origin", "main")
283287
284288 // The log shows the account name for the verified address only.
@@ -292,4 +296,15 @@ func TestAuthorNamesResolve(t *testing.T) {
292296 if !strings.Contains(body, "alice") {
293297 t.Fatalf("log missing the account name:\n%s", body)
294298 }
299 // A resolved name links to the profile; an unknown one stays text.
300 if !strings.Contains(body, `class="authorlink" href="/alice"`) {
301 t.Fatalf("account name is not a link:\n%s", body)
302 }
303 if strings.Contains(body, `href="/Outside Person"`) {
304 t.Fatalf("unknown author was linked:\n%s", body)
305 }
306 // The tipbar resolves and links the same way when the tip is an account's.
307 if _, tree := inst.get(t, "/alice/app"); !strings.Contains(tree, `class="authorlink" href="/alice"`) {
308 t.Fatalf("tipbar name is not a link:\n%s", tree)
309 }
295310}
internal/httpd/control.go +23 −12
@@ -108,33 +108,44 @@ func (a *authorNames) name(email, fallback string) string {
108108 return name
109109}
110110
111// known reports whether the address belongs to an account, so callers can
112// decide to link the name.
113func (a *authorNames) known(email string) bool {
111// account returns the account name behind an address, if any, so callers
112// can link the displayed name to a profile.
113func (a *authorNames) account(email string) (string, bool) {
114114 if email == "" {
115 return false
115 return "", false
116116 }
117117 if got, ok := a.cache[email]; ok {
118 return got != ""
118 return got, got != ""
119119 }
120120 name, _ := a.st.UsernameByVerifiedEmail(email)
121121 a.cache[email] = name
122 return name != ""
122 return name, name != ""
123}
124
125// namedCommit is a listing commit plus the account behind its author
126// address, when there is one, so the name can link to a profile.
127type namedCommit struct {
128 gitutil.EntryCommit
129 User string
123130}
124131
125132// namedCommits rewrites listing authors to account names where the
126133// address is verified here.
127func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]gitutil.EntryCommit {
134func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]namedCommit {
128135 names := s.authorNames()
136 out := make(map[string]namedCommit, len(m))
129137 for k, c := range m {
138 user, _ := names.account(c.Email)
130139 c.Author = names.name(c.Email, c.Author)
131 m[k] = c
140 out[k] = namedCommit{EntryCommit: c, User: user}
132141 }
133 return m
142 return out
134143}
135144
136145// namedTip does the same for the single commit above a tree listing.
137func (s *Server) namedTip(c gitutil.EntryCommit) gitutil.EntryCommit {
138 c.Author = s.authorNames().name(c.Email, c.Author)
139 return c
146func (s *Server) namedTip(c gitutil.EntryCommit) namedCommit {
147 names := s.authorNames()
148 user, _ := names.account(c.Email)
149 c.Author = names.name(c.Email, c.Author)
150 return namedCommit{EntryCommit: c, User: user}
140151}
internal/httpd/web.go +14 −10
@@ -435,8 +435,8 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
435435 Branches []gitutil.Ref
436436 ReadmeName string
437437 ReadmeHTML template.HTML
438 LastCommits map[string]gitutil.EntryCommit
439 Tip gitutil.EntryCommit
438 LastCommits map[string]namedCommit
439 Tip namedCommit
440440 }{repoPage: p, RefKind: "tree"})
441441 return
442442 }
@@ -479,8 +479,8 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
479479 Branches []gitutil.Ref
480480 ReadmeName string
481481 ReadmeHTML template.HTML
482 LastCommits map[string]gitutil.EntryCommit
483 Tip gitutil.EntryCommit
482 LastCommits map[string]namedCommit
483 Tip namedCommit
484484 }{p, crumbs(p, "tree", dirPath), prefix, dirPath, "tree", entries, branches,
485485 readmeName, readmeHTML,
486486 s.namedCommits(gitutil.LastCommits(p.Dir, p.Ref, dirPath, names)),
@@ -1201,8 +1201,8 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) {
12011201 shas = shas[:pageSize]
12021202 }
12031203 type row struct {
1204 SHA, ShortSHA, Subject, AuthorName, AuthorEmail, Date string
1205 Sig sigView
1204 SHA, ShortSHA, Subject, AuthorName, AuthorEmail, AuthorUser, Date string
1205 Sig sigView
12061206 }
12071207 names := s.authorNames()
12081208 var rows []row
@@ -1212,6 +1212,7 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) {
12121212 if parsed != nil {
12131213 rw.Subject = parsed.Subject
12141214 rw.AuthorName = names.name(parsed.AuthorEmail, parsed.AuthorName)
1215 rw.AuthorUser, _ = names.account(parsed.AuthorEmail)
12151216 rw.AuthorEmail = parsed.AuthorEmail
12161217 rw.Date = time.Unix(parsed.AuthorUnix, 0).UTC().Format("2006-01-02")
12171218 }
@@ -1249,18 +1250,20 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) {
12491250 committerEmail = parsed.CommitterEmail
12501251 }
12511252 checks, _ := s.st.ListCommitStatuses(p.Repo.ID, full)
1253 commitNames := s.authorNames()
1254 commitUser, _ := commitNames.account(parsed.AuthorEmail)
12521255 msg := ""
12531256 if i := bytes.Index(parsed.Payload, []byte("\n\n")); i >= 0 {
12541257 msg = string(parsed.Payload[i+2:])
12551258 }
12561259 s.render(w, "commit.html", struct {
12571260 repoPage
1258 SHA, ShortSHA, AuthorName, AuthorEmail, CommitterEmail, Date, Message string
1261 SHA, ShortSHA, AuthorName, AuthorEmail, AuthorUser, CommitterEmail, Date, Message string
12591262 Parents []string
12601263 Sig sigView
12611264 Checks []store.CommitStatus
12621265 DiffLines []diffLine
1263 }{p, full, full[:10], s.authorNames().name(parsed.AuthorEmail, parsed.AuthorName), parsed.AuthorEmail, committerEmail,
1266 }{p, full, full[:10], commitNames.name(parsed.AuthorEmail, parsed.AuthorName), parsed.AuthorEmail, commitUser, committerEmail,
12641267 time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339), msg,
12651268 gitutil.Parents(p.Dir, full), v, checks, lines})
12661269}
@@ -1482,8 +1485,8 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
14821485 }
14831486 // The commits this MR carries: base..head, the same range as the diff.
14841487 type commitRow struct {
1485 SHA, ShortSHA, Subject, AuthorName, Date string
1486 Sig sigView
1488 SHA, ShortSHA, Subject, AuthorName, AuthorUser, Date string
1489 Sig sigView
14871490 }
14881491 mrNames := s.authorNames()
14891492 var commits []commitRow
@@ -1499,6 +1502,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
14991502 if parsed != nil {
15001503 cr.Subject = parsed.Subject
15011504 cr.AuthorName = mrNames.name(parsed.AuthorEmail, parsed.AuthorName)
1505 cr.AuthorUser, _ = mrNames.account(parsed.AuthorEmail)
15021506 cr.Date = time.Unix(parsed.AuthorUnix, 0).UTC().Format("2006-01-02")
15031507 }
15041508 commits = append(commits, cr)
internal/web/templates/commit.html +1 −1
@@ -5,7 +5,7 @@
55 <p class="meta"><code class="fullsha">{{.SHA}}</code></p>
66 {{if .Parents}}<p class="meta">parent{{if gt (len .Parents) 1}}s{{end}}:{{range .Parents}} <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.}}">{{short .}}</a></code>{{end}}</p>{{end}}
77 <p>{{template "sigbadge" .Sig}}{{range .Checks}} <span class="badge check-{{.State}}">{{.Context}}: {{.State}}</span>{{end}}</p>
8 <p class="meta"><span title="{{.AuthorEmail}}">{{.AuthorName}}</span> &lt;{{.AuthorEmail}}&gt; · {{.Date}}
8 <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" .AuthorEmail}} &lt;{{.AuthorEmail}}&gt; · {{.Date}}
99 {{if .CommitterEmail}}<br>committer: &lt;{{.CommitterEmail}}&gt;{{end}}</p>
1010</div>
1111<pre class="message">{{.Message}}</pre>
internal/web/templates/layout.html +2
@@ -106,3 +106,5 @@
106106{{define "branchicon"}}<svg class="icon" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>{{end}}
107107
108108{{define "sigbadge"}}<span class="badge badge-{{.State}}" title="{{.Fingerprint}}">{{sigLabel .State}}{{if .Signer}} · {{.Signer}}{{end}}</span>{{end}}
109
110{{define "authorname"}}{{if .User}}<a class="authorlink" href="/{{.User}}" title="{{.Email}}">{{.Name}}</a>{{else}}<span title="{{.Email}}">{{.Name}}</span>{{end}}{{end}}
internal/web/templates/log.html +1 −1
@@ -6,7 +6,7 @@
66{{range .Commits}}<li>
77 <div class="commitmain">
88 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p>
9 <p class="meta"><span title="{{.AuthorEmail}}">{{.AuthorName}}</span> · {{.Date}}</p>
9 <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" .AuthorEmail}} · {{.Date}}</p>
1010 </div>
1111 <div class="commitside">
1212 {{template "sigbadge" .Sig}}
internal/web/templates/mr.html +1 −1
@@ -48,7 +48,7 @@
4848{{range .Commits}}<li>
4949 <div class="commitmain">
5050 <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p>
51 <p class="meta">{{.AuthorName}} · {{.Date}}</p>
51 <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" ""}} · {{.Date}}</p>
5252 </div>
5353 <div class="commitside">
5454 {{template "sigbadge" .Sig}}
internal/web/templates/tree.html +1 −1
@@ -11,7 +11,7 @@
1111</div>
1212{{if .Entries}}<p class="clone">Clone: <code>git clone {{.CloneURL}}</code></p>{{end}}
1313{{with .Tip}}{{if .SHA}}<div class="tipbar">
14 <span class="who">{{.Author}}</span>
14 <span class="who">{{template "authorname" dict "Name" .Author "User" .User "Email" .Email}}</span>
1515 <a class="subject" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a>
1616 <span class="spacer"></span>
1717 <a class="sha" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}"><code>{{short .SHA}}</code></a>