Add code tree commit metadata
- SHA
0cd18974bac51de78643105fe3060d3ef8a49a1b- Parents
-
01ee51f - Tree
8dc2a30
0cd1897
0cd18974bac51de78643105fe3060d3ef8a49a1b01ee51f
8dc2a30| Status | File | + | - |
|---|---|---|---|
| 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) { | ||
| 88 | 88 | return parseLogOutput(out) |
| 89 | 89 | } |
| 90 | 90 | |
| 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 | + | |
| 91 | 105 | // parseLogOutput unpacks the format above into Commits. Stable: the |
| 92 | 106 | // recordEnd lets us split records first, then unpack each. |
| 93 | 107 | func parseLogOutput(out []byte) ([]Commit, error) { |
internal/repos/git/logops_test.gomodified@@ -55,6 +55,14 @@ func TestLog_AndGetCommit_HappyPath(t *testing.T) { | ||
| 55 | 55 | if detail.TreeOID == "" { |
| 56 | 56 | t.Errorf("TreeOID empty") |
| 57 | 57 | } |
| 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 | + } | |
| 58 | 66 | } |
| 59 | 67 | |
| 60 | 68 | func TestGetCommit_ReturnsErrCommitNotFound(t *testing.T) { |
internal/web/handlers/repo/code.gomodified@@ -19,6 +19,7 @@ import ( | ||
| 19 | 19 | "github.com/tenseleyFlow/shithub/internal/repos/finder" |
| 20 | 20 | repogit "github.com/tenseleyFlow/shithub/internal/repos/git" |
| 21 | 21 | "github.com/tenseleyFlow/shithub/internal/repos/highlight" |
| 22 | + "github.com/tenseleyFlow/shithub/internal/repos/identity" | |
| 22 | 23 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 23 | 24 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 24 | 25 | ) |
@@ -148,6 +149,14 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co | ||
| 148 | 149 | if headErr != nil { |
| 149 | 150 | h.d.Logger.WarnContext(r.Context(), "code: HeadOf", "error", headErr) |
| 150 | 151 | } |
| 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 | + } | |
| 151 | 160 | topics, _ := h.rq.ListRepoTopics(r.Context(), h.d.Pool, cc.row.ID) |
| 152 | 161 | aboutEntries := entries |
| 153 | 162 | if cc.subpath != "" { |
@@ -167,10 +176,13 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co | ||
| 167 | 176 | "Path": cc.subpath, |
| 168 | 177 | "Crumbs": breadcrumbs(cc.owner, cc.row.Name, cc.ref, cc.subpath), |
| 169 | 178 | "Entries": entries, |
| 179 | + "EntryRows": h.codeTreeEntryRows(r.Context(), cc, entries), | |
| 170 | 180 | "Branches": cc.refs.Branches, |
| 171 | 181 | "Tags": cc.refs.Tags, |
| 172 | 182 | "Head": head, |
| 173 | 183 | "HeadFound": headFound, |
| 184 | + "HeadAuthor": headAuthor, | |
| 185 | + "CommitCount": commitCount, | |
| 174 | 186 | "README": template.HTML(readmeHTML), //nolint:gosec // sanitized by mdrender |
| 175 | 187 | "HTTPSCloneURL": h.cloneHTTPS(cc.owner, cc.row.Name), |
| 176 | 188 | "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 { | ||
| 55 | 55 | `><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>`), |
| 56 | 56 | "comment": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
| 57 | 57 | `><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>`), | |
| 58 | 60 | "gear": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls + |
| 59 | 61 | `><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>`), |
| 60 | 62 | "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 { | ||
| 1327 | 1327 | gap: 0.5rem; |
| 1328 | 1328 | min-width: 0; |
| 1329 | 1329 | } |
| 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 | +} | |
| 1331 | 1343 | .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 | +} | |
| 1332 | 1353 | .shithub-tree { |
| 1333 | 1354 | width: 100%; |
| 1334 | 1355 | border-collapse: collapse; |
| 1335 | 1356 | font-size: 0.9rem; |
| 1336 | 1357 | background: var(--canvas-default); |
| 1358 | + table-layout: fixed; | |
| 1337 | 1359 | } |
| 1338 | 1360 | .shithub-tree td { |
| 1339 | 1361 | padding: 0.5rem 0.75rem; |
| 1340 | 1362 | border-bottom: 1px solid var(--border-default); |
| 1363 | + vertical-align: middle; | |
| 1341 | 1364 | } |
| 1342 | 1365 | .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; } | |
| 1344 | 1367 | .shithub-tree-icon svg { display: block; } |
| 1368 | +.shithub-tree-name { | |
| 1369 | + width: 34%; | |
| 1370 | + font-weight: 600; | |
| 1371 | +} | |
| 1345 | 1372 | .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 | +} | |
| 1347 | 1387 | .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 | +} | |
| 1348 | 1411 | |
| 1349 | 1412 | /* Code-view layout: 2/3 main column + 1/3 About sidebar, mirroring |
| 1350 | 1413 | the GitHub repo home layout. Single-column on narrow viewports so |
internal/web/templates/repo/tree.htmlmodified@@ -71,37 +71,49 @@ | ||
| 71 | 71 | {{ if .HeadFound }} |
| 72 | 72 | <div class="shithub-tree-commit"> |
| 73 | 73 | <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> | |
| 74 | 79 | <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> | |
| 76 | 82 | </div> |
| 77 | 83 | <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> | |
| 79 | 85 | <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> | |
| 80 | 87 | </div> |
| 81 | 88 | </div> |
| 82 | 89 | {{ end }} |
| 83 | 90 | <table class="shithub-tree"> |
| 84 | 91 | <tbody> |
| 85 | - {{ range .Entries }} | |
| 92 | + {{ range .EntryRows }} | |
| 86 | 93 | <tr> |
| 87 | 94 | <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" }} | |
| 91 | 98 | {{ else }}{{ octicon "file" }}{{ end }} |
| 92 | 99 | </td> |
| 93 | 100 | <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> | |
| 100 | 105 | {{ 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> | |
| 102 | 112 | {{ end }} |
| 103 | 113 | </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> | |
| 105 | 117 | </tr> |
| 106 | 118 | {{ end }} |
| 107 | 119 | </tbody> |