Commit f65bfc3c4e
Verified · cmc ci/build: success
README.org +2
| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | #+title: gitbay |
| 2 | 2 | #+author: Christian Cleberg |
| 3 | 3 | |
| 4 | [[https://gitbay.org/krz/gitbay/builds][file:https://gitbay.org/krz/gitbay/badge/build.svg]] | |
| 5 | ||
| 4 | 6 | A CLI-first git forge. One binary, SQLite, and the system =git= — designed |
| 5 | 7 | so the command line is the product and the web UI is a rendering of state |
| 6 | 8 | the CLI already manages. Runs at [[https://gitbay.org]]. |
e2e/design_test.go +15
| @@ -279,6 +279,10 @@ func TestAuthorNamesResolve(t *testing.T) { | ||
| 279 | 279 | os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b\n"), 0o644) |
| 280 | 280 | mustGit(t, dir, stranger, "add", ".") |
| 281 | 281 | 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") | |
| 282 | 286 | mustGit(t, dir, known, "push", "-q", "origin", "main") |
| 283 | 287 | |
| 284 | 288 | // The log shows the account name for the verified address only. |
| @@ -292,4 +296,15 @@ func TestAuthorNamesResolve(t *testing.T) { | ||
| 292 | 296 | if !strings.Contains(body, "alice") { |
| 293 | 297 | t.Fatalf("log missing the account name:\n%s", body) |
| 294 | 298 | } |
| 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 | } | |
| 295 | 310 | } |
internal/httpd/control.go +23 −12
| @@ -108,33 +108,44 @@ func (a *authorNames) name(email, fallback string) string { | ||
| 108 | 108 | return name |
| 109 | 109 | } |
| 110 | 110 | |
| 111 | // known reports whether the address belongs to an account, so callers can | |
| 112 | // decide to link the name. | |
| 113 | func (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. | |
| 113 | func (a *authorNames) account(email string) (string, bool) { | |
| 114 | 114 | if email == "" { |
| 115 | return false | |
| 115 | return "", false | |
| 116 | 116 | } |
| 117 | 117 | if got, ok := a.cache[email]; ok { |
| 118 | return got != "" | |
| 118 | return got, got != "" | |
| 119 | 119 | } |
| 120 | 120 | name, _ := a.st.UsernameByVerifiedEmail(email) |
| 121 | 121 | 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. | |
| 127 | type namedCommit struct { | |
| 128 | gitutil.EntryCommit | |
| 129 | User string | |
| 123 | 130 | } |
| 124 | 131 | |
| 125 | 132 | // namedCommits rewrites listing authors to account names where the |
| 126 | 133 | // address is verified here. |
| 127 | func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]gitutil.EntryCommit { | |
| 134 | func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]namedCommit { | |
| 128 | 135 | names := s.authorNames() |
| 136 | out := make(map[string]namedCommit, len(m)) | |
| 129 | 137 | for k, c := range m { |
| 138 | user, _ := names.account(c.Email) | |
| 130 | 139 | c.Author = names.name(c.Email, c.Author) |
| 131 | m[k] = c | |
| 140 | out[k] = namedCommit{EntryCommit: c, User: user} | |
| 132 | 141 | } |
| 133 | return m | |
| 142 | return out | |
| 134 | 143 | } |
| 135 | 144 | |
| 136 | 145 | // namedTip does the same for the single commit above a tree listing. |
| 137 | func (s *Server) namedTip(c gitutil.EntryCommit) gitutil.EntryCommit { | |
| 138 | c.Author = s.authorNames().name(c.Email, c.Author) | |
| 139 | return c | |
| 146 | func (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} | |
| 140 | 151 | } |
internal/httpd/web.go +14 −10
| @@ -435,8 +435,8 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, | ||
| 435 | 435 | Branches []gitutil.Ref |
| 436 | 436 | ReadmeName string |
| 437 | 437 | ReadmeHTML template.HTML |
| 438 | LastCommits map[string]gitutil.EntryCommit | |
| 439 | Tip gitutil.EntryCommit | |
| 438 | LastCommits map[string]namedCommit | |
| 439 | Tip namedCommit | |
| 440 | 440 | }{repoPage: p, RefKind: "tree"}) |
| 441 | 441 | return |
| 442 | 442 | } |
| @@ -479,8 +479,8 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, | ||
| 479 | 479 | Branches []gitutil.Ref |
| 480 | 480 | ReadmeName string |
| 481 | 481 | ReadmeHTML template.HTML |
| 482 | LastCommits map[string]gitutil.EntryCommit | |
| 483 | Tip gitutil.EntryCommit | |
| 482 | LastCommits map[string]namedCommit | |
| 483 | Tip namedCommit | |
| 484 | 484 | }{p, crumbs(p, "tree", dirPath), prefix, dirPath, "tree", entries, branches, |
| 485 | 485 | readmeName, readmeHTML, |
| 486 | 486 | s.namedCommits(gitutil.LastCommits(p.Dir, p.Ref, dirPath, names)), |
| @@ -1201,8 +1201,8 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) { | ||
| 1201 | 1201 | shas = shas[:pageSize] |
| 1202 | 1202 | } |
| 1203 | 1203 | 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 | |
| 1206 | 1206 | } |
| 1207 | 1207 | names := s.authorNames() |
| 1208 | 1208 | var rows []row |
| @@ -1212,6 +1212,7 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) { | ||
| 1212 | 1212 | if parsed != nil { |
| 1213 | 1213 | rw.Subject = parsed.Subject |
| 1214 | 1214 | rw.AuthorName = names.name(parsed.AuthorEmail, parsed.AuthorName) |
| 1215 | rw.AuthorUser, _ = names.account(parsed.AuthorEmail) | |
| 1215 | 1216 | rw.AuthorEmail = parsed.AuthorEmail |
| 1216 | 1217 | rw.Date = time.Unix(parsed.AuthorUnix, 0).UTC().Format("2006-01-02") |
| 1217 | 1218 | } |
| @@ -1249,18 +1250,20 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) { | ||
| 1249 | 1250 | committerEmail = parsed.CommitterEmail |
| 1250 | 1251 | } |
| 1251 | 1252 | checks, _ := s.st.ListCommitStatuses(p.Repo.ID, full) |
| 1253 | commitNames := s.authorNames() | |
| 1254 | commitUser, _ := commitNames.account(parsed.AuthorEmail) | |
| 1252 | 1255 | msg := "" |
| 1253 | 1256 | if i := bytes.Index(parsed.Payload, []byte("\n\n")); i >= 0 { |
| 1254 | 1257 | msg = string(parsed.Payload[i+2:]) |
| 1255 | 1258 | } |
| 1256 | 1259 | s.render(w, "commit.html", struct { |
| 1257 | 1260 | repoPage |
| 1258 | SHA, ShortSHA, AuthorName, AuthorEmail, CommitterEmail, Date, Message string | |
| 1261 | SHA, ShortSHA, AuthorName, AuthorEmail, AuthorUser, CommitterEmail, Date, Message string | |
| 1259 | 1262 | Parents []string |
| 1260 | 1263 | Sig sigView |
| 1261 | 1264 | Checks []store.CommitStatus |
| 1262 | 1265 | 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, | |
| 1264 | 1267 | time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339), msg, |
| 1265 | 1268 | gitutil.Parents(p.Dir, full), v, checks, lines}) |
| 1266 | 1269 | } |
| @@ -1482,8 +1485,8 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | ||
| 1482 | 1485 | } |
| 1483 | 1486 | // The commits this MR carries: base..head, the same range as the diff. |
| 1484 | 1487 | type commitRow struct { |
| 1485 | SHA, ShortSHA, Subject, AuthorName, Date string | |
| 1486 | Sig sigView | |
| 1488 | SHA, ShortSHA, Subject, AuthorName, AuthorUser, Date string | |
| 1489 | Sig sigView | |
| 1487 | 1490 | } |
| 1488 | 1491 | mrNames := s.authorNames() |
| 1489 | 1492 | var commits []commitRow |
| @@ -1499,6 +1502,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | ||
| 1499 | 1502 | if parsed != nil { |
| 1500 | 1503 | cr.Subject = parsed.Subject |
| 1501 | 1504 | cr.AuthorName = mrNames.name(parsed.AuthorEmail, parsed.AuthorName) |
| 1505 | cr.AuthorUser, _ = mrNames.account(parsed.AuthorEmail) | |
| 1502 | 1506 | cr.Date = time.Unix(parsed.AuthorUnix, 0).UTC().Format("2006-01-02") |
| 1503 | 1507 | } |
| 1504 | 1508 | commits = append(commits, cr) |
internal/web/templates/commit.html +1 −1
| @@ -5,7 +5,7 @@ | ||
| 5 | 5 | <p class="meta"><code class="fullsha">{{.SHA}}</code></p> |
| 6 | 6 | {{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}} |
| 7 | 7 | <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> <{{.AuthorEmail}}> · {{.Date}} | |
| 8 | <p class="meta">{{template "authorname" dict "Name" .AuthorName "User" .AuthorUser "Email" .AuthorEmail}} <{{.AuthorEmail}}> · {{.Date}} | |
| 9 | 9 | {{if .CommitterEmail}}<br>committer: <{{.CommitterEmail}}>{{end}}</p> |
| 10 | 10 | </div> |
| 11 | 11 | <pre class="message">{{.Message}}</pre> |
internal/web/templates/layout.html +2
| @@ -106,3 +106,5 @@ | ||
| 106 | 106 | {{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}} |
| 107 | 107 | |
| 108 | 108 | {{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 @@ | ||
| 6 | 6 | {{range .Commits}}<li> |
| 7 | 7 | <div class="commitmain"> |
| 8 | 8 | <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> | |
| 10 | 10 | </div> |
| 11 | 11 | <div class="commitside"> |
| 12 | 12 | {{template "sigbadge" .Sig}} |
internal/web/templates/mr.html +1 −1
| @@ -48,7 +48,7 @@ | ||
| 48 | 48 | {{range .Commits}}<li> |
| 49 | 49 | <div class="commitmain"> |
| 50 | 50 | <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> | |
| 52 | 52 | </div> |
| 53 | 53 | <div class="commitside"> |
| 54 | 54 | {{template "sigbadge" .Sig}} |
internal/web/templates/tree.html +1 −1
| @@ -11,7 +11,7 @@ | ||
| 11 | 11 | </div> |
| 12 | 12 | {{if .Entries}}<p class="clone">Clone: <code>git clone {{.CloneURL}}</code></p>{{end}} |
| 13 | 13 | {{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> | |
| 15 | 15 | <a class="subject" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a> |
| 16 | 16 | <span class="spacer"></span> |
| 17 | 17 | <a class="sha" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}"><code>{{short .SHA}}</code></a> |