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 git |
| 2 | |
| 3 | // The per-file commit view reads the name list and then one file's patch. These |
| 4 | // tests pin the two together, because a list that disagrees with the patch is a |
| 5 | // tree row that opens nothing. |
| 6 | |
| 7 | import ( |
| 8 | "os" |
| 9 | "os/exec" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | ) |
| 14 | |
| 15 | // changeRepo builds a repository whose second commit adds, modifies, deletes and |
| 16 | // renames, with a space and a non-ASCII byte in the renamed path so git's quoting |
| 17 | // is exercised. |
| 18 | func changeRepo(t *testing.T) (*Repo, string) { |
| 19 | t.Helper() |
| 20 | dir := t.TempDir() |
| 21 | run := func(args ...string) { |
| 22 | t.Helper() |
| 23 | cmd := exec.Command("git", args...) |
| 24 | cmd.Dir = dir |
| 25 | cmd.Env = append(os.Environ(), |
| 26 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", |
| 27 | "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.com", |
| 28 | "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.com", |
| 29 | // Without this a platform can quote every non-ASCII path into an |
| 30 | // escape sequence, and the test would be reading a different format. |
| 31 | "GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=core.quotepath", "GIT_CONFIG_VALUE_0=false") |
| 32 | if out, err := cmd.CombinedOutput(); err != nil { |
| 33 | t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out) |
| 34 | } |
| 35 | } |
| 36 | write := func(name, content string) { |
| 37 | t.Helper() |
| 38 | if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | } |
| 42 | run("init", "-q", "-b", "main") |
| 43 | write("readme.md", "v1\n") |
| 44 | write("keep.txt", "keep\n") |
| 45 | write("old name.txt", "moved\n") |
| 46 | run("add", "-A") |
| 47 | run("commit", "-qm", "first") |
| 48 | |
| 49 | write("readme.md", "v1\nv2\n") |
| 50 | if err := os.MkdirAll(filepath.Join(dir, "docs"), 0o755); err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | write("docs/guide.md", "a guide\n") |
| 54 | if err := os.Remove(filepath.Join(dir, "keep.txt")); err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | run("mv", "old name.txt", "nëw name.txt") |
| 58 | run("add", "-A") |
| 59 | run("commit", "-qm", "second") |
| 60 | return open(t, dir), "HEAD" |
| 61 | } |
| 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 | |
| 152 | func TestShowNameStatusNamesEveryPathThePatchWillPrint(t *testing.T) { |
| 153 | r, rev := changeRepo(t) |
| 154 | |
| 155 | changes, err := r.ShowNameStatus(rev) |
| 156 | if err != nil { |
| 157 | t.Fatalf("ShowNameStatus: %v", err) |
| 158 | } |
| 159 | got := map[string]PathChange{} |
| 160 | for _, c := range changes { |
| 161 | got[c.New+c.Old] = c |
| 162 | } |
| 163 | if len(changes) != 4 { |
| 164 | t.Fatalf("ShowNameStatus returned %d changes, want 4: %+v", len(changes), changes) |
| 165 | } |
| 166 | for _, want := range []PathChange{ |
| 167 | {Status: "A", Old: "", New: "docs/guide.md"}, |
| 168 | {Status: "D", Old: "keep.txt", New: ""}, |
| 169 | {Status: "R", Old: "old name.txt", New: "nëw name.txt"}, |
| 170 | {Status: "M", Old: "readme.md", New: "readme.md"}, |
| 171 | } { |
| 172 | c, ok := got[want.New+want.Old] |
| 173 | if !ok { |
| 174 | t.Fatalf("ShowNameStatus has no %+v: %+v", want, changes) |
| 175 | } |
| 176 | if c.Status != want.Status { |
| 177 | t.Fatalf("%s status = %q, want %q", want.New, c.Status, want.Status) |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | func TestShowPatchPathReadsOneFile(t *testing.T) { |
| 183 | r, rev := changeRepo(t) |
| 184 | |
| 185 | patch, err := r.ShowPatchPath(rev, []string{"readme.md"}) |
| 186 | if err != nil { |
| 187 | t.Fatalf("ShowPatchPath: %v", err) |
| 188 | } |
| 189 | if !strings.Contains(patch, "+v2") { |
| 190 | t.Fatalf("the patch does not hold the change:\n%s", patch) |
| 191 | } |
| 192 | if strings.Contains(patch, "a guide") { |
| 193 | t.Fatalf("the patch holds another file:\n%s", patch) |
| 194 | } |
| 195 | |
| 196 | // A rename needs both paths: with only the destination named, git reports what |
| 197 | // is left as a new file and the view would lose the rename. |
| 198 | onlyNew, err := r.ShowPatchPath(rev, []string{"nëw name.txt"}) |
| 199 | if err != nil { |
| 200 | t.Fatalf("ShowPatchPath (new only): %v", err) |
| 201 | } |
| 202 | if strings.Contains(onlyNew, "rename from") { |
| 203 | t.Fatalf("one path should not have been enough for a rename:\n%s", onlyNew) |
| 204 | } |
| 205 | both, err := r.ShowPatchPath(rev, []string{"old name.txt", "nëw name.txt"}) |
| 206 | if err != nil { |
| 207 | t.Fatalf("ShowPatchPath (both): %v", err) |
| 208 | } |
| 209 | if !strings.Contains(both, "rename from old name.txt") || !strings.Contains(both, "similarity index 100%") { |
| 210 | t.Fatalf("the patch is not the whole rename:\n%s", both) |
| 211 | } |
| 212 | |
| 213 | // A root commit has no parent to diff against; the file is an addition. |
| 214 | root := strings.TrimSpace(mustGit(t, r, "rev-list", "--max-parents=0", "HEAD")) |
| 215 | changes, err := r.ShowNameStatus(root) |
| 216 | if err != nil { |
| 217 | t.Fatalf("ShowNameStatus (root): %v", err) |
| 218 | } |
| 219 | if len(changes) != 3 { |
| 220 | t.Fatalf("root commit lists %d files, want 3: %+v", len(changes), changes) |
| 221 | } |
| 222 | for _, c := range changes { |
| 223 | if c.Status != "A" || c.Old != "" { |
| 224 | t.Fatalf("root commit change = %+v, want an addition", c) |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | // mustGit runs one read command through the repository under test. |
| 230 | func mustGit(t *testing.T, r *Repo, args ...string) string { |
| 231 | t.Helper() |
| 232 | out, errs, err := r.run(nil, args...) |
| 233 | if err != nil { |
| 234 | t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, errs) |
| 235 | } |
| 236 | return string(out) |
| 237 | } |
| 238 |