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.

Blame · server/web/file_history.go

Line attribution on main · 117 line(s)

67a308ba
Keenan Nemetz
15d ago
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
d37d301f
Keenan Nemetz
21h ago
9 "rodent.software/vole/core/git"
67a308ba
Keenan Nemetz
15d ago
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 }
701232cf
Keenan Nemetz
13d ago
22 release, ok := w.gitSlot(rw, r)
23 if !ok {
24 return
25 }
26 defer release()
27 g, cancel := gitRepo(r, m)
28 defer cancel()
67a308ba
Keenan Nemetz
15d ago
29 commit, _ := g.RevParse(ref)
30 if commit == "" {
31 w.notFound(rw, r)
32 return
33 }
34 kind, err := g.ObjectType(commit + ":" + path)
35 if err != nil {
36 w.notFound(rw, r)
37 return
38 }
39 commits, err := g.LogPath(commit, path, 100, kind == "blob")
40 if err != nil {
41 w.serverError(rw, r, "cannot read file history: "+err.Error())
42 return
43 }
44 w.render(rw, "file_history", struct {
45 Page
46 Name string
47 Ref string
48 Path string
49 Commit string
50 Commits []git.Commit
51 IsBlob bool
52 }{
53 Page: w.page(r, m.Name+" · history · "+path), Name: m.Name, Ref: ref,
54 Path: path, Commit: commit, Commits: commits, IsBlob: kind == "blob",
55 }, 0)
56 }
57
58 type blameBlock struct {
59 SHA string
60 Author string
61 When int64
62 Lines []git.BlameLine
63 }
64
65 func (w *Web) Blame(rw http.ResponseWriter, r *http.Request) {
66 m, ok := w.open(rw, r)
67 if !ok {
68 return
69 }
70 ref, path := r.PathValue("ref"), r.PathValue("path")
71 if !safePath(path) {
72 w.notFound(rw, r)
73 return
74 }
701232cf
Keenan Nemetz
13d ago
75 release, ok := w.gitSlot(rw, r)
76 if !ok {
77 return
78 }
79 defer release()
80 g, cancel := gitRepo(r, m)
81 defer cancel()
67a308ba
Keenan Nemetz
15d ago
82 commit, _ := g.RevParse(ref)
83 if commit == "" {
84 w.notFound(rw, r)
85 return
86 }
87 if kind, err := g.ObjectType(commit + ":" + path); err != nil || kind != "blob" {
88 w.notFound(rw, r)
89 return
90 }
91 lines, truncated, err := g.Blame(commit, path, 1<<20)
92 if err != nil {
93 w.serverError(rw, r, "cannot blame that file: "+err.Error())
94 return
95 }
96 var blocks []blameBlock
97 for _, line := range lines {
98 if len(blocks) == 0 || blocks[len(blocks)-1].SHA != line.SHA {
99 blocks = append(blocks, blameBlock{SHA: line.SHA, Author: line.Author, When: line.When})
100 }
101 last := &blocks[len(blocks)-1]
102 last.Lines = append(last.Lines, line)
103 }
104 w.render(rw, "blame", struct {
105 Page
106 Name string
107 Ref string
108 Path string
109 Commit string
110 Blocks []blameBlock
111 Lines int
112 Truncated bool
113 }{
114 Page: w.page(r, m.Name+" · blame · "+path), Name: m.Name, Ref: ref,
115 Path: path, Commit: commit, Blocks: blocks, Lines: len(lines), Truncated: truncated,
116 }, 0)
117 }