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.
Read a change's file list without its patch, and never cut a diff
f64c77e4950c1759f336d3bc1849ead86e8b8ebf · Keenan Nemetz · 8d ago · parent 11720250
Read a change's file list without its patch, and never cut a diff The review tree, the file count and the landing summary were all parsed out of one `git diff` capped at 512 KiB. When a change carries a large file the cut lands inside it, and every file named after the cut is missing from the tree: reviewing #143, a 546 KB deletion hid the 296 files that came after it while the tab still said "6 changed". The cap existed to stop a page rendering every hunk, but the pages it guarded already render one file at a time. The inventory and the patch are separate reads now. DiffInventory runs `git diff --raw --numstat -z`, which names every file with its status and line counts in one walk of the trees and generates no hunks; the changed-file tree, the counts and the landing summary read that. The Files tab then asks for the one file it shows with DiffPaths, and the commit page keeps its per-file ShowPatchPath. Nothing is capped by bytes: a page is as large as the file it shows, and the inventory is the whole change however large it is. Compare still renders every file of a range at once, and is uncapped too, so a large comparison is one large page. Making that per-file is the next step if it turns out to matter. TestDiffInventoryCountsEveryFile covers the statuses, counts, renames and binary files the inventory has to report; TestRequestFilesListsEveryChangedFile is the regression for the tree that lost its files, and the commit page test now asserts the end of a large patch instead of its cut.
core/git/commit_test.go +92−6
| @@ -60,6 +60,95 @@ func changeRepo(t *testing.T) (*Repo, string) { | |||
| 60 | 60 | return open(t, dir), "HEAD" | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | // inventoryRepo builds one commit whose diff has every shape the inventory has | ||
| 64 | // to report: a modification, an addition, a deletion, a rename and a binary file. | ||
| 65 | func inventoryRepo(t *testing.T) (*Repo, string) { | ||
| 66 | t.Helper() | ||
| 67 | dir := t.TempDir() | ||
| 68 | run := func(args ...string) { | ||
| 69 | t.Helper() | ||
| 70 | cmd := exec.Command("git", args...) | ||
| 71 | cmd.Dir = dir | ||
| 72 | cmd.Env = append(os.Environ(), | ||
| 73 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", | ||
| 74 | "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.com", | ||
| 75 | "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.com") | ||
| 76 | if out, err := cmd.CombinedOutput(); err != nil { | ||
| 77 | t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | write := func(name, content string) { | ||
| 81 | t.Helper() | ||
| 82 | if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { | ||
| 83 | t.Fatal(err) | ||
| 84 | } | ||
| 85 | } | ||
| 86 | run("init", "-q", "-b", "main") | ||
| 87 | write("readme.md", "one\n") | ||
| 88 | write("gone.txt", "gone\n") | ||
| 89 | write("old name.txt", "moved\n") | ||
| 90 | write("blob.bin", "\x00\x01") | ||
| 91 | run("add", "-A") | ||
| 92 | run("commit", "-qm", "first") | ||
| 93 | |||
| 94 | write("readme.md", "one\ntwo\n") | ||
| 95 | write("added.txt", "new\nnew\n") | ||
| 96 | if err := os.Remove(filepath.Join(dir, "gone.txt")); err != nil { | ||
| 97 | t.Fatal(err) | ||
| 98 | } | ||
| 99 | run("mv", "old name.txt", "new name.txt") | ||
| 100 | write("blob.bin", "\x00\x01\x02") | ||
| 101 | run("add", "-A") | ||
| 102 | run("commit", "-qm", "second") | ||
| 103 | return open(t, dir), "HEAD" | ||
| 104 | } | ||
| 105 | |||
| 106 | // TestDiffInventoryCountsEveryFile: the inventory is what a changed-file tree and | ||
| 107 | // a landing summary read, so it has to name every file with the status the tree | ||
| 108 | // marks and the counts the summary adds up — without generating a patch. | ||
| 109 | func TestDiffInventoryCountsEveryFile(t *testing.T) { | ||
| 110 | t.Parallel() | ||
| 111 | r, rev := inventoryRepo(t) | ||
| 112 | |||
| 113 | changes, err := r.DiffInventory(rev+"~1...HEAD", "") | ||
| 114 | if err != nil { | ||
| 115 | t.Fatalf("DiffInventory: %v", err) | ||
| 116 | } | ||
| 117 | if len(changes) != 5 { | ||
| 118 | t.Fatalf("DiffInventory returned %d changes, want 5: %+v", len(changes), changes) | ||
| 119 | } | ||
| 120 | got := map[string]PathChange{} | ||
| 121 | for _, c := range changes { | ||
| 122 | got[pathLabel(c)] = c | ||
| 123 | } | ||
| 124 | for _, want := range []PathChange{ | ||
| 125 | {Status: "M", Old: "readme.md", New: "readme.md", Added: 1, Removed: 0}, | ||
| 126 | {Status: "A", Old: "", New: "added.txt", Added: 2, Removed: 0}, | ||
| 127 | {Status: "D", Old: "gone.txt", New: "", Added: 0, Removed: 1}, | ||
| 128 | {Status: "R", Old: "old name.txt", New: "new name.txt"}, | ||
| 129 | {Status: "M", Old: "blob.bin", New: "blob.bin", Binary: true}, | ||
| 130 | } { | ||
| 131 | have, ok := got[pathLabel(want)] | ||
| 132 | if !ok { | ||
| 133 | t.Fatalf("DiffInventory has no %+v: %+v", want, changes) | ||
| 134 | } | ||
| 135 | if have != want { | ||
| 136 | t.Fatalf("%s = %+v, want %+v", pathLabel(want), have, want) | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | // The two-revision form is what a merged request reads: the tip it landed on | ||
| 141 | // and the change that landed are commits, not a range. | ||
| 142 | base := strings.TrimSpace(mustGit(t, r, "rev-parse", "HEAD~1")) | ||
| 143 | pair, err := r.DiffInventory(base, "HEAD") | ||
| 144 | if err != nil { | ||
| 145 | t.Fatalf("DiffInventory (two revisions): %v", err) | ||
| 146 | } | ||
| 147 | if len(pair) != len(changes) { | ||
| 148 | t.Fatalf("two-revision inventory has %d files, want %d", len(pair), len(changes)) | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 63 | 152 | func TestShowNameStatusNamesEveryPathThePatchWillPrint(t *testing.T) { | |
| 64 | 153 | r, rev := changeRepo(t) | |
| 65 | 154 | ||
| @@ -93,13 +182,10 @@ func TestShowNameStatusNamesEveryPathThePatchWillPrint(t *testing.T) { | |||
| 93 | 182 | func TestShowPatchPathReadsOneFile(t *testing.T) { | |
| 94 | 183 | r, rev := changeRepo(t) | |
| 95 | 184 | ||
| 96 | patch, truncated, err := r.ShowPatchPath(rev, []string{"readme.md"}, 1<<20) | ||
| 185 | patch, err := r.ShowPatchPath(rev, []string{"readme.md"}) | ||
| 97 | 186 | if err != nil { | |
| 98 | 187 | t.Fatalf("ShowPatchPath: %v", err) | |
| 99 | 188 | } | |
| 100 | if truncated { | ||
| 101 | t.Fatal("a small patch was reported as cut") | ||
| 102 | } | ||
| 103 | 189 | if !strings.Contains(patch, "+v2") { | |
| 104 | 190 | t.Fatalf("the patch does not hold the change:\n%s", patch) | |
| 105 | 191 | } | |
| @@ -109,14 +195,14 @@ func TestShowPatchPathReadsOneFile(t *testing.T) { | |||
| 109 | 195 | ||
| 110 | 196 | // A rename needs both paths: with only the destination named, git reports what | |
| 111 | 197 | // is left as a new file and the view would lose the rename. | |
| 112 | onlyNew, _, err := r.ShowPatchPath(rev, []string{"nëw name.txt"}, 1<<20) | ||
| 198 | onlyNew, err := r.ShowPatchPath(rev, []string{"nëw name.txt"}) | ||
| 113 | 199 | if err != nil { | |
| 114 | 200 | t.Fatalf("ShowPatchPath (new only): %v", err) | |
| 115 | 201 | } | |
| 116 | 202 | if strings.Contains(onlyNew, "rename from") { | |
| 117 | 203 | t.Fatalf("one path should not have been enough for a rename:\n%s", onlyNew) | |
| 118 | 204 | } | |
| 119 | both, _, err := r.ShowPatchPath(rev, []string{"old name.txt", "nëw name.txt"}, 1<<20) | ||
| 205 | both, err := r.ShowPatchPath(rev, []string{"old name.txt", "nëw name.txt"}) | ||
| 120 | 206 | if err != nil { | |
| 121 | 207 | t.Fatalf("ShowPatchPath (both): %v", err) | |
| 122 | 208 | } | |