tenseleyflow/shithub / 0cd1897

Browse files

Add code tree commit metadata

Authored by espadonne
SHA
0cd18974bac51de78643105fe3060d3ef8a49a1b
Parents
01ee51f
Tree
8dc2a30

8 changed files

StatusFile+-
M internal/repos/git/logops.go 14 0
M internal/repos/git/logops_test.go 8 0
M internal/web/handlers/repo/code.go 12 0
A internal/web/handlers/repo/code_tree_rows.go 67 0
A internal/web/handlers/repo/code_tree_rows_test.go 26 0
M internal/web/render/octicons.go 2 0
M internal/web/static/css/shithub.css 66 3
M internal/web/templates/repo/tree.html 26 14
internal/repos/git/logops.gomodified
@@ -88,6 +88,20 @@ func Log(ctx context.Context, gitDir string, o LogOptions) ([]Commit, error) {
8888
 	return parseLogOutput(out)
8989
 }
9090
 
91
+// CountCommits returns the number of commits reachable from ref.
92
+func CountCommits(ctx context.Context, gitDir, ref string) (int, error) {
93
+	cmd := exec.CommandContext(ctx, "git", "-C", gitDir, "rev-list", "--count", ref)
94
+	out, err := cmd.Output()
95
+	if err != nil {
96
+		return 0, wrapExecErr(err)
97
+	}
98
+	count, err := strconv.Atoi(strings.TrimSpace(string(out)))
99
+	if err != nil {
100
+		return 0, fmt.Errorf("git rev-list count: %w", err)
101
+	}
102
+	return count, nil
103
+}
104
+
91105
 // parseLogOutput unpacks the format above into Commits. Stable: the
92106
 // recordEnd lets us split records first, then unpack each.
