Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }