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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 23m ago 32290114
history blame raw 5.2 KiB
1package 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
9import (
10 "html/template"
11 "regexp"
12 "strconv"
13 "strings"
14 "time"
15)
16
17type diffFile struct {
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
25}
26
27// Label is what to show as the file's name: the new path, or the old one for a
28// deletion.
29func (f diffFile) Label() string {
30 if f.NewPath != "" {
31 return f.NewPath
32 }
33 return f.OldPath
34}
35
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.
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".
44func (f diffFile) pathspec() []string {
45 if f.OldPath == "" && f.NewPath == "" {
46 return nil
47 }
48 if f.OldPath != "" && f.NewPath != "" && f.OldPath != f.NewPath {
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
57type diffHunk struct {
58 Header string
59 Lines []diffLine
60}
61
62type diffLine struct {
63 Kind string // "add", "del" or "ctx"
64 OldNo int
65 NewNo int
66 Text string
67 // Comments are review comments anchored to this line, filled in by the request
68 // view rather than by the parser. They are rendered beneath the line so the
69 // reviewer can read the note where it applies.
70 Comments []diffComment
71}
72
73// diffComment is one review comment shown beneath the line it is anchored to.
74type diffComment struct {
75 ID string
76 Author string
77 Created time.Time
78 Body template.HTML
79}
80
81var (
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.
90func 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 {
105 name := cur.NewPath
106 if name == "" {
107 name = cur.OldPath
108 }
109 cur.Language = prismLanguage(name)
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.
158func 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.
178func 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}
185