93107
 func parseLogOutput(out []byte) ([]Commit, error) {
internal/repos/git/logops_test.gomodified
@@ -55,6 +55,14 @@ func TestLog_AndGetCommit_HappyPath(t *testing.T) {
5555
 	if detail.TreeOID == "" {
5656
 		t.Errorf("TreeOID empty")
5757
 	}
58
+
59
+	count, err := gitops.CountCommits(context.Background(), gitDir, "trunk")
60
+	if err != nil {
61
+		t.Fatalf("CountCommits: %v", err)
62
+	}
63
+	if count != 1 {
64
+		t.Fatalf("CountCommits = %d, want 1", count)
65
+	}
5866
 }
5967
 
6068
 func TestGetCommit_ReturnsErrCommitNotFound(t *testing.T) {
internal/web/handlers/repo/code.gomodified
@@ -19,6 +19,7 @@ import (
1919
 	"github.com/tenseleyFlow/shithub/internal/repos/finder"
2020
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
2121
 	"github.com/tenseleyFlow/shithub/internal/repos/highlight"
22
+	"github.com/tenseleyFlow/shithub/internal/repos/identity"
2223
 	reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
2324
 	"github.com/tenseleyFlow/shithub/internal/web/middleware"
2425
 )
@@ -148,6 +149,14 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co
148149
 	if headErr != nil {
149150
 		h.d.Logger.WarnContext(r.Context(), "code: HeadOf", "error", headErr)
150151
 	}
152
+	headAuthor := identity.Resolved{}
153
+	if headFound {
154
+		headAuthor = identity.New(h.d.Pool).Resolve(r.Context(), head.AuthorEmail)
155
+	}
156
+	commitCount, countErr := repogit.CountCommits(r.Context(), cc.gitDir, cc.ref)
157
+	if countErr != nil {
158
+		h.d.Logger.WarnContext(r.Context(), "code: CountCommits", "error", countErr)
159
+	}
151160
 	topics, _ := h.rq.ListRepoTopics(r.Context(), h.d.Pool, cc.row.ID)
152161
 	aboutEntries := entries
153162
 	if cc.subpath != "" {
@@ -167,10 +176,13 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co
167176
 		"Path":          cc.subpath,
168177
 		"Crumbs":        breadcrumbs(cc.owner, cc.row.Name, cc.ref, cc.subpath),
169178
 		"Entries":       entries,
179
+		"EntryRows":     h.codeTreeEntryRows(r.Context(), cc, entries),
170180
 		"Branches":      cc.refs.Branches,
171181
 		"Tags":          cc.refs.Tags,
172182
 		"Head":          head,
173183
 		"HeadFound":     headFound,
184
+		"HeadAuthor":    headAuthor,
185
+		"CommitCount":   commitCount,
174186
 		"README":        template.HTML(readmeHTML), //nolint:gosec // sanitized by mdrender
175187
 		"HTTPSCloneURL": h.cloneHTTPS(cc.owner, cc.row.Name),
176188
 		"SSHEnabled":    h.d.CloneURLs.SSHEnabled,
internal/web/handlers/repo/code_tree_rows.goadded
@@ -0,0 +1,67 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package repo
4
+
5
+import (
6
+	"context"
7
+	"net/url"
8
+	"strings"
9
+
10
+	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
11
+)
12
+
13
+type codeTreeEntryRow struct {
14
+	Entry      repogit.TreeEntry
15
+	FullPath   string
16
+	URL        string
17
+	LastCommit repogit.Commit
18
+	LastFound  bool
19
+}
20
+
21
+func (h *Handlers) codeTreeEntryRows(ctx context.Context, cc *codeContext, entries []repogit.TreeEntry) []codeTreeEntryRow {
22
+	rows := make([]codeTreeEntryRow, 0, len(entries))
23
+	for _, e := range entries {
24
+		fullPath := joinPath(cc.subpath, e.Name)
25
+		row := codeTreeEntryRow{
26
+			Entry:    e,
27
+			FullPath: fullPath,
28
+			URL:      treeEntryURL(cc.owner, cc.row.Name, cc.ref, e.Kind, fullPath),
29
+		}
30
+		if commits, err := repogit.Log(ctx, cc.gitDir, repogit.LogOptions{
31
+			Ref:      cc.ref,
32
+			MaxCount: 1,
33
+			Path:     fullPath,
34
+		}); err == nil && len(commits) > 0 {
35
+			row.LastCommit = commits[0]
36
+			row.LastFound = true
37
+		} else if err != nil && h.d.Logger != nil {
38
+			h.d.Logger.WarnContext(ctx, "code: row history", "error", err, "path", fullPath)
39
+		}
40
+		rows = append(rows, row)
41
+	}
42
+	return rows
43
+}
44
+
45
+func treeEntryURL(owner, repoName, ref string, kind repogit.TreeEntryKind, fullPath string) string {
46
+	base := "/" + url.PathEscape(owner) + "/" + url.PathEscape(repoName)
47
+	refPath := escapePathSegments(ref)
48
+	switch kind {
49
+	case repogit.EntryTree:
50
+		return base + "/tree/" + refPath + "/" + escapePathSegments(fullPath)
51
+	case repogit.EntryBlob:
52
+		return base + "/blob/" + refPath + "/" + escapePathSegments(fullPath)
53
+	default:
54
+		return ""
55
+	}
56
+}
57
+
58
+func escapePathSegments(p string) string {
59
+	if p == "" {
60
+		return ""
61
+	}
62
+	parts := strings.Split(p, "/")
63
+	for i := range parts {
64
+		parts[i] = url.PathEscape(parts[i])
65
+	}
66
+	return strings.Join(parts, "/")
67
+}
internal/web/handlers/repo/code_tree_rows_test.goadded
@@ -0,0 +1,26 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package repo
4
+
5
+import (
6
+	"testing"
7
+
8
+	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
9
+)
10
+
11
+func TestTreeEntryURL_EscapesPathSegments(t *testing.T) {
12
+	t.Parallel()
13
+	got := treeEntryURL("octo-user", "demo.repo", "feature/x", repogit.EntryBlob, "dir/a file.go")
14
+	want := "/octo-user/demo.repo/blob/feature/x/dir/a%20file.go"
15
+	if got != want {
16
+		t.Fatalf("treeEntryURL = %q, want %q", got, want)
17
+	}
18
+}
19
+
20
+func TestTreeEntryURL_NonNavigableEntries(t *testing.T) {
21
+	t.Parallel()
22
+	got := treeEntryURL("octo-user", "demo", "trunk", repogit.EntrySubmod, "vendor/lib")
23
+	if got != "" {
24
+		t.Fatalf("submodule URL = %q, want empty", got)
25
+	}
26
+}
internal/web/render/octicons.gomodified
@@ -55,6 +55,8 @@ func BuiltinOcticons() OcticonResolver {
5555
 			`><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm11.03-1.78a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.97 8.78a.75.75 0 0 1 1.06-1.06l1.22 1.22 2.72-2.72a.75.75 0 0 1 1.06 0Z"/></svg>`),
5656
 		"comment": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
5757
 			`><path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0 1 13.25 11H8.06l-3.31 2.48A.75.75 0 0 1 3.5 12.88V11h-.75A1.75 1.75 0 0 1 1 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v6.5c0 .138.112.25.25.25h1.5a.75.75 0 0 1 .75.75v1.13l2.36-1.77a.75.75 0 0 1 .45-.15h5.44a.25.25 0 0 0 .25-.25v-6.5a.25.25 0 0 0-.25-.25Z"/></svg>`),
58
+		"history": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
59
+			`><path d="M1.643 3.143.427 1.927A.25.25 0 0 0 0 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 0 0 .177-.427L2.715 4.215A6.5 6.5 0 1 1 8 14.5a.75.75 0 0 0 0 1.5 8 8 0 1 0-6.357-12.857ZM7.25 4.75a.75.75 0 0 1 1.5 0v3.19l2.03 2.03a.75.75 0 1 1-1.06 1.06L7.47 8.78a.75.75 0 0 1-.22-.53Z"/></svg>`),
5860
 		"gear": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
5961
 			`><path d="M8 0a1.5 1.5 0 0 1 1.45 1.13l.21.83c.23.08.45.18.66.3l.75-.44a1.5 1.5 0 0 1 1.93.3l.88.88a1.5 1.5 0 0 1 .3 1.93l-.44.75c.12.21.22.43.3.66l.83.21A1.5 1.5 0 0 1 16 8a1.5 1.5 0 0 1-1.13 1.45l-.83.21c-.08.23-.18.45-.3.66l.44.75a1.5 1.5 0 0 1-.3 1.93l-.88.88a1.5 1.5 0 0 1-1.93.3l-.75-.44c-.21.12-.43.22-.66.3l-.21.83A1.5 1.5 0 0 1 8 16a1.5 1.5 0 0 1-1.45-1.13l-.21-.83a5.36 5.36 0 0 1-.66-.3l-.75.44a1.5 1.5 0 0 1-1.93-.3L2.12 13a1.5 1.5 0 0 1-.3-1.93l.44-.75a5.36 5.36 0 0 1-.3-.66l-.83-.21A1.5 1.5 0 0 1 0 8c0-.69.47-1.29 1.13-1.45l.83-.21c.08-.23.18-.45.3-.66l-.44-.75a1.5 1.5 0 0 1 .3-1.93L3 2.12a1.5 1.5 0 0 1 1.93-.3l.75.44c.21-.12.43-.22.66-.3l.21-.83A1.5 1.5 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg>`),
