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.
Phase 7 slice 1: review on the web, so approving does not start with a clone
78912785b00227cffe3f8fb26b057cd60159a6b4 · Keenan Nemetz · 17d ago · parent 9bea7f11
Phase 7 slice 1: review on the web, so approving does not start with a clone Phase 7 is re-scoped: signing is deferred (the SSH handshake already establishes who pushed, and there is no third-party verifier here), and the phase is now about the experience of using the forge. - GET /r/<repo>/commit/<sha>: a commit's metadata and the patch it introduced, per-file hunks with line numbers; the commit list links to it - change requests show the same diff, computed three-dot: a review is what the branch adds since it diverged, not every commit main gained meanwhile - diffs are capped at 512 KiB and labelled when cut, binary files are named rather than faked, and every line is escaped (a <script> line is a test) - the repository README renders on its home page and markdown blobs render as markdown, through the sanitiser issues already use
internal/web/diff.go +143
| @@ -0,0 +1,143 @@ | |||
| 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 | "regexp" | ||
| 11 | "strconv" | ||
| 12 | "strings" | ||
| 13 | ) | ||
| 14 | |||
| 15 | type diffFile struct { | ||
| 16 | OldPath string | ||
| 17 | NewPath string | ||
| 18 | Binary bool | ||
| 19 | Hunks []diffHunk | ||
| 20 | Added int | ||
| 21 | Removed int | ||
| 22 | } | ||
| 23 | |||
| 24 | // Label is what to show as the file's name: the new path, or the old one for a | ||
| 25 | // deletion. | ||
| 26 | func (f diffFile) Label() string { | ||
| 27 | if f.NewPath != "" { | ||
| 28 | return f.NewPath | ||
| 29 | } | ||
| 30 | return f.OldPath | ||
| 31 | } | ||
| 32 | |||
| 33 | type diffHunk struct { | ||
| 34 | Header string | ||
| 35 | Lines []diffLine | ||
| 36 | } | ||
| 37 | |||
| 38 | type diffLine struct { | ||
| 39 | Kind string // "add", "del" or "ctx" | ||
| 40 | OldNo int | ||
| 41 | NewNo int | ||
| 42 | Text string | ||
| 43 | } | ||
| 44 | |||
| 45 | var ( | ||
| 46 | hunkRE = regexp.MustCompile(`^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@`) | ||
| 47 | metaRE = regexp.MustCompile(`^(index |old mode|new mode|similarity index|rename (from|to)|copy (from|to)|new file mode|deleted file mode)`) | ||
| 48 | quoteRE = regexp.MustCompile(`^"(.*)"$`) | ||
| 49 | ) | ||
| 50 | |||
| 51 | // parseDiff turns `git diff`/`git show` output into files, hunks and numbered | ||
| 52 | // lines. Unrecognised metadata is dropped: a viewer that shows "index 3f2a.." is | ||
| 53 | // showing the plumbing, not the change. | ||
| 54 | func parseDiff(patch string) []diffFile { | ||
| 55 | var files []diffFile | ||
| 56 | var cur *diffFile | ||
| 57 | var hunk *diffHunk | ||
| 58 | oldNo, newNo := 0, 0 | ||
| 59 | |||
| 60 | flushHunk := func() { | ||
| 61 | if cur != nil && hunk != nil { | ||
| 62 | cur.Hunks = append(cur.Hunks, *hunk) | ||
| 63 | hunk = nil | ||
| 64 | } | ||
| 65 | } | ||
| 66 | flushFile := func() { | ||
| 67 | flushHunk() | ||
| 68 | if cur != nil { | ||
| 69 | files = append(files, *cur) | ||
| 70 | cur = nil | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | for _, line := range strings.Split(patch, "\n") { | ||
| 75 | switch { | ||
| 76 | case strings.HasPrefix(line, "diff --git "): | ||
| 77 | flushFile() | ||
| 78 | cur = &diffFile{} | ||
| 79 | case cur == nil: | ||
| 80 | continue | ||
| 81 | case strings.HasPrefix(line, "--- "): | ||
| 82 | cur.OldPath = diffPath(strings.TrimPrefix(line, "--- ")) | ||
| 83 | case strings.HasPrefix(line, "+++ "): | ||
| 84 | cur.NewPath = diffPath(strings.TrimPrefix(line, "+++ ")) | ||
| 85 | case strings.HasPrefix(line, "Binary files ") || strings.HasPrefix(line, "GIT binary patch"): | ||
| 86 | cur.Binary = true | ||
| 87 | case metaRE.MatchString(line): | ||
| 88 | // plumbing: shown nowhere | ||
| 89 | case hunkRE.MatchString(line): | ||
| 90 | flushHunk() | ||
| 91 | m := hunkRE.FindStringSubmatch(line) | ||
| 92 | oldNo, _ = strconv.Atoi(m[1]) | ||
| 93 | newNo, _ = strconv.Atoi(m[2]) | ||
| 94 | hunk = &diffHunk{Header: line} | ||
| 95 | case hunk != nil && strings.HasPrefix(line, "+"): | ||
| 96 | hunk.Lines = append(hunk.Lines, diffLine{Kind: "add", NewNo: newNo, Text: line[1:]}) | ||
| 97 | cur.Added++ | ||
| 98 | newNo++ | ||
| 99 | case hunk != nil && strings.HasPrefix(line, "-"): | ||
| 100 | hunk.Lines = append(hunk.Lines, diffLine{Kind: "del", OldNo: oldNo, Text: line[1:]}) | ||
| 101 | cur.Removed++ | ||
| 102 | oldNo++ | ||
| 103 | case hunk != nil && strings.HasPrefix(line, " "): | ||
| 104 | hunk.Lines = append(hunk.Lines, diffLine{Kind: "ctx", OldNo: oldNo, NewNo: newNo, Text: line[1:]}) | ||
| 105 | oldNo++ | ||
| 106 | newNo++ | ||
| 107 | case hunk != nil && strings.HasPrefix(line, `\`): | ||
| 108 | // "\ No newline at end of file" belongs to the previous line. | ||
| 109 | } | ||
| 110 | } | ||
| 111 | flushFile() | ||
| 112 | return files | ||
| 113 | } | ||
| 114 | |||
| 115 | // diffPath normalises a ---/+++ path: strips the a/ or b/ prefix and the tab | ||
| 116 | // git appends when a path contains spaces. | ||
| 117 | func diffPath(p string) string { | ||
| 118 | if i := strings.IndexByte(p, '\t'); i >= 0 { | ||
| 119 | p = p[:i] | ||
| 120 | } | ||
| 121 | p = strings.TrimSpace(p) | ||
| 122 | if m := quoteRE.FindStringSubmatch(p); m != nil { | ||
| 123 | if q, err := strconv.Unquote(`"` + m[1] + `"`); err == nil { | ||
| 124 | p = q | ||
| 125 | } | ||
| 126 | } | ||
| 127 | if p == "/dev/null" { | ||
| 128 | return "" | ||
| 129 | } | ||
| 130 | if len(p) > 2 && (strings.HasPrefix(p, "a/") || strings.HasPrefix(p, "b/")) { | ||
| 131 | return p[2:] | ||
| 132 | } | ||
| 133 | return p | ||
| 134 | } | ||
| 135 | |||
| 136 | // diffSummary is the "3 files changed, +12 −4" line under a heading. | ||
| 137 | func diffSummary(files []diffFile) (added, removed int) { | ||
| 138 | for _, f := range files { | ||
| 139 | added += f.Added | ||
| 140 | removed += f.Removed | ||
| 141 | } | ||
| 142 | return added, removed | ||
| 143 | } | ||