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.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 19m ago
4b8c9e1d
| 1 | package git |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | // TestBlobIDMatchesGit pins the one piece of object formatting this package does |
| 13 | // itself. It only decides whether a blob needs writing; git still writes it. |
| 14 | func TestBlobIDMatchesGit(t *testing.T) { |
| 15 | g := blobRepo(t, nil) |
| 16 | for _, content := range [][]byte{ |
| 17 | nil, |
| 18 | {}, |
| 19 | []byte("hello\n"), |
| 20 | {0, 1, 2, 255, '\n', 0}, |
| 21 | bytes.Repeat([]byte("0123456789abcdef"), 4096), |
| 22 | } { |
| 23 | want, err := g.HashObject(content) |
| 24 | if err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | if got := blobID(content, false); got != want { |
| 28 | t.Fatalf("blobID(%d bytes) = %s, want %s", len(content), got, want) |
| 29 | } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // TestBlobsBySHABinary pins the batch framing: content may contain newlines and |
| 34 | // NULs, an empty blob still has a header, and asking for the same object twice |
| 35 | // answers twice. |
| 36 | func TestBlobsBySHABinary(t *testing.T) { |
| 37 | files := map[string][]byte{ |
| 38 | "empty": {}, |
| 39 | "nul": {0, '\n', 0, 255}, |
| 40 | "newlines": []byte("\n\n\n"), |
| 41 | "text": []byte("no trailing newline"), |
| 42 | } |
| 43 | g := blobRepo(t, files) |
| 44 | head, err := g.RevParse("refs/heads/main") |
| 45 | if err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | entries, err := g.LsTree(head) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | shas := make([]string, 0, len(entries)) |
| 53 | for _, e := range entries { |
| 54 | shas = append(shas, e.SHA) |
| 55 | } |
| 56 | blobs, err := g.BlobsBySHA(append(shas, shas...)) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | if len(blobs) != len(entries) { |
| 61 | t.Fatalf("BlobsBySHA returned %d objects, want %d", len(blobs), len(entries)) |
| 62 | } |
| 63 | for _, e := range entries { |
| 64 | if !bytes.Equal(blobs[e.SHA], files[e.Name]) { |
| 65 | t.Fatalf("%s = %q, want %q", e.Name, blobs[e.SHA], files[e.Name]) |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | func TestUsesSHA256(t *testing.T) { |
| 71 | if usesSHA256(nil) { |
| 72 | t.Fatal("an empty tree claimed SHA-256") |
| 73 | } |
| 74 | if usesSHA256(map[string]Entry{"a": {SHA: strings.Repeat("a", 40)}}) { |
| 75 | t.Fatal("a 40-character id claimed SHA-256") |
| 76 | } |
| 77 | if !usesSHA256(map[string]Entry{"a": {SHA: strings.Repeat("a", 64)}}) { |
| 78 | t.Fatal("a 64-character id did not claim SHA-256") |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // TestCommitFilesIncrementalEdits checks that a change, an add, and a delete |
| 83 | // produce the tree the caller asked for while leaving unrelated subtrees alone. |
| 84 | func TestCommitFilesIncrementalEdits(t *testing.T) { |
| 85 | dir := filepath.Join(t.TempDir(), "r.git") |
| 86 | if err := InitBare(dir, "main"); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | g := Open(dir) |
| 90 | ref := "refs/heads/main" |
| 91 | if _, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 92 | files["top.txt"] = []byte("top\n") |
| 93 | files["stable/one.txt"] = []byte("one\n") |
| 94 | files["dir/keep.txt"] = []byte("keep\n") |
| 95 | files["dir/change.txt"] = []byte("before\n") |
| 96 | files["dir/sub/gone.txt"] = []byte("gone\n") |
| 97 | return nil |
| 98 | }, "seed", "test"); err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | before, err := g.RevParse(ref) |
| 102 | if err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | beforeTree := treeID(t, g, before) |
| 106 | beforeRoot, err := g.LsTree(before) |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | |
| 111 | after, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 112 | files["dir/change.txt"] = []byte("after\n") |
| 113 | files["dir/new.txt"] = []byte("new\n") |
| 114 | delete(files, "dir/sub/gone.txt") |
| 115 | return nil |
| 116 | }, "edit", "test") |
| 117 | if err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | afterRoot, err := g.LsTree(after) |
| 121 | if err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | root := func(entries []Entry) map[string]Entry { |
| 125 | out := map[string]Entry{} |
| 126 | for _, e := range entries { |
| 127 | out[e.Name] = e |
| 128 | } |
| 129 | return out |
| 130 | } |
| 131 | if got, want := root(afterRoot)["stable"].SHA, root(beforeRoot)["stable"].SHA; got != want { |
| 132 | t.Fatalf("unchanged subtree was rewritten: %s, want %s", got, want) |
| 133 | } |
| 134 | if root(afterRoot)["dir"].SHA == root(beforeRoot)["dir"].SHA { |
| 135 | t.Fatal("the changed subtree kept its old id") |
| 136 | } |
| 137 | |
| 138 | files, err := g.FlatTree(after) |
| 139 | if err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | want := map[string]string{ |
| 143 | "top.txt": "top\n", |
| 144 | "stable/one.txt": "one\n", |
| 145 | "dir/keep.txt": "keep\n", |
| 146 | "dir/change.txt": "after\n", |
| 147 | "dir/new.txt": "new\n", |
| 148 | } |
| 149 | if len(files) != len(want) { |
| 150 | t.Fatalf("tree has %d files, want %d: %v", len(files), len(want), files) |
| 151 | } |
| 152 | for path, content := range want { |
| 153 | if got := string(files[path]); got != content { |
| 154 | t.Fatalf("%s = %q, want %q", path, got, content) |
| 155 | } |
| 156 | } |
| 157 | if _, ok := files["dir/sub/gone.txt"]; ok { |
| 158 | t.Fatal("the deleted file is still in the tree") |
| 159 | } |
| 160 | afterTree := treeID(t, g, after) |
| 161 | if afterTree == beforeTree { |
| 162 | t.Fatal("the root tree did not change") |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // TestCommitFilesReusesTreeWhenNothingChanged guards the reuse path: a mutator |
| 167 | // that writes the same bytes must not move the tree. |
| 168 | func TestCommitFilesReusesTreeWhenNothingChanged(t *testing.T) { |
| 169 | dir := filepath.Join(t.TempDir(), "r.git") |
| 170 | if err := InitBare(dir, "main"); err != nil { |
| 171 | t.Fatal(err) |
| 172 | } |
| 173 | g := Open(dir) |
| 174 | ref := "refs/heads/main" |
| 175 | if _, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 176 | files["a.txt"] = []byte("same\n") |
| 177 | files["dir/b.txt"] = []byte("same\n") |
| 178 | return nil |
| 179 | }, "seed", "test"); err != nil { |
| 180 | t.Fatal(err) |
| 181 | } |
| 182 | before := treeID(t, g, ref) |
| 183 | commit, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 184 | files["a.txt"] = []byte("same\n") |
| 185 | files["dir/b.txt"] = []byte("same\n") |
| 186 | return nil |
| 187 | }, "no-op", "test") |
| 188 | if err != nil { |
| 189 | t.Fatal(err) |
| 190 | } |
| 191 | after := treeID(t, g, commit) |
| 192 | if after != before { |
| 193 | t.Fatalf("tree moved from %s to %s without a content change", before, after) |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // TestCommitFilesStaysProportionalToTheChange is the regression test for the |
| 198 | // thousands of git processes a one-file write used to start. GIT_TRACE counts |
| 199 | // every child git, and a changed file at depth two needs a bounded handful. |
| 200 | func TestCommitFilesStaysProportionalToTheChange(t *testing.T) { |
| 201 | dir := filepath.Join(t.TempDir(), "r.git") |
| 202 | if err := InitBare(dir, "main"); err != nil { |
| 203 | t.Fatal(err) |
| 204 | } |
| 205 | g := Open(dir) |
| 206 | ref := "refs/heads/main" |
| 207 | seed := map[string][]byte{} |
| 208 | for i := 0; i < 200; i++ { |
| 209 | seed[fmt.Sprintf("runs/r%03d/1/record.md", i)] = []byte("record\n") |
| 210 | } |
| 211 | seed["issues/change/issue.md"] = []byte("before\n") |
| 212 | if _, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 213 | for path, content := range seed { |
| 214 | files[path] = content |
| 215 | } |
| 216 | return nil |
| 217 | }, "seed", "test"); err != nil { |
| 218 | t.Fatal(err) |
| 219 | } |
| 220 | |
| 221 | trace := filepath.Join(t.TempDir(), "git.trace") |
| 222 | traced := *g |
| 223 | traced.ExtraEnv = append(traced.ExtraEnv, "GIT_TRACE="+trace) |
| 224 | if _, err := CommitFiles(&traced, ref, func(files map[string][]byte) error { |
| 225 | files["issues/change/issue.md"] = []byte("after\n") |
| 226 | return nil |
| 227 | }, "change", "test"); err != nil { |
| 228 | t.Fatal(err) |
| 229 | } |
| 230 | data, err := os.ReadFile(trace) |
| 231 | if err != nil { |
| 232 | t.Fatal(err) |
| 233 | } |
| 234 | // rev-parse, ls-tree, one cat-file --batch, one hash-object, three mktree |
| 235 | // (issues/change, issues, root), commit-tree, update-ref. A full rewrite |
| 236 | // starts one git per file and per directory: over 400 here. |
| 237 | const limit = 20 |
| 238 | if starts := bytes.Count(data, []byte("built-in: git ")); starts > limit { |
| 239 | t.Fatalf("one changed file started %d git processes (limit %d):\n%s", starts, limit, data) |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | func treeID(t *testing.T, g *Repo, rev string) string { |
| 244 | t.Helper() |
| 245 | out, err := g.Run(nil, "rev-parse", "--verify", rev+"^{tree}") |
| 246 | if err != nil { |
| 247 | t.Fatal(err) |
| 248 | } |
| 249 | return strings.TrimSpace(string(out)) |
| 250 | } |
| 251 | |
| 252 | func BenchmarkCommitFilesOneFile(b *testing.B) { |
| 253 | dir := filepath.Join(b.TempDir(), "r.git") |
| 254 | if err := InitBare(dir, "main"); err != nil { |
| 255 | b.Fatal(err) |
| 256 | } |
| 257 | g := Open(dir) |
| 258 | ref := "refs/heads/main" |
| 259 | const unrelated = 2000 |
| 260 | if _, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 261 | for i := 0; i < unrelated; i++ { |
| 262 | files[fmt.Sprintf("runs/r%04d/1/record.md", i)] = []byte("record\n") |
| 263 | } |
| 264 | return nil |
| 265 | }, "seed", "bench"); err != nil { |
| 266 | b.Fatal(err) |
| 267 | } |
| 268 | b.ResetTimer() |
| 269 | for i := 0; i < b.N; i++ { |
| 270 | content := []byte(fmt.Sprintf("change %d\n", i)) |
| 271 | if _, err := CommitFiles(g, ref, func(files map[string][]byte) error { |
| 272 | files["issues/one/issue.md"] = content |
| 273 | return nil |
| 274 | }, "change", "bench"); err != nil { |
| 275 | b.Fatal(err) |
| 276 | } |
| 277 | } |
| 278 | } |
| 279 |