6062
 		"lock": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
internal/web/static/css/shithub.cssmodified
@@ -1327,24 +1327,87 @@ button.shithub-repo-action {
13271327
   gap: 0.5rem;
13281328
   min-width: 0;
13291329
 }
1330
-.shithub-tree-commit-message span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1330
+.shithub-tree-commit-message .shithub-avatar-sm {
1331
+  margin-right: 0;
1332
+}
1333
+.shithub-tree-commit-subject,
1334
+.shithub-tree-row-commit a {
1335
+  overflow: hidden;
1336
+  text-overflow: ellipsis;
1337
+  white-space: nowrap;
1338
+}
1339
+.shithub-tree-commit-subject {
1340
+  color: var(--fg-default);
1341
+  min-width: 0;
1342
+}
13311343
 .shithub-tree-commit-meta { color: var(--fg-muted); flex: 0 0 auto; }
1344
+.shithub-tree-commit-meta a {
1345
+  color: var(--fg-muted);
1346
+}
1347
+.shithub-tree-commit-count {
1348
+  display: inline-flex;
1349
+  align-items: center;
1350
+  gap: 0.35rem;
1351
+  font-weight: 600;
1352
+}
13321353
 .shithub-tree {
13331354
   width: 100%;
13341355
   border-collapse: collapse;
13351356
   font-size: 0.9rem;
13361357
   background: var(--canvas-default);
1358
+  table-layout: fixed;
13371359
 }
