6 files changed,
91 insertions(+),
1 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-05-07 20:24:11 +0300
Authored at:
2026-05-07 19:26:35 +0300
Change ID:
ktsykqsvznnrwzxxyllvntknrwzyssnk
Parent:
6bb47d3
M
internal/git/repo.go
路路路 4 4 "context" 5 5 "errors" 6 6 "fmt" 7 + "path" 7 8 "path/filepath" 8 9 "strings" 9 10 "time" 路路路 205 206 c, err := g.r.CommitObject(g.h) 206 207 if err != nil { 207 208 return nil, fmt.Errorf("last commit: %w", err) 209 + } 210 + 211 + return newCommit(c), nil 212 +} 213 + 214 +func (g *Repo) LastFileCommit(ctx context.Context, fpath string) (*Commit, error) { 215 + path := path.Clean(fpath) 216 + hash, err := g.lastFileCommitHash(ctx, path) 217 + if err != nil { 218 + return nil, err 219 + } 220 + 221 + c, err := g.r.CommitObject(plumbing.NewHash(hash)) 222 + if err != nil { 223 + return nil, fmt.Errorf("commit object %s: %w", hash, err) 208 224 } 209 225 210 226 return newCommit(c), nil
M
internal/git/repo_test.go
路路路 172 172 r.commitFile("latest.txt", "latest", "latest commit") 173 173 174 174 commit, err := r.open().LastCommit() 175 - is.Equal(t, commit.Message, "latest commit") 176 175 is.Err(t, err, nil) 176 + is.Equal(t, commit.Message, "latest commit") 177 177 }) 178 178 179 179 t.Run("empty repo returns empty commit", func(t *testing.T) { 180 180 commit, err := newTestRepo(t).open().LastCommit() 181 181 is.Err(t, err, nil) 182 182 is.Equal(t, commit.Message, "") 183 + }) 184 +} 185 + 186 +func TestRepo_LastFileCommit(t *testing.T) { 187 + t.Run("returns last commit for root file", func(t *testing.T) { 188 + r := newTestRepo(t) 189 + r.commitFile("README.md", "v1\n", "init readme") 190 + last := r.commitFile("README.md", "v2\n", "update readme") 191 + 192 + c, err := r.open().LastFileCommit(t.Context(), "README.md") 193 + is.Err(t, err, nil) 194 + is.Equal(t, c.Hash, last.String()) 195 + is.Equal(t, c.Message, "update readme") 196 + }) 197 + 198 + t.Run("returns last commit for nested file", func(t *testing.T) { 199 + r := newTestRepo(t) 200 + r.commitFile("docs/guide.md", "v1\n", "add guide") 201 + last := r.commitFile("docs/guide.md", "v2\n", "update guide") 202 + r.commitFile("README.md", "root\n", "touch root") 203 + 204 + c, err := r.open().LastFileCommit(t.Context(), "docs/guide.md") 205 + is.Err(t, err, nil) 206 + is.Equal(t, c.Hash, last.String()) 207 + is.Equal(t, c.Message, "update guide") 183 208 }) 184 209 } 185 210
M
internal/git/tree.go
路路路 151 151 files []string 152 152 } 153 153 154 +func (g *Repo) lastFileCommitHash(ctx context.Context, fpath string) (string, error) { 155 + output, err := g.streamingGitLog(ctx, "-n", "1", "--format=%H", "--", fpath) 156 + if err != nil { 157 + return "", fmt.Errorf("last file commit for %q: %w", fpath, err) 158 + } 159 + defer output.Close() 160 + 161 + raw, err := io.ReadAll(output) 162 + if err != nil { 163 + return "", fmt.Errorf("reading log output for %q: %w", fpath, err) 164 + } 165 + 166 + hash := string(raw) 167 + if hash == "" { 168 + return "", fmt.Errorf("no last commit found for %q", fpath) 169 + } 170 + 171 + return hash, nil 172 +} 173 + 154 174 func (g *Repo) lastCommitForFilesInTree(ctx context.Context, subtree *object.Tree, parent string) (map[string]*Commit, error) { 155 175 filesToDo := make(map[string]struct{}) 156 176 filesDone := make(map[string]*Commit)
M
internal/handlers/repo.go
路路路 169 169 Ref string 170 170 Desc string 171 171 Lines []string 172 + LastCommit *git.Commit 172 173 Breadcrumbs []Breadcrumb 173 174 Path string 174 175 IsImage bool 路路路 205 206 IsBinary: fc.IsBinary, 206 207 Mime: fc.Mime, 207 208 Size: fc.Size, 209 + } 210 + 211 + p.LastCommit, err = repo.LastFileCommit(r.Context(), treePath) 212 + if err != nil { 213 + h.write500(w, err) 214 + return 208 215 } 209 216 210 217 p.Desc, err = repo.Description()
M
web/static/style.css
路路路 88 88 89 89 /* utilities */ 90 90 .mono { font-family: var(--mono-font); } 91 +.bold { font-weight: 700; } 91 92 .nowrap { white-space: nowrap; } 92 93 .fill { width: 100%; } 93 94 .muted { color: var(--gray); } 94 95 .pl { padding-left: 0.3rem; } 95 96 .pt { padding-top: 1rem; } 96 97 .mb { margin-bottom: 1rem; } 98 +.msb {margin-bottom: 0.3rem; } 97 99 .center { 98 100 display: flex; 99 101 justify-content: center;
M
web/templates/repo_file.html
路路路 18 18 (<a class="muted" href="/{{ .RepoName }}/raw/{{ .P.Ref }}/{{ .P.Path }}">view raw</a>) 19 19 </span> 20 20 </p> 21 + 22 + <div class="box msb"> 23 + <span class="has-tip"> 24 + <span> 25 + <a class="bold" href="mailto:{{ .P.LastCommit.AuthorEmail }}">{{ .P.LastCommit.AuthorName }}</a> 26 + </span> 27 + <span class="tooltip" role="tooltip"> 28 + <strong>{{ .P.LastCommit.AuthorName }}</strong><br> 29 + <a href="mailto:{{ .P.LastCommit.AuthorEmail }}" class="commit-email">{{ .P.LastCommit.AuthorEmail }}</a> 30 + </span> 31 + </span> 32 + <span class="pl"> 33 + <a class="link" href="/{{ .RepoName }}/commit/{{ .P.LastCommit.Hash }}"> 34 + {{- if .P.LastCommit.Message }}{{- commitSummary .P.LastCommit.Message -}} 35 + {{- else -}}<span class="muted">Empty message</span>{{- end -}} 36 + </a>, 37 + </span> 38 + <span class="muted">{{ humanizeRelTime .P.LastCommit.Committed }}</span> 39 + </div> 40 + 21 41 <div class="file-wrapper"> 22 42 {{ if .P.IsImage }} 23 43 <div class="image-viewer">