Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add file history and blame views

67a308badf4a88298639271a1201ff45cc3737a6 · Keenan Nemetz · 15d ago · parent f204e7ee

Add file history and blame views

A blob's toolbar now links to the commits that touched its path and to per-line attribution. File history uses git log for the path, following renames for blobs; blame groups consecutive lines by commit and shows the author and age in a left column. Both are read-only and reuse the existing commit-link and escaping rules.

server/web/file_history.go +105

@@ -0,0 +1,105 @@
1 package web
2
3 // File history and blame are the two answers a blob page cannot give: how this
4 // path changed over time, and which commit last touched each line.
5
6 import (
7 "net/http"
8
9 "github.com/rodent-software/vole/core/git"
10 )
11
12 func (w *Web) FileHistory(rw http.ResponseWriter, r *http.Request) {
13 m, ok := w.open(rw, r)
14 if !ok {
15 return
16 }
17 ref, path := r.PathValue("ref"), r.PathValue("path")
18 if !safePath(path) {
19 w.notFound(rw, r)
20 return
21 }
22 g := m.Git()
23 commit, _ := g.RevParse(ref)
24 if commit == "" {
25 w.notFound(rw, r)
26 return
27 }
28 kind, err := g.ObjectType(commit + ":" + path)
29 if err != nil {
30 w.notFound(rw, r)
31 return
32 }
33 commits, err := g.LogPath(commit, path, 100, kind == "blob")
34 if err != nil {
35 w.serverError(rw, r, "cannot read file history: "+err.Error())
36 return
37 }
38 w.render(rw, "file_history", struct {
39 Page
40 Name string
41 Ref string
42 Path string
43 Commit string
44 Commits []git.Commit
45 IsBlob bool
46 }{
47 Page: w.page(r, m.Name+" · history · "+path), Name: m.Name, Ref: ref,
48 Path: path, Commit: commit, Commits: commits, IsBlob: kind == "blob",
49 }, 0)
50 }
51
52 type blameBlock struct {
53 SHA string
54 Author string
55 When int64
56 Lines []git.BlameLine
57 }
58
59 func (w *Web) Blame(rw http.ResponseWriter, r *http.Request) {
60 m, ok := w.open(rw, r)
61 if !ok {
62 return
63 }
64 ref, path := r.PathValue("ref"), r.PathValue("path")
65 if !safePath(path) {
66 w.notFound(rw, r)
67 return
68 }
69 g := m.Git()
70 commit, _ := g.RevParse(ref)
71 if commit == "" {
72 w.notFound(rw, r)
73 return
74 }
75 if kind, err := g.ObjectType(commit + ":" + path); err != nil || kind != "blob" {
76 w.notFound(rw, r)
77 return
78 }
79 lines, truncated, err := g.Blame(commit, path, 1<<20)
80 if err != nil {
81 w.serverError(rw, r, "cannot blame that file: "+err.Error())
82 return
83 }
84 var blocks []blameBlock
85 for _, line := range lines {
86 if len(blocks) == 0 || blocks[len(blocks)-1].SHA != line.SHA {
87 blocks = append(blocks, blameBlock{SHA: line.SHA, Author: line.Author, When: line.When})
88 }
89 last := &blocks[len(blocks)-1]
90 last.Lines = append(last.Lines, line)
91 }
92 w.render(rw, "blame", struct {
93 Page
94 Name string
95 Ref string
96 Path string
97 Commit string
98 Blocks []blameBlock
99 Lines int
100 Truncated bool
101 }{
102 Page: w.page(r, m.Name+" · blame · "+path), Name: m.Name, Ref: ref,
103 Path: path, Commit: commit, Blocks: blocks, Lines: len(lines), Truncated: truncated,
104 }, 0)
105 }