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/diff.go

Line attribution on main · 184 line(s)

78912785
Keenan Nemetz
17d ago
1 package web
2
3 // Unified diff parsing for the web views. Git is the source of truth for what
4 // changed, so nothing here computes a diff: it reads the patch git prints and
5 // shapes it for a template. Two rules matter: every line is escaped by
6 // html/template on the way out, and a patch that is cut off (the cap) is shown
7 // as cut off rather than as a smaller change.
8
9 import (
f6f4b1db
Keenan Nemetz
15d ago
10 "html/template"
78912785
Keenan Nemetz
17d ago
11 "regexp"
12 "strconv"
13 "strings"
f6f4b1db
Keenan Nemetz
15d ago
14 "time"
78912785
Keenan Nemetz
17d ago
15 )
16
17 type diffFile struct {
e7982ac5
Keenan Nemetz
12d ago
18 OldPath string
19 NewPath string
20 Binary bool
21 Language string // the Prism language for Label, or "" when it is not known
22 Hunks []diffHunk
23 Added int
24 Removed int
78912785
Keenan Nemetz
17d ago
25 }
26
27 // Label is what to show as the file's name: the new path, or the old one for a
28 // deletion.
29 func (f diffFile) Label() string {
30 if f.NewPath != "" {
31 return f.NewPath
32 }
33 return f.OldPath
34 }
35
628d4523
Keenan Nemetz
11d ago
36 // pathspec is what git has to be told to print this file's patch. A rename needs
37 // both of its paths: filtered to the destination alone, git reports what is left
38 // as a new file and the rename is lost.
bef0c093
Keenan Nemetz
9d ago
39 //
40 // A deletion has one path, on the old side, and must not hand git the empty new
41 // side: git refuses an empty pathspec outright, so asking for a deleted file used
42 // to fail the whole page. Nothing at all is the one answer a caller has to
43 // handle: it means there is no patch to ask for, not "every path".
628d4523
Keenan Nemetz
11d ago
44 func (f diffFile) pathspec() []string {
bef0c093
Keenan Nemetz
9d ago
45 if f.OldPath == "" && f.NewPath == "" {
46 return nil
47 }
48 if f.OldPath != "" && f.NewPath != "" && f.OldPath != f.NewPath {
628d4523
Keenan Nemetz
11d ago
49 return []string{f.OldPath, f.NewPath}
50 }
51 if f.NewPath != "" {
52 return []string{f.NewPath}
53 }
54 return []string{f.OldPath}
55 }
56
78912785
Keenan Nemetz
17d ago
57 type diffHunk struct {
58 Header string
59 Lines []diffLine
60 }
61
62 type diffLine struct {
63 Kind string // "add", "del" or "ctx"
64 OldNo int
65 NewNo int
66 Text string
9cc2f087
Keenan Nemetz
17d ago
67 // Comments are review comments anchored to this line, filled in by the request
f6f4b1db
Keenan Nemetz
15d ago
68 // view rather than by the parser. They are rendered beneath the line so the
69 // reviewer can read the note where it applies.
9cc2f087
Keenan Nemetz
17d ago
70 Comments []diffComment
71 }
72
f6f4b1db
Keenan Nemetz
15d ago
73 // diffComment is one review comment shown beneath the line it is anchored to.
9cc2f087
Keenan Nemetz
17d ago
74 type diffComment struct {
f6f4b1db
Keenan Nemetz
15d ago
75 ID string
76 Author string
77 Created time.Time
78 Body template.HTML
78912785
Keenan Nemetz
17d ago
79 }
80
81 var (
82 hunkRE = regexp.MustCompile(`^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@`)
83 metaRE = regexp.MustCompile(`^(index |old mode|new mode|similarity index|rename (from|to)|copy (from|to)|new file mode|deleted file mode)`)
84 quoteRE = regexp.MustCompile(`^"(.*)"$`)
85 )
86
87 // parseDiff turns `git diff`/`git show` output into files, hunks and numbered
88 // lines. Unrecognised metadata is dropped: a viewer that shows "index 3f2a.." is
89 // showing the plumbing, not the change.
90 func parseDiff(patch string) []diffFile {
91 var files []diffFile
92 var cur *diffFile
93 var hunk *diffHunk
94 oldNo, newNo := 0, 0
95
96 flushHunk := func() {
97 if cur != nil && hunk != nil {
98 cur.Hunks = append(cur.Hunks, *hunk)
99 hunk = nil
100 }
101 }
102 flushFile := func() {
103 flushHunk()
104 if cur != nil {
e7982ac5
Keenan Nemetz
12d ago
105 name := cur.NewPath
106 if name == "" {
107 name = cur.OldPath
108 }
109 cur.Language = prismLanguage(name)
78912785
Keenan Nemetz
17d ago
110 files = append(files, *cur)
111 cur = nil
112 }
113 }
114
115 for _, line := range strings.Split(patch, "\n") {
116 switch {
117 case strings.HasPrefix(line, "diff --git "):
118 flushFile()
119 cur = &diffFile{}
120 case cur == nil:
121 continue
122 case strings.HasPrefix(line, "--- "):
123 cur.OldPath = diffPath(strings.TrimPrefix(line, "--- "))
124 case strings.HasPrefix(line, "+++ "):
125 cur.NewPath = diffPath(strings.TrimPrefix(line, "+++ "))
126 case strings.HasPrefix(line, "Binary files ") || strings.HasPrefix(line, "GIT binary patch"):
127 cur.Binary = true
128 case metaRE.MatchString(line):
129 // plumbing: shown nowhere
130 case hunkRE.MatchString(line):
131 flushHunk()
132 m := hunkRE.FindStringSubmatch(line)
133 oldNo, _ = strconv.Atoi(m[1])
134 newNo, _ = strconv.Atoi(m[2])
135 hunk = &diffHunk{Header: line}
136 case hunk != nil && strings.HasPrefix(line, "+"):
137 hunk.Lines = append(hunk.Lines, diffLine{Kind: "add", NewNo: newNo, Text: line[1:]})
138 cur.Added++
139 newNo++
140 case hunk != nil && strings.HasPrefix(line, "-"):
141 hunk.Lines = append(hunk.Lines, diffLine{Kind: "del", OldNo: oldNo, Text: line[1:]})
142 cur.Removed++
143 oldNo++
144 case hunk != nil && strings.HasPrefix(line, " "):
145 hunk.Lines = append(hunk.Lines, diffLine{Kind: "ctx", OldNo: oldNo, NewNo: newNo, Text: line[1:]})
146 oldNo++
147 newNo++
148 case hunk != nil && strings.HasPrefix(line, `\`):
149 // "\ No newline at end of file" belongs to the previous line.
150 }
151 }
152 flushFile()
153 return files
154 }
155
156 // diffPath normalises a ---/+++ path: strips the a/ or b/ prefix and the tab
157 // git appends when a path contains spaces.
158 func diffPath(p string) string {
159 if i := strings.IndexByte(p, '\t'); i >= 0 {
160 p = p[:i]
161 }
162 p = strings.TrimSpace(p)
163 if m := quoteRE.FindStringSubmatch(p); m != nil {
164 if q, err := strconv.Unquote(`"` + m[1] + `"`); err == nil {
165 p = q
166 }
167 }
168 if p == "/dev/null" {
169 return ""
170 }
171 if len(p) > 2 && (strings.HasPrefix(p, "a/") || strings.HasPrefix(p, "b/")) {
172 return p[2:]
173 }
174 return p
175 }
176
177 // diffSummary is the "3 files changed, +12 −4" line under a heading.
178 func diffSummary(files []diffFile) (added, removed int) {
179 for _, f := range files {
180 added += f.Added
181 removed += f.Removed
182 }
183 return added, removed
184 }