overview
tree
log
refs
☰
Show tag and branch decorations next to commits
The log, overview, and commit views carried no ref context, so a
release commit looked like any other.
Decorate commits with clickable tag and branch-head pills (git's %D),
each linking to the log from that ref.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
diff --git a/internal/git/git.go b/internal/git/git.go
index b8982bb..bbbc521 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -49,6 +49,13 @@ type Commit struct {
Author string
Date time.Time
Subject string
+ Refs []CommitRef // branches/tags pointing here, for decoration
+}
+
+// CommitRef is a branch or tag pointing at a commit (git's %D).
+type CommitRef struct {
+ Name string
+ Tag bool
}
// CommitDetail is a full commit as shown on the commit page.
@@ -369,11 +376,11 @@ func (r *Repo) DefaultBranch(candidates []string) (string, Hash, error) {
return "", "", ErrNotFound
}
-var logFormat = strings.Join([]string{"%H", "%an", "%at", "%s"}, fieldSep) + recordSep
+var logFormat = strings.Join([]string{"%H", "%an", "%at", "%s", "%D"}, fieldSep) + recordSep
// Log returns up to n commits starting from sha.
func (r *Repo) Log(sha Hash, n int) ([]*Commit, error) {
- out, err := r.git("log", "-n", strconv.Itoa(n), "--format="+logFormat, string(sha), "--")
+ out, err := r.git("log", "-n", strconv.Itoa(n), "--decorate", "--format="+logFormat, string(sha), "--")
if err != nil {
return nil, err
}
@@ -384,7 +391,7 @@ func (r *Repo) Log(sha Hash, n int) ([]*Commit, error) {
// query, matched case-insensitively and verbatim (no regexp).
func (r *Repo) Search(sha Hash, query string, n int) ([]*Commit, error) {
out, err := r.git("log", "-n", strconv.Itoa(n), "-i", "--fixed-strings",
- "--grep="+query, "--format="+logFormat, string(sha), "--")
+ "--grep="+query, "--decorate", "--format="+logFormat, string(sha), "--")
if err != nil {
return nil, err
}
@@ -395,7 +402,7 @@ func parseLog(out []byte) []*Commit {
var commits []*Commit
for _, rec := range strings.Split(string(out), recordSep) {
f := strings.Split(strings.TrimPrefix(rec, "\n"), fieldSep)
- if len(f) < 4 {
+ if len(f) < 5 {
continue
}
commits = append(commits, &Commit{
@@ -403,11 +410,35 @@ func parseLog(out []byte) []*Commit {
Author: f[1],
Date: unixTime(f[2]),
Subject: f[3],
+ Refs: parseDecoration(f[4]),
})
}
return commits
}
+// parseDecoration turns git's %D ("HEAD -> main, tag: v1.0, origin/main")
+// into typed refs, dropping the bare HEAD pointer.
+func parseDecoration(d string) []CommitRef {
+ d = strings.TrimSpace(d)
+ if d == "" {
+ return nil
+ }
+ var refs []CommitRef
+ for _, part := range strings.Split(d, ", ") {
+ switch {
+ case part == "HEAD":
+ continue
+ case strings.HasPrefix(part, "HEAD -> "):
+ refs = append(refs, CommitRef{Name: strings.TrimPrefix(part, "HEAD -> ")})
+ case strings.HasPrefix(part, "tag: "):
+ refs = append(refs, CommitRef{Name: strings.TrimPrefix(part, "tag: "), Tag: true})
+ default:
+ refs = append(refs, CommitRef{Name: part})
+ }
+ }
+ return refs
+}
+
func (r *Repo) refs(patterns ...string) ([]*Ref, error) {
args := []string{"for-each-ref", "--sort=-creatordate",
"--format=%(refname)" + fieldSep + "%(refname:short)" + fieldSep +
@@ -517,24 +548,24 @@ func (r *Repo) Tree(sha Hash, path string) ([]TreeEntry, error) {
return ents, nil
}
-var showFormat = strings.Join([]string{"%H", "%P", "%an", "%at", "%s", "%b"}, fieldSep) + recordSep
+var showFormat = strings.Join([]string{"%H", "%P", "%an", "%at", "%s", "%b", "%D"}, fieldSep) + recordSep
// Show returns full commit details including the diff against its
// first parent, reading at most max bytes of git output. The second
// return value reports whether the diff was truncated.
func (r *Repo) Show(sha Hash, max int64) (*CommitDetail, bool, error) {
out, truncated, err := r.gitLimited(max,
- "show", "--no-color", "--format="+showFormat, string(sha), "--")
+ "show", "--no-color", "--decorate", "--format="+showFormat, string(sha), "--")
if err != nil {
return nil, false, ErrNotFound
}
head, diff, _ := strings.Cut(string(out), recordSep)
f := strings.Split(head, fieldSep)
- if len(f) < 6 {
+ if len(f) < 7 {
return nil, false, fmt.Errorf("unexpected git show output for %s", sha)
}
return &CommitDetail{
- Commit: Commit{Hash: f[0], Author: f[2], Date: unixTime(f[3]), Subject: f[4]},
+ Commit: Commit{Hash: f[0], Author: f[2], Date: unixTime(f[3]), Subject: f[4], Refs: parseDecoration(f[6])},
Parents: strings.Fields(f[1]),
Body: strings.TrimSpace(f[5]),
Diff: strings.TrimLeft(diff, "\n"),
diff --git a/static/style.css b/static/style.css
index 3bf240a..da0a908 100644
--- a/static/style.css
+++ b/static/style.css
@@ -285,6 +285,26 @@ td.idle, td.author {
white-space: nowrap;
}
+/* Ref decorations (tags/branch heads) shown next to commits. */
+.ref {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.2rem;
+ margin-right: 0.4rem;
+ padding: 0 0.45rem;
+ border: 1px solid;
+ border-color: color-mix(in srgb, var(--c) 40%, var(--border));
+ border-radius: 999px;
+ font-size: 0.72rem;
+ line-height: 1.55;
+ white-space: nowrap;
+ vertical-align: middle;
+ color: var(--c);
+}
+.ref.tag { --c: var(--warn); }
+.ref.branch { --c: var(--accent); }
+.ref:hover { text-decoration: none; border-color: currentColor; }
+
code, pre, .sha, td.mode, kbd {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.875rem;
diff --git a/templates/commit.html b/templates/commit.html
index ee897af..d7be9df 100644
--- a/templates/commit.html
+++ b/templates/commit.html
@@ -7,7 +7,7 @@
{{with .Body}}<pre class="body">{{.}}</pre>{{end}}
<div class="commitmeta">
<span class="who">{{.Author}} · {{ago .Date}}</span>
- <span class="refs"><span class="sha" title="{{.Hash}}">commit {{shortsha .Hash}}</span>{{range $i, $p := .Parents}} · {{if eq $i 0}}parent{{else}}other parent{{end}} <a class="sha"{{if eq $i 0}} data-nav="parent" title="parent (p)"{{else if eq $i 1}} data-nav="parent2" title="other parent (o)"{{end}} href="{{url $.Data.Name "commit" $p}}">{{shortsha $p}}</a>{{end}}{{with $.Data.Child}} · child <a class="sha" data-nav="child" title="child (c)" href="{{url $.Data.Name "commit" .}}">{{shortsha .}}</a>{{end}} · <a href="{{url $.Data.Name "commit" (printf "%s.patch" .Hash)}}">patch</a></span>
+ <span class="refs">{{range .Refs}}<a class="ref {{if .Tag}}tag{{else}}branch{{end}}" href="{{url $.Data.Name "log" .Name}}" title="log from {{.Name}}">{{if .Tag}}🏷{{else}}⎇{{end}} {{.Name}}</a>{{end}}<span class="sha" title="{{.Hash}}">commit {{shortsha .Hash}}</span>{{range $i, $p := .Parents}} · {{if eq $i 0}}parent{{else}}other parent{{end}} <a class="sha"{{if eq $i 0}} data-nav="parent" title="parent (p)"{{else if eq $i 1}} data-nav="parent2" title="other parent (o)"{{end}} href="{{url $.Data.Name "commit" $p}}">{{shortsha $p}}</a>{{end}}{{with $.Data.Child}} · child <a class="sha" data-nav="child" title="child (c)" href="{{url $.Data.Name "commit" .}}">{{shortsha .}}</a>{{end}} · <a href="{{url $.Data.Name "commit" (printf "%s.patch" .Hash)}}">patch</a></span>
</div>
</div>
{{end}}
diff --git a/templates/layout.html b/templates/layout.html
index 94cde82..3e4e1c5 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -96,7 +96,7 @@
{{range .Commits}}
<tr>
<td class="sha"><a href="{{url $.Name "commit" .Hash}}">{{shortsha .Hash}}</a></td>
- <td class="subject">{{.Subject}}</td>
+ <td class="subject">{{range .Refs}}<a class="ref {{if .Tag}}tag{{else}}branch{{end}}" href="{{url $.Name "log" .Name}}" title="log from {{.Name}}">{{if .Tag}}🏷{{else}}⎇{{end}} {{.Name}}</a>{{end}}{{.Subject}}</td>
<td class="author">{{.Author}}</td>
<td class="idle">{{ago .Date}}</td>
</tr>