vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
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
8d 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
8d 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 | } |