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.
| 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 ( |
| 10 | "html/template" |
| 11 | "regexp" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | "time" |
| 15 | ) |
| 16 | |
| 17 | type 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. |
| 29 | func (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". |
| 44 | func (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 | |
| 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 |
| 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. |
| 74 | type diffComment struct { |
| 75 | ID string |
| 76 | Author string |
| 77 | Created time.Time |
| 78 | Body template.HTML |
| 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 { |
| 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. |
| 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 | } |
| 185 |