13381360
 .shithub-tree td {
13391361
   padding: 0.5rem 0.75rem;
13401362
   border-bottom: 1px solid var(--border-default);
1363
+  vertical-align: middle;
13411364
 }
13421365
 .shithub-tree tr:last-child td { border-bottom: 0; }
1343
-.shithub-tree-icon { width: 24px; color: var(--fg-muted); }
1366
+.shithub-tree-icon { width: 36px; color: var(--fg-muted); padding-right: 0; }
13441367
 .shithub-tree-icon svg { display: block; }
1368
+.shithub-tree-name {
1369
+  width: 34%;
1370
+  font-weight: 600;
1371
+}
13451372
 .shithub-tree-name a { color: var(--fg-default); }
1346
-.shithub-tree-size { width: 100px; text-align: right; color: var(--fg-muted); font-variant-numeric: tabular-nums; }
1373
+.shithub-tree-row-commit {
1374
+  width: auto;
1375
+  color: var(--fg-muted);
1376
+}
1377
+.shithub-tree-row-commit a {
1378
+  display: block;
1379
+  color: var(--fg-muted);
1380
+}
1381
+.shithub-tree-row-time {
1382
+  width: 120px;
1383
+  color: var(--fg-muted);
1384
+  text-align: right;
1385
+  white-space: nowrap;
1386
+}
13471387
 .shithub-tree-symlink, .shithub-tree-submodule { color: var(--fg-muted); font-style: italic; font-size: 0.8rem; }
1388
+@media (max-width: 760px) {
1389
+  .shithub-code-actions {
1390
+    flex: 1 1 100%;
1391
+    justify-content: stretch;
1392
+  }
1393
+  .shithub-go-to-file {
1394
+    flex: 1 1 auto;
1395
+  }
1396
+  .shithub-tree-commit {
1397
+    align-items: flex-start;
1398
+    flex-direction: column;
1399
+  }
1400
+  .shithub-tree-commit-meta {
1401
+    flex-wrap: wrap;
1402
+  }
1403
+  .shithub-tree-name {
1404
+    width: auto;
1405
+  }
1406
+  .shithub-tree-row-commit,
1407
+  .shithub-tree-row-time {
1408
+    display: none;
1409
+  }
1410
+}
13481411
 
