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_test.go +152
| @@ -0,0 +1,152 @@ | |||
| 1 | package web | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "strings" | ||
| 5 | "testing" | ||
| 6 | ) | ||
| 7 | |||
| 8 | // The parser reads what git prints. These fixtures are real `git diff` output, | ||
| 9 | // because a hand-written approximation is what lets a parser drift from git. | ||
| 10 | const patchTwoFiles = `diff --git a/readme.md b/readme.md | ||
| 11 | index 1111111..2222222 100644 | ||
| 12 | --- a/readme.md | ||
| 13 | +++ b/readme.md | ||
| 14 | @@ -1,3 +1,4 @@ | ||
| 15 | # Title | ||
| 16 | |||
| 17 | -old line | ||
| 18 | +new line | ||
| 19 | +another line | ||
| 20 | diff --git a/notes.txt b/notes.txt | ||
| 21 | deleted file mode 100644 | ||
| 22 | index 3333333..0000000 | ||
| 23 | --- a/notes.txt | ||
| 24 | +++ /dev/null | ||
| 25 | @@ -1,2 +0,0 @@ | ||
| 26 | -gone | ||
| 27 | -gone too | ||
| 28 | ` | ||
| 29 | |||
| 30 | func TestParseDiff(t *testing.T) { | ||
| 31 | files := parseDiff(patchTwoFiles) | ||
| 32 | if len(files) != 2 { | ||
| 33 | t.Fatalf("parsed %d files, want 2: %+v", len(files), files) | ||
| 34 | } | ||
| 35 | first := files[0] | ||
| 36 | if first.OldPath != "readme.md" || first.NewPath != "readme.md" { | ||
| 37 | t.Fatalf("paths = %q -> %q", first.OldPath, first.NewPath) | ||
| 38 | } | ||
| 39 | if first.Added != 2 || first.Removed != 1 { | ||
| 40 | t.Fatalf("counts = +%d −%d, want +2 −1", first.Added, first.Removed) | ||
| 41 | } | ||
| 42 | if len(first.Hunks) != 1 || len(first.Hunks[0].Lines) != 5 { | ||
| 43 | t.Fatalf("hunk shape = %+v", first.Hunks) | ||
| 44 | } | ||
| 45 | lines := first.Hunks[0].Lines | ||
| 46 | if lines[0].Kind != "ctx" || lines[0].Text != "# Title" || lines[0].OldNo != 1 || lines[0].NewNo != 1 { | ||
| 47 | t.Fatalf("first line = %+v", lines[0]) | ||
| 48 | } | ||
| 49 | if lines[2].Kind != "del" || lines[2].OldNo != 3 || lines[2].NewNo != 0 { | ||
| 50 | t.Fatalf("deleted line = %+v", lines[2]) | ||
| 51 | } | ||
| 52 | if lines[3].Kind != "add" || lines[3].NewNo != 3 || lines[3].Text != "new line" { | ||
| 53 | t.Fatalf("added line = %+v", lines[3]) | ||
| 54 | } | ||
| 55 | second := files[1] | ||
| 56 | if second.NewPath != "" || second.Label() != "notes.txt" { | ||
| 57 | t.Fatalf("deletion = %+v, label %q", second, second.Label()) | ||
| 58 | } | ||
| 59 | if second.Removed != 2 { | ||
| 60 | t.Fatalf("deletion counts = −%d", second.Removed) | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | func TestParseDiffOfABinaryFile(t *testing.T) { | ||
| 65 | files := parseDiff(`diff --git a/logo.png b/logo.png | ||
| 66 | index 4444444..5555555 100644 | ||
| 67 | Binary files a/logo.png and b/logo.png differ | ||
| 68 | `) | ||
| 69 | if len(files) != 1 || !files[0].Binary { | ||
| 70 | t.Fatalf("binary file not recognised: %+v", files) | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | func TestParseDiffOfANewFile(t *testing.T) { | ||
| 75 | files := parseDiff(`diff --git a/new.txt b/new.txt | ||
| 76 | new file mode 100644 | ||
| 77 | index 0000000..6666666 | ||
| 78 | --- /dev/null | ||
| 79 | +++ b/new.txt | ||
| 80 | @@ -0,0 +1,2 @@ | ||
| 81 | +one | ||
| 82 | +two | ||
| 83 | `) | ||
| 84 | if len(files) != 1 { | ||
| 85 | t.Fatalf("files = %+v", files) | ||
| 86 | } | ||
| 87 | f := files[0] | ||
| 88 | if f.OldPath != "" || f.NewPath != "new.txt" || f.Added != 2 { | ||
| 89 | t.Fatalf("new file = %+v", f) | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | func TestParseDiffOfARename(t *testing.T) { | ||
| 94 | files := parseDiff(`diff --git a/old.md b/new.md | ||
| 95 | similarity index 100% | ||
| 96 | rename from old.md | ||
| 97 | rename to new.md | ||
| 98 | `) | ||
| 99 | if len(files) != 1 { | ||
| 100 | t.Fatalf("files = %+v", files) | ||
| 101 | } | ||
| 102 | // A pure rename has no ---/+++ lines, so both paths are empty and the file is | ||
| 103 | // shown under the name git gave it in the diff header. | ||
| 104 | if files[0].OldPath != "" || files[0].NewPath != "" { | ||
| 105 | t.Fatalf("rename paths = %+v", files[0]) | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | func TestParseDiffIgnoresPlumbing(t *testing.T) { | ||
| 110 | files := parseDiff(`diff --git a/app.go b/app.go | ||
| 111 | index aaa..bbb 100644 | ||
| 112 | --- a/app.go | ||
| 113 | +++ b/app.go | ||
| 114 | @@ -10,3 +10,3 @@ func main() { | ||
| 115 | ctx := context.Background() | ||
| 116 | - run(ctx) | ||
| 117 | + run(ctx, cfg) | ||
| 118 | `) | ||
| 119 | got := files[0].Hunks[0].Lines | ||
| 120 | for _, l := range got { | ||
| 121 | if strings.HasPrefix(l.Text, "index aaa") { | ||
| 122 | t.Fatalf("plumbing leaked into the rendered lines: %+v", got) | ||
| 123 | } | ||
| 124 | } | ||
| 125 | if got[1].OldNo != 11 || got[2].NewNo != 11 { | ||
| 126 | t.Fatalf("line numbers = %+v", got) | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | func TestParseDiffToleratesATruncatedPatch(t *testing.T) { | ||
| 131 | // A cut patch ends mid-hunk: it must render what it has, not panic or claim | ||
| 132 | // the missing lines were unchanged. | ||
| 133 | files := parseDiff(`diff --git a/big.txt b/big.txt | ||
| 134 | --- a/big.txt | ||
| 135 | +++ b/big.txt | ||
| 136 | @@ -1,2 +1,2 @@ | ||
| 137 | - first | ||
| 138 | + second`) | ||
| 139 | if len(files) != 1 { | ||
| 140 | t.Fatalf("files = %+v", files) | ||
| 141 | } | ||
| 142 | if files[0].Added != 1 || files[0].Removed != 1 { | ||
| 143 | t.Fatalf("counts = %+v", files[0]) | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | func TestDiffSummary(t *testing.T) { | ||
| 148 | added, removed := diffSummary(parseDiff(patchTwoFiles)) | ||
| 149 | if added != 2 || removed != 3 { | ||
| 150 | t.Fatalf("summary = +%d −%d, want +2 −3", added, removed) | ||
| 151 | } | ||
| 152 | } | ||