13491412
 /* Code-view layout: 2/3 main column + 1/3 About sidebar, mirroring
13501413
    the GitHub repo home layout. Single-column on narrow viewports so
internal/web/templates/repo/tree.htmlmodified
@@ -71,37 +71,49 @@
7171
           {{ if .HeadFound }}
7272
           <div class="shithub-tree-commit">
7373
             <div class="shithub-tree-commit-message">
74
+              {{ if .HeadAuthor.User }}
75
+              <a href="/{{ .HeadAuthor.Username }}"><img src="{{ .HeadAuthor.AvatarURL }}" alt="" class="shithub-avatar-sm"></a>
76
+              <a href="/{{ .HeadAuthor.Username }}"><strong>{{ if .HeadAuthor.DisplayName }}{{ .HeadAuthor.DisplayName }}{{ else }}{{ .HeadAuthor.Username }}{{ end }}</strong></a>
77
+              {{ else }}
78
+              <span class="shithub-avatar-sm shithub-identicon" data-seed="{{ .HeadAuthor.IdenticonSeed }}" aria-hidden="true"></span>
7479
               <strong>{{ .Head.AuthorName }}</strong>
75
-              <span>{{ .Head.Subject }}</span>
80
+              {{ end }}
81
+              <a href="/{{ .Owner }}/{{ .Repo.Name }}/commit/{{ .Head.OID }}" class="shithub-tree-commit-subject">{{ .Head.Subject }}</a>
7682
             </div>
7783
             <div class="shithub-tree-commit-meta">
78
-              <code title="{{ .Head.OID }}">{{ slice .Head.OID 0 7 }}</code>
84
+              <a href="/{{ .Owner }}/{{ .Repo.Name }}/commit/{{ .Head.OID }}"><code title="{{ .Head.OID }}">{{ slice .Head.OID 0 7 }}</code></a>
7985
               <time datetime="{{ .Head.AuthorWhen.Format "2006-01-02T15:04:05Z" }}">{{ relativeTime .Head.AuthorWhen }}</time>
86
+              <a href="/{{ .Owner }}/{{ .Repo.Name }}/commits/{{ .Ref }}" class="shithub-tree-commit-count">{{ octicon "history" }} {{ .CommitCount }} Commits</a>
8087
             </div>
8188
           </div>
8289
           {{ end }}
8390
           <table class="shithub-tree">
8491
             <tbody>
85
-              {{ range .Entries }}
92
+              {{ range .EntryRows }}
8693
               <tr>
8794
                 <td class="shithub-tree-icon" aria-hidden="true">
88
-                  {{ if eq (printf "%s" .Kind) "tree" }}{{ octicon "directory" }}
89
-                  {{ else if eq (printf "%s" .Kind) "commit" }}{{ octicon "submodule" }}
90
-                  {{ else if eq (printf "%s" .Kind) "symlink" }}{{ octicon "symlink" }}
95
+                  {{ if eq (printf "%s" .Entry.Kind) "tree" }}{{ octicon "directory" }}
96
+                  {{ else if eq (printf "%s" .Entry.Kind) "commit" }}{{ octicon "submodule" }}
97
+                  {{ else if eq (printf "%s" .Entry.Kind) "symlink" }}{{ octicon "symlink" }}
9198
                   {{ else }}{{ octicon "file" }}{{ end }}
9299
                 </td>
93100
                 <td class="shithub-tree-name">
94
-                  {{ if eq (printf "%s" .Kind) "tree" }}
95
-                  <a href="/{{ $.Owner }}/{{ $.Repo.Name }}/tree/{{ $.Ref }}/{{ if $.Path }}{{ $.Path }}/{{ end }}{{ .Name }}">{{ .Name }}</a>
96
-                  {{ else if eq (printf "%s" .Kind) "blob" }}
97
-                  <a href="/{{ $.Owner }}/{{ $.Repo.Name }}/blob/{{ $.Ref }}/{{ if $.Path }}{{ $.Path }}/{{ end }}{{ .Name }}">{{ .Name }}</a>
98
-                  {{ else if eq (printf "%s" .Kind) "symlink" }}
99
-                  {{ .Name }} <em class="shithub-tree-symlink">(symlink)</em>
101
+                  {{ if .URL }}
102
+                  <a href="{{ .URL }}">{{ .Entry.Name }}</a>
103
+                  {{ else if eq (printf "%s" .Entry.Kind) "symlink" }}
104
+                  {{ .Entry.Name }} <em class="shithub-tree-symlink">(symlink)</em>
100105
                   {{ else }}
101
-                  {{ .Name }} <em class="shithub-tree-submodule">@ {{ slice .OID 0 7 }}</em>
106
+                  {{ .Entry.Name }} <em class="shithub-tree-submodule">@ {{ slice .Entry.OID 0 7 }}</em>
107
+                  {{ end }}
108
+                </td>
109
+                <td class="shithub-tree-row-commit">
110
+                  {{ if .LastFound }}
111
+                  <a href="/{{ $.Owner }}/{{ $.Repo.Name }}/commit/{{ .LastCommit.OID }}">{{ .LastCommit.Subject }}</a>
102112
                   {{ end }}
103113
                 </td>
104
-                <td class="shithub-tree-size">{{ if gt .Size 0 }}{{ .Size }}{{ end }}</td>
114
+                <td class="shithub-tree-row-time">
115
+                  {{ if .LastFound }}<time datetime="{{ .LastCommit.AuthorWhen.Format "2006-01-02T15:04:05Z" }}">{{ relativeTime .LastCommit.AuthorWhen }}</time>{{ end }}
116
+                </td>
105117
               </tr>
106118
               {{ end }}
107119
             </tbody>