Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Urgh I made some wrong amends and I'm too tired to separate the commits out this time

ancestor: Split out of reachability
mergebase: Add merge base routines
internal/commitquery: Add commit query context engine thingy
internal/peel: Shared tag peeling
errors: Shared object query errors
internal/testgit: Add rooted repo helpers; remove raw path access
objectstore/memory: Add in-memory object store
objectid: Add Compare helper

Runxi Yu 01d15bcc e67774f1

+2932 -980
+45
ancestor/ancestor.go
··· 1 + // Package ancestor answers commit ancestry queries. 2 + package ancestor 3 + 4 + import ( 5 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 6 + "codeberg.org/lindenii/furgit/internal/commitquery" 7 + "codeberg.org/lindenii/furgit/internal/peel" 8 + "codeberg.org/lindenii/furgit/objectid" 9 + "codeberg.org/lindenii/furgit/objectstore" 10 + ) 11 + 12 + // Is reports whether ancestor is reachable from descendant through commit 13 + // parent edges. 14 + // 15 + // Both inputs are peeled through annotated tags before commit traversal. 16 + func Is( 17 + store objectstore.Store, 18 + graph *commitgraphread.Reader, 19 + ancestor objectid.ObjectID, 20 + descendant objectid.ObjectID, 21 + ) (bool, error) { 22 + ancestorCommit, err := peel.ToCommit(store, ancestor) 23 + if err != nil { 24 + return false, err 25 + } 26 + 27 + descendantCommit, err := peel.ToCommit(store, descendant) 28 + if err != nil { 29 + return false, err 30 + } 31 + 32 + ctx := commitquery.NewContext(store, graph) 33 + 34 + ancestorIdx, err := ctx.ResolveOID(ancestorCommit) 35 + if err != nil { 36 + return false, err 37 + } 38 + 39 + descendantIdx, err := ctx.ResolveOID(descendantCommit) 40 + if err != nil { 41 + return false, err 42 + } 43 + 44 + return commitquery.IsAncestor(ctx, ancestorIdx, descendantIdx) 45 + }
+133
ancestor/integration_test.go
··· 1 + package ancestor_test 2 + 3 + import ( 4 + "errors" 5 + "testing" 6 + 7 + giterrors "codeberg.org/lindenii/furgit/errors" 8 + "codeberg.org/lindenii/furgit/internal/testgit" 9 + "codeberg.org/lindenii/furgit/objectid" 10 + 11 + "codeberg.org/lindenii/furgit/ancestor" 12 + ) 13 + 14 + func TestIsMatchesGitMergeBase(t *testing.T) { 15 + t.Parallel() 16 + 17 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 18 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 19 + ObjectFormat: algo, 20 + Bare: true, 21 + RefFormat: "files", 22 + }) 23 + 24 + _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) 25 + c1 := testRepo.CommitTree(t, tree1, "c1") 26 + 27 + _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) 28 + c2 := testRepo.CommitTree(t, tree2, "c2", c1) 29 + 30 + _, tree3 := testRepo.MakeSingleFileTree(t, "three.txt", []byte("three\n")) 31 + c3 := testRepo.CommitTree(t, tree3, "c3", c2) 32 + 33 + tag := testRepo.TagAnnotated(t, "tip", c2, "tip") 34 + 35 + store := testRepo.OpenObjectStore(t) 36 + 37 + got, err := ancestor.Is(store, nil, c1, tag) 38 + if err != nil { 39 + t.Fatalf("Is(c1, tag): %v", err) 40 + } 41 + 42 + want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) 43 + if got != want { 44 + t.Fatalf("Is(c1, tag)=%v, want %v", got, want) 45 + } 46 + 47 + got, err = ancestor.Is(store, nil, c3, c2) 48 + if err != nil { 49 + t.Fatalf("Is(c3, c2): %v", err) 50 + } 51 + 52 + want = gitMergeBaseIsAncestor(t, testRepo, c3, c2) 53 + if got != want { 54 + t.Fatalf("Is(c3, c2)=%v, want %v", got, want) 55 + } 56 + }) 57 + } 58 + 59 + func TestIsMatchesGitMergeBaseWithCommitGraph(t *testing.T) { 60 + t.Parallel() 61 + 62 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 63 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 64 + ObjectFormat: algo, 65 + Bare: true, 66 + RefFormat: "files", 67 + }) 68 + 69 + _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) 70 + c1 := testRepo.CommitTree(t, tree1, "c1") 71 + 72 + _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) 73 + c2 := testRepo.CommitTree(t, tree2, "c2", c1) 74 + 75 + testRepo.UpdateRef(t, "refs/heads/main", c2) 76 + testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") 77 + testRepo.CommitGraphWrite(t, "--reachable") 78 + 79 + store := testRepo.OpenObjectStore(t) 80 + graph := testRepo.OpenCommitGraph(t) 81 + 82 + got, err := ancestor.Is(store, graph, c1, c2) 83 + if err != nil { 84 + t.Fatalf("Is(c1, c2): %v", err) 85 + } 86 + 87 + want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) 88 + if got != want { 89 + t.Fatalf("Is(c1, c2)=%v, want %v", got, want) 90 + } 91 + }) 92 + } 93 + 94 + func TestIsMissingObject(t *testing.T) { 95 + t.Parallel() 96 + 97 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 98 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 99 + ObjectFormat: algo, 100 + Bare: true, 101 + RefFormat: "files", 102 + }) 103 + 104 + _, treeID, commitID := testRepo.MakeCommit(t, "missing") 105 + 106 + testRepo.RemoveLooseObject(t, treeID) 107 + 108 + store := testRepo.OpenObjectStore(t) 109 + 110 + _, err := ancestor.Is(store, nil, treeID, commitID) 111 + if err == nil { 112 + t.Fatal("expected error") 113 + } 114 + 115 + var missing *giterrors.ObjectMissingError 116 + if !errors.As(err, &missing) { 117 + t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) 118 + } 119 + 120 + if missing.OID != treeID { 121 + t.Fatalf("missing oid = %s, want %s", missing.OID, treeID) 122 + } 123 + }) 124 + } 125 + 126 + // gitMergeBaseIsAncestor reports Git's merge-base ancestry answer. 127 + func gitMergeBaseIsAncestor(t *testing.T, testRepo *testgit.TestRepo, left, right objectid.ObjectID) bool { 128 + t.Helper() 129 + 130 + out := testRepo.Run(t, "merge-base", left.String(), right.String()) 131 + 132 + return out == left.String() 133 + }
+118
ancestor/unit_test.go
··· 1 + package ancestor_test 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "testing" 7 + 8 + giterrors "codeberg.org/lindenii/furgit/errors" 9 + "codeberg.org/lindenii/furgit/internal/testgit" 10 + "codeberg.org/lindenii/furgit/object" 11 + "codeberg.org/lindenii/furgit/objectid" 12 + "codeberg.org/lindenii/furgit/objectstore/memory" 13 + "codeberg.org/lindenii/furgit/objecttype" 14 + 15 + "codeberg.org/lindenii/furgit/ancestor" 16 + ) 17 + 18 + // commitBody serializes one minimal commit body. 19 + func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { 20 + buf := fmt.Appendf(nil, "tree %s\n", tree.String()) 21 + for _, parent := range parents { 22 + buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) 23 + } 24 + 25 + buf = append(buf, []byte("\nmsg\n")...) 26 + 27 + return buf 28 + } 29 + 30 + // tagBody serializes one minimal annotated tag body. 31 + func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { 32 + targetName, ok := objecttype.Name(targetType) 33 + if !ok { 34 + panic("invalid tag target type") 35 + } 36 + 37 + return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) 38 + } 39 + 40 + // mustSerializeTree serializes one tree or fails the test. 41 + func mustSerializeTree(tb testing.TB, tree *object.Tree) []byte { 42 + tb.Helper() 43 + 44 + body, err := tree.SerializeWithoutHeader() 45 + if err != nil { 46 + tb.Fatalf("SerializeWithoutHeader: %v", err) 47 + } 48 + 49 + return body 50 + } 51 + 52 + func TestIs(t *testing.T) { 53 + t.Parallel() 54 + 55 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 56 + store := memory.New(algo) 57 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 58 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 59 + Mode: object.FileModeRegular, 60 + Name: []byte("f"), 61 + ID: blob, 62 + }}})) 63 + c1 := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 64 + c2 := store.AddObject(objecttype.TypeCommit, commitBody(tree, c1)) 65 + otherBlob := store.AddObject(objecttype.TypeBlob, []byte("other-blob\n")) 66 + otherTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 67 + Mode: object.FileModeRegular, 68 + Name: []byte("g"), 69 + ID: otherBlob, 70 + }}})) 71 + c3 := store.AddObject(objecttype.TypeCommit, commitBody(otherTree)) 72 + tag := store.AddObject(objecttype.TypeTag, tagBody(c2, objecttype.TypeCommit)) 73 + 74 + ok, err := ancestor.Is(store, nil, c1, tag) 75 + if err != nil { 76 + t.Fatalf("Is(c1, tag): %v", err) 77 + } 78 + 79 + if !ok { 80 + t.Fatal("expected c1 to be ancestor of tag->c2") 81 + } 82 + 83 + ok, err = ancestor.Is(store, nil, c3, c2) 84 + if err != nil { 85 + t.Fatalf("Is(c3, c2): %v", err) 86 + } 87 + 88 + if ok { 89 + t.Fatal("did not expect c3 to be ancestor of c2") 90 + } 91 + }) 92 + } 93 + 94 + func TestIsRejectsNonCommitAfterPeel(t *testing.T) { 95 + t.Parallel() 96 + 97 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 98 + store := memory.New(algo) 99 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 100 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 101 + Mode: object.FileModeRegular, 102 + Name: []byte("f"), 103 + ID: blob, 104 + }}})) 105 + commit := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 106 + tagToTree := store.AddObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) 107 + 108 + _, err := ancestor.Is(store, nil, commit, tagToTree) 109 + if err == nil { 110 + t.Fatal("expected error") 111 + } 112 + 113 + var typeErr *giterrors.ObjectTypeError 114 + if !errors.As(err, &typeErr) { 115 + t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) 116 + } 117 + }) 118 + }
+19 -43
config/config_test.go
··· 3 3 import ( 4 4 "bytes" 5 5 "os" 6 - "os/exec" 7 - "path/filepath" 8 6 "strings" 9 7 "testing" 10 8 ··· 16 14 func openConfig(t *testing.T, testRepo *testgit.TestRepo) *os.File { 17 15 t.Helper() 18 16 19 - cfgFile, err := os.Open(filepath.Join(testRepo.Dir(), "config")) 17 + root := testRepo.OpenGitRoot(t) 18 + 19 + cfgFile, err := root.Open("config") 20 20 if err != nil { 21 21 t.Fatalf("failed to open config: %v", err) 22 22 } ··· 30 30 return testRepo.Run(t, "config", "--get", key) 31 31 } 32 32 33 - func gitConfigGetE(testRepo *testgit.TestRepo, key string) (string, error) { 34 - //nolint:noctx 35 - cmd := exec.Command("git", "config", "--get", key) //#nosec G204 36 - cmd.Dir = testRepo.Dir() 37 - cmd.Env = testRepo.Env() 38 - out, err := cmd.CombinedOutput() 33 + func gitConfigGetE(t *testing.T, testRepo *testgit.TestRepo, key string) (string, error) { 34 + t.Helper() 39 35 40 - return strings.TrimSpace(string(out)), err 36 + return testRepo.RunE(t, "config", "--get", key) 41 37 } 42 38 43 39 func lookupValue(cfg *config.Config, section, subsection, key string) string { ··· 463 459 } 464 460 } 465 461 466 - func TestConfigEOFAfterKeyAgainstGit(t *testing.T) { //nolint:dupl 462 + func TestConfigEOFAfterKeyAgainstGit(t *testing.T) { 467 463 t.Parallel() 468 464 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 469 465 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 470 - cfgPath := filepath.Join(testRepo.Dir(), "config") 471 - 472 466 cfgData := []byte("[Core]BAre") 473 467 474 - err := os.WriteFile(cfgPath, cfgData, 0o600) 475 - if err != nil { 476 - t.Fatalf("failed to write config: %v", err) 477 - } 468 + testRepo.WriteFile(t, "config", cfgData, 0o600) 478 469 479 - gitValue, gitErr := gitConfigGetE(testRepo, "Core.BAre") 470 + gitValue, gitErr := gitConfigGetE(t, testRepo, "Core.BAre") 480 471 furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) 481 472 482 473 if (gitErr == nil) != (furErr == nil) { ··· 493 484 }) 494 485 } 495 486 496 - func TestConfigNULValueAgainstGit(t *testing.T) { //nolint:dupl 487 + func TestConfigNULValueAgainstGit(t *testing.T) { 497 488 t.Parallel() 498 489 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 499 490 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 500 - cfgPath := filepath.Join(testRepo.Dir(), "config") 501 - 502 491 cfgData := []byte("[Core]BAre=\x00") 503 492 504 - err := os.WriteFile(cfgPath, cfgData, 0o600) 505 - if err != nil { 506 - t.Fatalf("failed to write config: %v", err) 507 - } 493 + testRepo.WriteFile(t, "config", cfgData, 0o600) 508 494 509 - gitValue, gitErr := gitConfigGetE(testRepo, "Core.BAre") 495 + gitValue, gitErr := gitConfigGetE(t, testRepo, "Core.BAre") 510 496 furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) 511 497 512 498 if (gitErr == nil) != (furErr == nil) { ··· 523 509 }) 524 510 } 525 511 526 - func TestConfigCarriageReturnSeparatorAgainstGit(t *testing.T) { //nolint:dupl 512 + func TestConfigCarriageReturnSeparatorAgainstGit(t *testing.T) { 527 513 t.Parallel() 528 514 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 529 515 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 530 - cfgPath := filepath.Join(testRepo.Dir(), "config") 531 - 532 516 cfgData := []byte("[Core \"sub\"]\rBAre") 533 517 534 - err := os.WriteFile(cfgPath, cfgData, 0o600) 535 - if err != nil { 536 - t.Fatalf("failed to write config: %v", err) 537 - } 518 + testRepo.WriteFile(t, "config", cfgData, 0o600) 538 519 539 - gitValue, gitErr := gitConfigGetE(testRepo, "Core.sub.BAre") 520 + gitValue, gitErr := gitConfigGetE(t, testRepo, "Core.sub.BAre") 540 521 furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) 541 522 542 523 if (gitErr == nil) != (furErr == nil) { ··· 559 540 f.Add([]byte("[core \"sub\"]\nbare = true"), "core.sub.bare") 560 541 561 542 type fuzzRepoState struct { 562 - repo *testgit.TestRepo 563 - cfgPath string 543 + repo *testgit.TestRepo 564 544 } 565 545 566 546 repos := make(map[objectid.Algorithm]fuzzRepoState, len(objectid.SupportedAlgorithms())) 567 547 for _, algo := range objectid.SupportedAlgorithms() { 568 548 testRepo := testgit.NewRepo(f, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 569 549 repos[algo] = fuzzRepoState{ 570 - repo: testRepo, 571 - cfgPath: filepath.Join(testRepo.Dir(), "config"), 550 + repo: testRepo, 572 551 } 573 552 } 574 553 ··· 579 558 t.Fatalf("missing fuzz repo state for %v", algo) 580 559 } 581 560 582 - err := os.WriteFile(state.cfgPath, cfgData, 0o600) 583 - if err != nil { 584 - t.Fatalf("failed to write config: %v", err) 585 - } 561 + state.repo.WriteFile(t, "config", cfgData, 0o600) 586 562 587 - gitValue, gitErr := gitConfigGetE(state.repo, gitKey) 563 + gitValue, gitErr := gitConfigGetE(t, state.repo, gitKey) 588 564 589 565 furConfig, furErr := config.ParseConfig(bytes.NewReader(cfgData)) 590 566 if furErr == nil && furConfig == nil {
+31 -46
diff/trees/diff_test.go
··· 2 2 3 3 import ( 4 4 "errors" 5 - "os" 6 - "path/filepath" 7 5 "testing" 8 6 9 7 "codeberg.org/lindenii/furgit/diff/trees" ··· 19 17 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 20 18 repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: false}) 21 19 22 - writeTestFile(t, filepath.Join(repo.Dir(), "README.md"), "initial readme\n") 23 - writeTestFile(t, filepath.Join(repo.Dir(), "unchanged.txt"), "leave me as-is\n") 24 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "file_a.txt"), "alpha v1\n") 25 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "file_b.txt"), "beta v1\n") 26 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "deeper", "file_c.txt"), "gamma v1\n") 27 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "deeper", "old.txt"), "old branch\n") 28 - writeTestFile(t, filepath.Join(repo.Dir(), "treeB", "legacy.txt"), "legacy root\n") 29 - writeTestFile(t, filepath.Join(repo.Dir(), "treeB", "sub", "retired.txt"), "retired\n") 20 + writeTestFile(t, repo, "README.md", "initial readme\n") 21 + writeTestFile(t, repo, "unchanged.txt", "leave me as-is\n") 22 + writeTestFile(t, repo, "dir/file_a.txt", "alpha v1\n") 23 + writeTestFile(t, repo, "dir/nested/file_b.txt", "beta v1\n") 24 + writeTestFile(t, repo, "dir/nested/deeper/file_c.txt", "gamma v1\n") 25 + writeTestFile(t, repo, "dir/nested/deeper/old.txt", "old branch\n") 26 + writeTestFile(t, repo, "treeB/legacy.txt", "legacy root\n") 27 + writeTestFile(t, repo, "treeB/sub/retired.txt", "retired\n") 30 28 31 29 repo.Run(t, "add", ".") 32 30 baseTreeID := parseID(t, algo, repo.Run(t, "write-tree")) 33 31 34 - writeTestFile(t, filepath.Join(repo.Dir(), "README.md"), "updated readme\n") 32 + writeTestFile(t, repo, "README.md", "updated readme\n") 35 33 repo.Run(t, "rm", "-f", "dir/file_a.txt") 36 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "file_b.txt"), "beta v2\n") 34 + writeTestFile(t, repo, "dir/nested/file_b.txt", "beta v2\n") 37 35 repo.Run(t, "rm", "-f", "dir/nested/deeper/old.txt") 38 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "deeper", "new.txt"), "new branch entry\n") 39 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "deeper", "branch", "info.md"), "branch info\n") 40 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "deeper", "branch", "subbranch", "leaf.txt"), "leaf data\n") 41 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "nested", "deeper", "branch", "subbranch", "deep", "final.txt"), "final artifact\n") 42 - writeTestFile(t, filepath.Join(repo.Dir(), "dir", "newchild.txt"), "brand new sibling\n") 36 + writeTestFile(t, repo, "dir/nested/deeper/new.txt", "new branch entry\n") 37 + writeTestFile(t, repo, "dir/nested/deeper/branch/info.md", "branch info\n") 38 + writeTestFile(t, repo, "dir/nested/deeper/branch/subbranch/leaf.txt", "leaf data\n") 39 + writeTestFile(t, repo, "dir/nested/deeper/branch/subbranch/deep/final.txt", "final artifact\n") 40 + writeTestFile(t, repo, "dir/newchild.txt", "brand new sibling\n") 43 41 repo.Run(t, "rm", "-r", "-f", "treeB") 44 - writeTestFile(t, filepath.Join(repo.Dir(), "features", "alpha", "README.md"), "alpha docs\n") 45 - writeTestFile(t, filepath.Join(repo.Dir(), "features", "alpha", "beta", "gamma.txt"), "gamma payload\n") 46 - writeTestFile(t, filepath.Join(repo.Dir(), "modules", "v2", "core", "main.go"), "package core\n") 47 - writeTestFile(t, filepath.Join(repo.Dir(), "root_addition.txt"), "root level file\n") 42 + writeTestFile(t, repo, "features/alpha/README.md", "alpha docs\n") 43 + writeTestFile(t, repo, "features/alpha/beta/gamma.txt", "gamma payload\n") 44 + writeTestFile(t, repo, "modules/v2/core/main.go", "package core\n") 45 + writeTestFile(t, repo, "root_addition.txt", "root level file\n") 48 46 49 47 repo.Run(t, "add", ".") 50 48 updatedTreeID := parseID(t, algo, repo.Run(t, "write-tree")) 51 49 52 - store := openLooseStore(t, filepath.Join(repo.Dir(), ".git", "objects"), algo) 50 + store := openLooseStore(t, repo, algo) 53 51 readTree := makeReadTree(t, store, algo) 54 52 baseTree := mustReadTree(t, readTree, baseTreeID) 55 53 updatedTree := mustReadTree(t, readTree, updatedTreeID) ··· 103 101 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 104 102 repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: false}) 105 103 106 - writeTestFile(t, filepath.Join(repo.Dir(), "old_dir", "old.txt"), "stale directory\n") 107 - writeTestFile(t, filepath.Join(repo.Dir(), "old_dir", "sub1", "legacy.txt"), "legacy path\n") 108 - writeTestFile(t, filepath.Join(repo.Dir(), "old_dir", "sub1", "nested", "end.txt"), "legacy end\n") 104 + writeTestFile(t, repo, "old_dir/old.txt", "stale directory\n") 105 + writeTestFile(t, repo, "old_dir/sub1/legacy.txt", "legacy path\n") 106 + writeTestFile(t, repo, "old_dir/sub1/nested/end.txt", "legacy end\n") 109 107 110 108 repo.Run(t, "add", ".") 111 109 originalTreeID := parseID(t, algo, repo.Run(t, "write-tree")) 112 110 113 111 repo.Run(t, "rm", "-r", "-f", "old_dir") 114 - writeTestFile(t, filepath.Join(repo.Dir(), "fresh", "alpha", "beta", "new.txt"), "brand new directory\n") 115 - writeTestFile(t, filepath.Join(repo.Dir(), "fresh", "alpha", "docs", "note.md"), "docs note\n") 116 - writeTestFile(t, filepath.Join(repo.Dir(), "fresh", "alpha", "beta", "gamma", "delta.txt"), "delta payload\n") 112 + writeTestFile(t, repo, "fresh/alpha/beta/new.txt", "brand new directory\n") 113 + writeTestFile(t, repo, "fresh/alpha/docs/note.md", "docs note\n") 114 + writeTestFile(t, repo, "fresh/alpha/beta/gamma/delta.txt", "delta payload\n") 117 115 118 116 repo.Run(t, "add", ".") 119 117 nextTreeID := parseID(t, algo, repo.Run(t, "write-tree")) 120 118 121 - store := openLooseStore(t, filepath.Join(repo.Dir(), ".git", "objects"), algo) 119 + store := openLooseStore(t, repo, algo) 122 120 readTree := makeReadTree(t, store, algo) 123 121 originalTree := mustReadTree(t, readTree, originalTreeID) 124 122 nextTree := mustReadTree(t, readTree, nextTreeID) ··· 155 153 newNil bool 156 154 } 157 155 158 - func writeTestFile(t *testing.T, path, data string) { 156 + func writeTestFile(t *testing.T, repo *testgit.TestRepo, path, data string) { 159 157 t.Helper() 160 158 161 - err := os.MkdirAll(filepath.Dir(path), 0o755) 162 - if err != nil { 163 - t.Fatalf("create directory for %s: %v", path, err) 164 - } 165 - 166 - err = os.WriteFile(path, []byte(data), 0o644) 167 - if err != nil { 168 - t.Fatalf("write %s: %v", path, err) 169 - } 159 + repo.WriteFileAll(t, path, []byte(data), 0o755, 0o644) 170 160 } 171 161 172 - func openLooseStore(t *testing.T, objectsPath string, algo objectid.Algorithm) *loose.Store { 162 + func openLooseStore(t *testing.T, repo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { 173 163 t.Helper() 174 164 175 - root, err := os.OpenRoot(objectsPath) 176 - if err != nil { 177 - t.Fatalf("OpenRoot(%q): %v", objectsPath, err) 178 - } 179 - 180 - t.Cleanup(func() { _ = root.Close() }) 165 + root := repo.OpenObjectsRoot(t) 181 166 182 167 store, err := loose.New(root, algo) 183 168 if err != nil {
+2
errors/doc.go
··· 1 + // Package errors defines error types shared across furgit. 2 + package errors
+18
errors/missing.go
··· 1 + package errors 2 + 3 + import ( 4 + "fmt" 5 + 6 + "codeberg.org/lindenii/furgit/objectid" 7 + ) 8 + 9 + // ObjectMissingError indicates that a referenced object is absent from the 10 + // repository object store. 11 + type ObjectMissingError struct { 12 + OID objectid.ObjectID 13 + } 14 + 15 + // Error implements error. 16 + func (e *ObjectMissingError) Error() string { 17 + return fmt.Sprintf("missing object %s", e.OID) 18 + }
+31
errors/type.go
··· 1 + package errors 2 + 3 + import ( 4 + "fmt" 5 + 6 + "codeberg.org/lindenii/furgit/objectid" 7 + "codeberg.org/lindenii/furgit/objecttype" 8 + ) 9 + 10 + // ObjectTypeError indicates that a referenced object has a different type than 11 + // what the operation expected. 12 + type ObjectTypeError struct { 13 + OID objectid.ObjectID 14 + Got objecttype.Type 15 + Want objecttype.Type 16 + } 17 + 18 + // Error implements error. 19 + func (e *ObjectTypeError) Error() string { 20 + gotName, gotOK := objecttype.Name(e.Got) 21 + if !gotOK { 22 + gotName = fmt.Sprintf("type(%d)", e.Got) 23 + } 24 + 25 + wantName, wantOK := objecttype.Name(e.Want) 26 + if !wantOK { 27 + wantName = fmt.Sprintf("type(%d)", e.Want) 28 + } 29 + 30 + return fmt.Sprintf("object %s has type %s, want %s", e.OID, gotName, wantName) 31 + }
+2 -14
format/commitgraph/read/read_test.go
··· 2 2 3 3 import ( 4 4 "errors" 5 - "os" 6 5 "path/filepath" 7 6 "strconv" 8 7 "strings" ··· 174 173 func openReader(tb testing.TB, testRepo *testgit.TestRepo, mode read.OpenMode) *read.Reader { 175 174 tb.Helper() 176 175 177 - objectsPath := filepath.Join(testRepo.Dir(), "objects") 178 - 179 - root, err := os.OpenRoot(objectsPath) 180 - if err != nil { 181 - tb.Fatalf("os.OpenRoot(%q): %v", objectsPath, err) 182 - } 176 + root := testRepo.OpenObjectsRoot(tb) 183 177 184 178 reader, err := read.Open(root, testRepo.Algorithm(), mode) 185 - 186 - closeErr := root.Close() 187 - if closeErr != nil { 188 - tb.Fatalf("close objects root: %v", closeErr) 189 - } 190 - 191 179 if err != nil { 192 - tb.Fatalf("read.Open(%q): %v", objectsPath, err) 180 + tb.Fatalf("read.Open(objects): %v", err) 193 181 } 194 182 195 183 return reader
+30 -108
format/pack/ingest/ingest_test.go
··· 3 3 import ( 4 4 "bytes" 5 5 "errors" 6 + "io/fs" 6 7 "os" 7 8 "path/filepath" 8 9 "strings" ··· 11 12 "codeberg.org/lindenii/furgit/format/pack/ingest" 12 13 "codeberg.org/lindenii/furgit/internal/testgit" 13 14 "codeberg.org/lindenii/furgit/objectid" 14 - "codeberg.org/lindenii/furgit/repository" 15 15 ) 16 16 17 17 // fixturePath returns one fixture file path for the selected algorithm. ··· 99 99 100 100 // verifyReindexOracle regenerates idx/rev with upstream git index-pack and 101 101 // compares bytes with files produced by ingest. 102 - func verifyReindexOracle(t *testing.T, repo *testgit.TestRepo, packPath, idxPath, revPath string) { 102 + func verifyReindexOracle(t *testing.T, repo *testgit.TestRepo, packName, idxName, revName string) { 103 103 t.Helper() 104 104 105 105 oracleDir := t.TempDir() 106 106 oracleIdxPath := filepath.Join(oracleDir, "oracle.idx") 107 - _ = repo.Run(t, "index-pack", "--rev-index", "-o", oracleIdxPath, packPath) 107 + _ = repo.Run(t, "index-pack", "--rev-index", "-o", oracleIdxPath, filepath.Join("objects", "pack", packName)) 108 108 oracleRevPath := strings.TrimSuffix(oracleIdxPath, ".idx") + ".rev" 109 109 110 - idxRoot, err := os.OpenRoot(filepath.Dir(idxPath)) 111 - if err != nil { 112 - t.Fatalf("open idx root: %v", err) 113 - } 110 + packRoot := repo.OpenPackRoot(t) 114 111 115 - defer func() { 116 - err := idxRoot.Close() 117 - if err != nil { 118 - t.Fatalf("close idx root: %v", err) 119 - } 120 - }() 121 - 122 - gotIdx, err := idxRoot.ReadFile(filepath.Base(idxPath)) 112 + gotIdx, err := packRoot.ReadFile(idxName) 123 113 if err != nil { 124 114 t.Fatalf("read idx: %v", err) 125 115 } ··· 145 135 t.Fatal("idx bytes differ from git index-pack output") 146 136 } 147 137 148 - gotRev, err := idxRoot.ReadFile(filepath.Base(revPath)) 138 + gotRev, err := packRoot.ReadFile(revName) 149 139 if err != nil { 150 140 t.Fatalf("read rev: %v", err) 151 141 } ··· 169 159 170 160 receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 171 161 172 - packRoot, err := os.OpenRoot(filepath.Join(receiver.Dir(), "objects", "pack")) 173 - if err != nil { 174 - t.Fatalf("open pack root: %v", err) 175 - } 176 - 177 - defer func() { 178 - err = packRoot.Close() 179 - if err != nil { 180 - t.Fatalf("close pack root: %v", err) 181 - } 182 - }() 162 + packRoot := receiver.OpenPackRoot(t) 183 163 184 164 result, err := ingest.Ingest(bytes.NewReader(packBytes), packRoot, algo, false, true, nil) 185 165 if err != nil { ··· 209 189 t.Fatalf("stat rev: %v", err) 210 190 } 211 191 212 - idxPath := filepath.Join(receiver.Dir(), "objects", "pack", result.IdxName) 213 - packPath := filepath.Join(receiver.Dir(), "objects", "pack", result.PackName) 214 - revPath := filepath.Join(receiver.Dir(), "objects", "pack", result.RevName) 215 - _ = receiver.Run(t, "verify-pack", "-v", idxPath) 216 - verifyReindexOracle(t, receiver, packPath, idxPath, revPath) 192 + _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) 193 + verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName) 217 194 218 195 receiver.UpdateRef(t, "refs/heads/main", head) 219 196 _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") ··· 227 204 thinPack := fixtureBytes(t, algo, "thin.pack") 228 205 229 206 receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 230 - packDir := filepath.Join(receiver.Dir(), "objects", "pack") 231 - 232 - packRoot, err := os.OpenRoot(packDir) 233 - if err != nil { 234 - t.Fatalf("open pack root: %v", err) 235 - } 236 - 237 - defer func() { 238 - err = packRoot.Close() 239 - if err != nil { 240 - t.Fatalf("close pack root: %v", err) 241 - } 242 - }() 207 + packRoot := receiver.OpenPackRoot(t) 243 208 244 - _, err = ingest.Ingest(bytes.NewReader(thinPack), packRoot, algo, false, true, nil) 209 + _, err := ingest.Ingest(bytes.NewReader(thinPack), packRoot, algo, false, true, nil) 245 210 if err == nil { 246 211 t.Fatal("Ingest error = nil, want error") 247 212 } ··· 251 216 t.Fatalf("Ingest error type = %T (%v), want *ThinPackUnresolvedError", err, err) 252 217 } 253 218 254 - matches, err := filepath.Glob(filepath.Join(packDir, "pack-*.pack")) 219 + entries, err := fs.ReadDir(packRoot.FS(), ".") 255 220 if err != nil { 256 - t.Fatalf("glob pack files: %v", err) 221 + t.Fatalf("ReadDir(pack): %v", err) 257 222 } 258 223 259 - if len(matches) != 0 { 260 - t.Fatalf("found finalized pack files after failure: %v", matches) 224 + for _, entry := range entries { 225 + if strings.HasSuffix(entry.Name(), ".pack") { 226 + t.Fatalf("found finalized pack file after failure: %v", entry.Name()) 227 + } 261 228 } 262 229 }) 263 230 } ··· 271 238 thinPack := fixtureBytes(t, algo, "thin.pack") 272 239 receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 273 240 274 - packRoot, err := os.OpenRoot(filepath.Join(receiver.Dir(), "objects", "pack")) 275 - if err != nil { 276 - t.Fatalf("open pack root: %v", err) 277 - } 278 - 279 - defer func() { 280 - err = packRoot.Close() 281 - if err != nil { 282 - t.Fatalf("close pack root: %v", err) 283 - } 284 - }() 241 + packRoot := receiver.OpenPackRoot(t) 285 242 286 - _, err = ingest.Ingest(bytes.NewReader(basePack), packRoot, algo, false, false, nil) 243 + _, err := ingest.Ingest(bytes.NewReader(basePack), packRoot, algo, false, false, nil) 287 244 if err != nil { 288 245 t.Fatalf("ingest base pack: %v", err) 289 246 } 290 247 291 - receiverRoot, err := os.OpenRoot(receiver.Dir()) 292 - if err != nil { 293 - t.Fatalf("open receiver root: %v", err) 294 - } 295 - 296 - defer func() { 297 - err = receiverRoot.Close() 298 - if err != nil { 299 - t.Fatalf("close receiver root: %v", err) 300 - } 301 - }() 302 - 303 - receiverRepo, err := repository.Open(receiverRoot) 304 - if err != nil { 305 - t.Fatalf("repository.Open(receiver): %v", err) 306 - } 307 - 308 - defer func() { 309 - err = receiverRepo.Close() 310 - if err != nil { 311 - t.Fatalf("close receiver repo: %v", err) 312 - } 313 - }() 248 + receiverRepo := receiver.OpenRepository(t) 314 249 315 250 result, err := ingest.Ingest(bytes.NewReader(thinPack), packRoot, algo, true, true, receiverRepo.Objects()) 316 251 if err != nil { ··· 321 256 t.Fatal("ThinFixed = false, want true") 322 257 } 323 258 324 - idxPath := filepath.Join(receiver.Dir(), "objects", "pack", result.IdxName) 325 - packPath := filepath.Join(receiver.Dir(), "objects", "pack", result.PackName) 326 - revPath := filepath.Join(receiver.Dir(), "objects", "pack", result.RevName) 327 - _ = receiver.Run(t, "verify-pack", "-v", idxPath) 328 - verifyReindexOracle(t, receiver, packPath, idxPath, revPath) 259 + _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName)) 260 + verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName) 329 261 receiver.UpdateRef(t, "refs/heads/main", head) 330 262 _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling") 331 263 }) ··· 343 275 packBytes[len(packBytes)-1] ^= 0xff 344 276 345 277 receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 346 - packDir := filepath.Join(receiver.Dir(), "objects", "pack") 347 - 348 - packRoot, err := os.OpenRoot(packDir) 349 - if err != nil { 350 - t.Fatalf("open pack root: %v", err) 351 - } 352 - 353 - defer func() { 354 - err = packRoot.Close() 355 - if err != nil { 356 - t.Fatalf("close pack root: %v", err) 357 - } 358 - }() 278 + packRoot := receiver.OpenPackRoot(t) 359 279 360 - _, err = ingest.Ingest(bytes.NewReader(packBytes), packRoot, algo, false, true, nil) 280 + _, err := ingest.Ingest(bytes.NewReader(packBytes), packRoot, algo, false, true, nil) 361 281 if err == nil { 362 282 t.Fatal("Ingest error = nil, want error") 363 283 } ··· 367 287 t.Fatalf("Ingest error type = %T (%v), want *PackTrailerMismatchError", err, err) 368 288 } 369 289 370 - matches, err := filepath.Glob(filepath.Join(packDir, "pack-*.pack")) 290 + entries, err := fs.ReadDir(packRoot.FS(), ".") 371 291 if err != nil { 372 - t.Fatalf("glob pack files: %v", err) 292 + t.Fatalf("ReadDir(pack): %v", err) 373 293 } 374 294 375 - if len(matches) != 0 { 376 - t.Fatalf("found finalized pack files after failure: %v", matches) 295 + for _, entry := range entries { 296 + if strings.HasSuffix(entry.Name(), ".pack") { 297 + t.Fatalf("found finalized pack file after failure: %v", entry.Name()) 298 + } 377 299 } 378 300 }) 379 301 }
+30
internal/commitquery/ancestor.go
··· 1 + package commitquery 2 + 3 + // IsAncestor reports whether ancestor is reachable from descendant through 4 + // commit parent edges. 5 + func IsAncestor(ctx *Context, ancestor, descendant NodeIndex) (bool, error) { 6 + if ancestor == descendant { 7 + return true, nil 8 + } 9 + 10 + ancestorGeneration := ctx.EffectiveGeneration(ancestor) 11 + descendantGeneration := ctx.EffectiveGeneration(descendant) 12 + 13 + if ancestorGeneration != generationInfinity && 14 + descendantGeneration != generationInfinity && 15 + ancestorGeneration > descendantGeneration { 16 + return false, nil 17 + } 18 + 19 + minGeneration := uint64(0) 20 + if ancestorGeneration != generationInfinity { 21 + minGeneration = ancestorGeneration 22 + } 23 + 24 + _, err := paintDownToCommon(ctx, ancestor, []NodeIndex{descendant}, minGeneration) 25 + if err != nil { 26 + return false, err 27 + } 28 + 29 + return ctx.HasAnyMarks(ancestor, markRight), nil 30 + }
+14
internal/commitquery/bits.go
··· 1 + package commitquery 2 + 3 + type markBits uint8 4 + 5 + const ( 6 + markLeft markBits = 1 << iota 7 + markRight 8 + markStale 9 + markResult 10 + ) 11 + 12 + const ( 13 + allMarks = markLeft | markRight | markStale | markResult 14 + )
+17
internal/commitquery/commit.go
··· 1 + package commitquery 2 + 3 + import ( 4 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + ) 7 + 8 + // Commit stores the metadata needed by commit-domain queries. 9 + type Commit struct { 10 + ID objectid.ObjectID 11 + Parents []Parent 12 + CommitTime int64 13 + Generation uint64 14 + HasGeneration bool 15 + GraphPos commitgraphread.Position 16 + HasGraphPos bool 17 + }
+25
internal/commitquery/compare.go
··· 1 + package commitquery 2 + 3 + import "codeberg.org/lindenii/furgit/objectid" 4 + 5 + // Compare compares two internal nodes using merge-base queue ordering. 6 + func (ctx *Context) Compare(left, right NodeIndex) int { 7 + leftGeneration := ctx.EffectiveGeneration(left) 8 + rightGeneration := ctx.EffectiveGeneration(right) 9 + 10 + switch { 11 + case leftGeneration < rightGeneration: 12 + return -1 13 + case leftGeneration > rightGeneration: 14 + return 1 15 + } 16 + 17 + switch { 18 + case ctx.nodes[left].commitTime < ctx.nodes[right].commitTime: 19 + return -1 20 + case ctx.nodes[left].commitTime > ctx.nodes[right].commitTime: 21 + return 1 22 + } 23 + 24 + return objectid.Compare(ctx.nodes[left].id, ctx.nodes[right].id) 25 + }
+32
internal/commitquery/context.go
··· 1 + // Package commitquery provides private commit-domain query routines. 2 + package commitquery 3 + 4 + import ( 5 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 6 + "codeberg.org/lindenii/furgit/objectid" 7 + "codeberg.org/lindenii/furgit/objectstore" 8 + ) 9 + 10 + // Context owns the mutable node arena for one commit query. 11 + type Context struct { 12 + store objectstore.Store 13 + graph *commitgraphread.Reader 14 + 15 + nodes []node 16 + 17 + byOID map[objectid.ObjectID]NodeIndex 18 + byGraphPos map[commitgraphread.Position]NodeIndex 19 + 20 + markPhase uint32 21 + touched []NodeIndex 22 + } 23 + 24 + // NewContext builds one empty query context over one object store and optional commit-graph reader. 25 + func NewContext(store objectstore.Store, graph *commitgraphread.Reader) *Context { 26 + return &Context{ 27 + store: store, 28 + graph: graph, 29 + byOID: make(map[objectid.ObjectID]NodeIndex), 30 + byGraphPos: make(map[commitgraphread.Position]NodeIndex), 31 + } 32 + }
+5
internal/commitquery/errors.go
··· 1 + package commitquery 2 + 3 + import "errors" 4 + 5 + var errBadGenerationOrder = errors.New("commitquery: priority queue violated generation ordering")
+43
internal/commitquery/generation.go
··· 1 + package commitquery 2 + 3 + import ( 4 + "math" 5 + 6 + "codeberg.org/lindenii/furgit/objectid" 7 + ) 8 + 9 + // EffectiveGeneration returns one node's generation value. 10 + func (ctx *Context) EffectiveGeneration(idx NodeIndex) uint64 { 11 + if !ctx.nodes[idx].hasGeneration { 12 + return generationInfinity 13 + } 14 + 15 + return ctx.nodes[idx].generation 16 + } 17 + 18 + const ( 19 + generationInfinity = uint64(math.MaxUint64) 20 + ) 21 + 22 + func compareByGeneration(ctx *Context) func(NodeIndex, NodeIndex) int { 23 + return func(left, right NodeIndex) int { 24 + leftGeneration := ctx.EffectiveGeneration(left) 25 + rightGeneration := ctx.EffectiveGeneration(right) 26 + 27 + switch { 28 + case leftGeneration < rightGeneration: 29 + return -1 30 + case leftGeneration > rightGeneration: 31 + return 1 32 + } 33 + 34 + switch { 35 + case ctx.nodes[left].commitTime < ctx.nodes[right].commitTime: 36 + return -1 37 + case ctx.nodes[left].commitTime > ctx.nodes[right].commitTime: 38 + return 1 39 + } 40 + 41 + return objectid.Compare(ctx.nodes[left].id, ctx.nodes[right].id) 42 + } 43 + }
+107
internal/commitquery/graph_pos.go
··· 1 + package commitquery 2 + 3 + import commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 4 + 5 + // ResolveGraphPos resolves one commit-graph position to one internal query node. 6 + func (ctx *Context) ResolveGraphPos(pos commitgraphread.Position) (NodeIndex, error) { 7 + idx, ok := ctx.byGraphPos[pos] 8 + if ok { 9 + err := ctx.ensureLoaded(idx) 10 + if err != nil { 11 + return 0, err 12 + } 13 + 14 + return idx, nil 15 + } 16 + 17 + commit, err := ctx.graph.CommitAt(pos) 18 + if err != nil { 19 + return 0, err 20 + } 21 + 22 + idx, ok = ctx.byOID[commit.OID] 23 + if !ok { 24 + idx = ctx.newNode(commit.OID) 25 + ctx.byOID[commit.OID] = idx 26 + } 27 + 28 + ctx.byGraphPos[pos] = idx 29 + ctx.nodes[idx].graphPos = pos 30 + ctx.nodes[idx].hasGraphPos = true 31 + 32 + err = ctx.loadCommitAtGraphPos(idx, pos) 33 + if err != nil { 34 + delete(ctx.byGraphPos, pos) 35 + 36 + return 0, err 37 + } 38 + 39 + return idx, nil 40 + } 41 + 42 + // loadByGraphPos populates one node from a commit-graph position. 43 + func (ctx *Context) loadByGraphPos(idx NodeIndex) error { 44 + pos := ctx.nodes[idx].graphPos 45 + 46 + return ctx.loadCommitAtGraphPos(idx, pos) 47 + } 48 + 49 + func (ctx *Context) loadCommitAtGraphPos(idx NodeIndex, pos commitgraphread.Position) error { 50 + commit, err := ctx.graph.CommitAt(pos) 51 + if err != nil { 52 + return err 53 + } 54 + 55 + parents := make([]Parent, 0, 2+len(commit.ExtraParents)) 56 + 57 + if commit.Parent1.Valid { 58 + parentOID, err := ctx.graph.OIDAt(commit.Parent1.Pos) 59 + if err != nil { 60 + return err 61 + } 62 + 63 + parents = append(parents, Parent{ 64 + ID: parentOID, 65 + GraphPos: commit.Parent1.Pos, 66 + HasGraphPos: true, 67 + }) 68 + } 69 + 70 + if commit.Parent2.Valid { 71 + parentOID, err := ctx.graph.OIDAt(commit.Parent2.Pos) 72 + if err != nil { 73 + return err 74 + } 75 + 76 + parents = append(parents, Parent{ 77 + ID: parentOID, 78 + GraphPos: commit.Parent2.Pos, 79 + HasGraphPos: true, 80 + }) 81 + } 82 + 83 + for _, parentPos := range commit.ExtraParents { 84 + parentOID, err := ctx.graph.OIDAt(parentPos) 85 + if err != nil { 86 + return err 87 + } 88 + 89 + parents = append(parents, Parent{ 90 + ID: parentOID, 91 + GraphPos: parentPos, 92 + HasGraphPos: true, 93 + }) 94 + } 95 + 96 + data := Commit{ 97 + ID: commit.OID, 98 + Parents: parents, 99 + CommitTime: commit.CommitTimeUnix, 100 + Generation: commit.GenerationV2, 101 + HasGeneration: commit.GenerationV2 != 0, 102 + GraphPos: pos, 103 + HasGraphPos: true, 104 + } 105 + 106 + return ctx.populateNode(idx, data) 107 + }
+14
internal/commitquery/load.go
··· 1 + package commitquery 2 + 3 + // ensureLoaded completes one node's metadata load if it has not been loaded yet. 4 + func (ctx *Context) ensureLoaded(idx NodeIndex) error { 5 + if ctx.nodes[idx].loaded { 6 + return nil 7 + } 8 + 9 + if ctx.nodes[idx].hasGraphPos { 10 + return ctx.loadByGraphPos(idx) 11 + } 12 + 13 + return ctx.loadByOID(idx) 14 + }
+67
internal/commitquery/marks.go
··· 1 + package commitquery 2 + 3 + // Marks returns the mark bits of one internal node. 4 + func (ctx *Context) Marks(idx NodeIndex) markBits { 5 + return ctx.nodes[idx].marks 6 + } 7 + 8 + // HasAnyMarks reports whether one internal node has any requested bit. 9 + func (ctx *Context) HasAnyMarks(idx NodeIndex, bits markBits) bool { 10 + return ctx.nodes[idx].marks&bits != 0 11 + } 12 + 13 + // HasAllMarks reports whether one internal node already has all requested bits. 14 + func (ctx *Context) HasAllMarks(idx NodeIndex, bits markBits) bool { 15 + return ctx.nodes[idx].marks&bits == bits 16 + } 17 + 18 + // SetMarks ORs one set of mark bits into one internal node. 19 + func (ctx *Context) SetMarks(idx NodeIndex, bits markBits) { 20 + newBits := bits &^ ctx.nodes[idx].marks 21 + if newBits == 0 { 22 + return 23 + } 24 + 25 + ctx.trackTouched(idx) 26 + ctx.nodes[idx].marks |= bits 27 + } 28 + 29 + // ClearMarks removes one set of mark bits from one internal node. 30 + func (ctx *Context) ClearMarks(idx NodeIndex, bits markBits) { 31 + if ctx.nodes[idx].marks&bits == 0 { 32 + return 33 + } 34 + 35 + ctx.trackTouched(idx) 36 + ctx.nodes[idx].marks &^= bits 37 + } 38 + 39 + // BeginMarkPhase starts one tracked mark-mutation phase. 40 + func (ctx *Context) BeginMarkPhase() { 41 + ctx.markPhase++ 42 + if ctx.markPhase == 0 { 43 + ctx.markPhase++ 44 + for i := range ctx.nodes { 45 + ctx.nodes[i].touchedPhase = 0 46 + } 47 + } 48 + 49 + ctx.touched = ctx.touched[:0] 50 + } 51 + 52 + // ClearTouchedMarks clears the provided bits from all nodes touched in the 53 + // current mark phase. 54 + func (ctx *Context) ClearTouchedMarks(bits markBits) { 55 + for _, idx := range ctx.touched { 56 + ctx.nodes[idx].marks &^= bits 57 + } 58 + } 59 + 60 + func (ctx *Context) trackTouched(idx NodeIndex) { 61 + if ctx.nodes[idx].touchedPhase == ctx.markPhase { 62 + return 63 + } 64 + 65 + ctx.nodes[idx].touchedPhase = ctx.markPhase 66 + ctx.touched = append(ctx.touched, idx) 67 + }
+105
internal/commitquery/merge_bases.go
··· 1 + package commitquery 2 + 3 + import "slices" 4 + 5 + // MergeBases computes fully reduced merge bases using one query context. 6 + func MergeBases(ctx *Context, left, right NodeIndex) ([]NodeIndex, error) { 7 + if left == right { 8 + return []NodeIndex{left}, nil 9 + } 10 + 11 + candidates, err := paintDownToCommon(ctx, left, []NodeIndex{right}, 0) 12 + if err != nil { 13 + return nil, err 14 + } 15 + 16 + if len(candidates) <= 1 { 17 + slices.SortFunc(candidates, ctx.Compare) 18 + 19 + return candidates, nil 20 + } 21 + 22 + ctx.ClearTouchedMarks(allMarks) 23 + 24 + reduced, err := removeRedundant(ctx, candidates) 25 + if err != nil { 26 + return nil, err 27 + } 28 + 29 + slices.SortFunc(reduced, ctx.Compare) 30 + 31 + return reduced, nil 32 + } 33 + 34 + func paintDownToCommon(ctx *Context, left NodeIndex, rights []NodeIndex, minGeneration uint64) ([]NodeIndex, error) { 35 + ctx.BeginMarkPhase() 36 + 37 + ctx.SetMarks(left, markLeft) 38 + 39 + if len(rights) == 0 { 40 + return []NodeIndex{left}, nil 41 + } 42 + 43 + queue := NewPriorityQueue(ctx) 44 + queue.PushNode(left) 45 + 46 + for _, right := range rights { 47 + ctx.SetMarks(right, markRight) 48 + queue.PushNode(right) 49 + } 50 + 51 + lastGeneration := generationInfinity 52 + results := make([]NodeIndex, 0, 4) 53 + 54 + for queueHasNonStale(ctx, queue) { 55 + idx := queue.PopNode() 56 + 57 + generation := ctx.EffectiveGeneration(idx) 58 + if generation > lastGeneration { 59 + return nil, errBadGenerationOrder 60 + } 61 + 62 + lastGeneration = generation 63 + if generation < minGeneration { 64 + break 65 + } 66 + 67 + flags := ctx.Marks(idx) & (markLeft | markRight | markStale) 68 + if flags == (markLeft | markRight) { 69 + if !ctx.HasAnyMarks(idx, markResult) { 70 + ctx.SetMarks(idx, markResult) 71 + results = append(results, idx) 72 + } 73 + 74 + flags |= markStale 75 + } 76 + 77 + for _, parent := range ctx.Parents(idx) { 78 + if ctx.HasAllMarks(parent, flags) { 79 + continue 80 + } 81 + 82 + ctx.SetMarks(parent, flags) 83 + queue.PushNode(parent) 84 + } 85 + } 86 + 87 + out := results[:0] 88 + for _, idx := range results { 89 + if !ctx.HasAnyMarks(idx, markStale) { 90 + out = append(out, idx) 91 + } 92 + } 93 + 94 + return out, nil 95 + } 96 + 97 + func queueHasNonStale(ctx *Context, queue *PriorityQueue) bool { 98 + for _, idx := range queue.items { 99 + if !ctx.HasAnyMarks(idx, markStale) { 100 + return true 101 + } 102 + } 103 + 104 + return false 105 + }
+39
internal/commitquery/node.go
··· 1 + package commitquery 2 + 3 + import ( 4 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + ) 7 + 8 + // NodeIndex identifies one internal query node. 9 + type NodeIndex int 10 + 11 + // node stores one mutable commit traversal node. 12 + type node struct { 13 + id objectid.ObjectID 14 + 15 + parents []NodeIndex 16 + 17 + commitTime int64 18 + generation uint64 19 + 20 + hasGeneration bool 21 + hasGraphPos bool 22 + loaded bool 23 + 24 + graphPos commitgraphread.Position 25 + marks markBits 26 + 27 + touchedPhase uint32 28 + } 29 + 30 + // newNode allocates one empty internal node. 31 + func (ctx *Context) newNode(id objectid.ObjectID) NodeIndex { 32 + count := len(ctx.nodes) 33 + 34 + idx := NodeIndex(count) 35 + 36 + ctx.nodes = append(ctx.nodes, node{id: id}) 37 + 38 + return idx 39 + }
+95
internal/commitquery/oid.go
··· 1 + package commitquery 2 + 3 + import ( 4 + stderrors "errors" 5 + 6 + giterrors "codeberg.org/lindenii/furgit/errors" 7 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 8 + "codeberg.org/lindenii/furgit/object" 9 + "codeberg.org/lindenii/furgit/objectid" 10 + "codeberg.org/lindenii/furgit/objectstore" 11 + "codeberg.org/lindenii/furgit/objecttype" 12 + ) 13 + 14 + // ID returns the canonical object ID of one internal node. 15 + func (ctx *Context) ID(idx NodeIndex) objectid.ObjectID { 16 + return ctx.nodes[idx].id 17 + } 18 + 19 + // CommitTime returns the committer timestamp used for one internal node. 20 + func (ctx *Context) CommitTime(idx NodeIndex) int64 { 21 + return ctx.nodes[idx].commitTime 22 + } 23 + 24 + // ResolveOID resolves one commit object ID to one internal query node. 25 + func (ctx *Context) ResolveOID(id objectid.ObjectID) (NodeIndex, error) { 26 + idx, ok := ctx.byOID[id] 27 + if ok { 28 + err := ctx.ensureLoaded(idx) 29 + if err != nil { 30 + return 0, err 31 + } 32 + 33 + return idx, nil 34 + } 35 + 36 + idx = ctx.newNode(id) 37 + ctx.byOID[id] = idx 38 + 39 + err := ctx.loadByOID(idx) 40 + if err != nil { 41 + delete(ctx.byOID, id) 42 + 43 + return 0, err 44 + } 45 + 46 + return idx, nil 47 + } 48 + 49 + // loadByOID populates one node from an object ID. 50 + func (ctx *Context) loadByOID(idx NodeIndex) error { 51 + id := ctx.nodes[idx].id 52 + 53 + if ctx.graph != nil { 54 + pos, err := ctx.graph.Lookup(id) 55 + if err != nil { 56 + var notFound *commitgraphread.NotFoundError 57 + if !stderrors.As(err, &notFound) { 58 + return err 59 + } 60 + } else { 61 + return ctx.loadCommitAtGraphPos(idx, pos) 62 + } 63 + } 64 + 65 + ty, content, err := ctx.store.ReadBytesContent(id) 66 + if err != nil { 67 + if stderrors.Is(err, objectstore.ErrObjectNotFound) { 68 + return &giterrors.ObjectMissingError{OID: id} 69 + } 70 + 71 + return err 72 + } 73 + 74 + if ty != objecttype.TypeCommit { 75 + return &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} 76 + } 77 + 78 + commitObj, err := object.ParseCommit(content, id.Algorithm()) 79 + if err != nil { 80 + return err 81 + } 82 + 83 + parents := make([]Parent, 0, len(commitObj.Parents)) 84 + for _, parentID := range commitObj.Parents { 85 + parents = append(parents, Parent{ID: parentID}) 86 + } 87 + 88 + commit := Commit{ 89 + ID: id, 90 + Parents: parents, 91 + CommitTime: commitObj.Committer.WhenUnix, 92 + } 93 + 94 + return ctx.populateNode(idx, commit) 95 + }
+27
internal/commitquery/parent.go
··· 1 + package commitquery 2 + 3 + import ( 4 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + ) 7 + 8 + // Parent references one commit parent. 9 + type Parent struct { 10 + ID objectid.ObjectID 11 + GraphPos commitgraphread.Position 12 + HasGraphPos bool 13 + } 14 + 15 + // Parents returns resolved parent node indices for one internal node. 16 + func (ctx *Context) Parents(idx NodeIndex) []NodeIndex { 17 + return ctx.nodes[idx].parents 18 + } 19 + 20 + // resolveParent resolves one parent descriptor to one internal node. 21 + func (ctx *Context) resolveParent(parent Parent) (NodeIndex, error) { 22 + if parent.HasGraphPos { 23 + return ctx.ResolveGraphPos(parent.GraphPos) 24 + } 25 + 26 + return ctx.ResolveOID(parent.ID) 27 + }
+42
internal/commitquery/populate.go
··· 1 + package commitquery 2 + 3 + import "fmt" 4 + 5 + // populateNode fills one node's metadata and resolves its parents. 6 + func (ctx *Context) populateNode(idx NodeIndex, commit Commit) error { 7 + if ctx.nodes[idx].loaded { 8 + if ctx.nodes[idx].id != commit.ID { 9 + return fmt.Errorf("commitquery: node identity mismatch: have %s, got %s", ctx.nodes[idx].id, commit.ID) 10 + } 11 + 12 + return nil 13 + } 14 + 15 + ctx.nodes[idx].id = commit.ID 16 + ctx.nodes[idx].commitTime = commit.CommitTime 17 + ctx.nodes[idx].generation = commit.Generation 18 + ctx.nodes[idx].hasGeneration = commit.HasGeneration 19 + 20 + if commit.HasGraphPos { 21 + ctx.nodes[idx].graphPos = commit.GraphPos 22 + ctx.nodes[idx].hasGraphPos = true 23 + ctx.byGraphPos[commit.GraphPos] = idx 24 + } 25 + 26 + ctx.nodes[idx].loaded = true 27 + ctx.nodes[idx].parents = ctx.nodes[idx].parents[:0] 28 + 29 + for _, parent := range commit.Parents { 30 + parentIdx, err := ctx.resolveParent(parent) 31 + if err != nil { 32 + ctx.nodes[idx].loaded = false 33 + ctx.nodes[idx].parents = nil 34 + 35 + return err 36 + } 37 + 38 + ctx.nodes[idx].parents = append(ctx.nodes[idx].parents, parentIdx) 39 + } 40 + 41 + return nil 42 + }
+68
internal/commitquery/priority_queue.go
··· 1 + package commitquery 2 + 3 + import "container/heap" 4 + 5 + // PriorityQueue orders internal nodes using one query context's comparator. 6 + type PriorityQueue struct { 7 + ctx *Context 8 + items []NodeIndex 9 + } 10 + 11 + // NewPriorityQueue builds one empty priority queue over one query context. 12 + func NewPriorityQueue(ctx *Context) *PriorityQueue { 13 + queue := &PriorityQueue{ctx: ctx} 14 + heap.Init(queue) 15 + 16 + return queue 17 + } 18 + 19 + // Len reports the number of queued items. 20 + func (queue *PriorityQueue) Len() int { 21 + return len(queue.items) 22 + } 23 + 24 + // Less reports whether one heap slot sorts ahead of another. 25 + func (queue *PriorityQueue) Less(left, right int) bool { 26 + return queue.ctx.Compare(queue.items[left], queue.items[right]) > 0 27 + } 28 + 29 + // Swap exchanges two heap slots. 30 + func (queue *PriorityQueue) Swap(left, right int) { 31 + queue.items[left], queue.items[right] = queue.items[right], queue.items[left] 32 + } 33 + 34 + // Push appends one heap element. 35 + func (queue *PriorityQueue) Push(item any) { 36 + idx, ok := item.(NodeIndex) 37 + if !ok { 38 + panic("commitquery: heap push item is not a NodeIndex") 39 + } 40 + 41 + queue.items = append(queue.items, idx) 42 + } 43 + 44 + // Pop removes one heap element. 45 + func (queue *PriorityQueue) Pop() any { 46 + last := len(queue.items) - 1 47 + item := queue.items[last] 48 + queue.items = queue.items[:last] 49 + 50 + return item 51 + } 52 + 53 + // PushNode inserts one internal node. 54 + func (queue *PriorityQueue) PushNode(idx NodeIndex) { 55 + heap.Push(queue, idx) 56 + } 57 + 58 + // PopNode removes the highest-priority internal node. 59 + func (queue *PriorityQueue) PopNode() NodeIndex { 60 + item := heap.Pop(queue) 61 + 62 + idx, ok := item.(NodeIndex) 63 + if !ok { 64 + panic("commitquery: heap pop item is not a NodeIndex") 65 + } 66 + 67 + return idx 68 + }
+166
internal/commitquery/reduce.go
··· 1 + package commitquery 2 + 3 + import ( 4 + "slices" 5 + ) 6 + 7 + // removeRedundant removes redundant merge-base candidates. 8 + func removeRedundant(ctx *Context, candidates []NodeIndex) ([]NodeIndex, error) { 9 + for _, idx := range candidates { 10 + if ctx.EffectiveGeneration(idx) != generationInfinity { 11 + return removeRedundantWithGen(ctx, candidates), nil 12 + } 13 + } 14 + 15 + return removeRedundantNoGen(ctx, candidates) 16 + } 17 + 18 + func removeRedundantNoGen(ctx *Context, candidates []NodeIndex) ([]NodeIndex, error) { 19 + redundant := make([]bool, len(candidates)) 20 + work := make([]NodeIndex, 0, len(candidates)-1) 21 + filledIndex := make([]int, 0, len(candidates)-1) 22 + 23 + for i, candidate := range candidates { 24 + if redundant[i] { 25 + continue 26 + } 27 + 28 + work = work[:0] 29 + filledIndex = filledIndex[:0] 30 + 31 + minGeneration := ctx.EffectiveGeneration(candidate) 32 + 33 + for j, other := range candidates { 34 + if i == j || redundant[j] { 35 + continue 36 + } 37 + 38 + work = append(work, other) 39 + filledIndex = append(filledIndex, j) 40 + 41 + otherGeneration := ctx.EffectiveGeneration(other) 42 + if otherGeneration < minGeneration { 43 + minGeneration = otherGeneration 44 + } 45 + } 46 + 47 + _, err := paintDownToCommon(ctx, candidate, work, minGeneration) 48 + if err != nil { 49 + return nil, err 50 + } 51 + 52 + if ctx.HasAnyMarks(candidate, markRight) { 53 + redundant[i] = true 54 + } 55 + 56 + for j, other := range work { 57 + if ctx.HasAnyMarks(other, markLeft) { 58 + redundant[filledIndex[j]] = true 59 + } 60 + } 61 + 62 + ctx.ClearTouchedMarks(allMarks) 63 + } 64 + 65 + out := make([]NodeIndex, 0, len(candidates)) 66 + for i, idx := range candidates { 67 + if !redundant[i] { 68 + out = append(out, idx) 69 + } 70 + } 71 + 72 + return out, nil 73 + } 74 + 75 + func removeRedundantWithGen(ctx *Context, candidates []NodeIndex) []NodeIndex { 76 + sorted := append([]NodeIndex(nil), candidates...) 77 + slices.SortFunc(sorted, compareByGeneration(ctx)) 78 + 79 + minGeneration := ctx.EffectiveGeneration(sorted[0]) 80 + minGenPos := 0 81 + countStillIndependent := len(candidates) 82 + 83 + ctx.BeginMarkPhase() 84 + 85 + walkStart := make([]NodeIndex, 0, len(candidates)*2) 86 + 87 + for _, idx := range candidates { 88 + ctx.SetMarks(idx, markResult) 89 + 90 + for _, parent := range ctx.Parents(idx) { 91 + if ctx.HasAnyMarks(parent, markStale) { 92 + continue 93 + } 94 + 95 + ctx.SetMarks(parent, markStale) 96 + walkStart = append(walkStart, parent) 97 + } 98 + } 99 + 100 + slices.SortFunc(walkStart, compareByGeneration(ctx)) 101 + 102 + for _, idx := range walkStart { 103 + ctx.ClearMarks(idx, markStale) 104 + } 105 + 106 + for i := len(walkStart) - 1; i >= 0 && countStillIndependent > 1; i-- { 107 + stack := []NodeIndex{walkStart[i]} 108 + ctx.SetMarks(walkStart[i], markStale) 109 + 110 + for len(stack) > 0 { 111 + top := stack[len(stack)-1] 112 + 113 + if ctx.HasAnyMarks(top, markResult) { 114 + ctx.ClearMarks(top, markResult) 115 + 116 + countStillIndependent-- 117 + if countStillIndependent <= 1 { 118 + break 119 + } 120 + 121 + if top == sorted[minGenPos] { 122 + for minGenPos < len(sorted)-1 && ctx.HasAnyMarks(sorted[minGenPos], markStale) { 123 + minGenPos++ 124 + } 125 + 126 + minGeneration = ctx.EffectiveGeneration(sorted[minGenPos]) 127 + } 128 + } 129 + 130 + if ctx.EffectiveGeneration(top) < minGeneration { 131 + stack = stack[:len(stack)-1] 132 + 133 + continue 134 + } 135 + 136 + pushed := false 137 + 138 + for _, parent := range ctx.Parents(top) { 139 + if ctx.HasAnyMarks(parent, markStale) { 140 + continue 141 + } 142 + 143 + ctx.SetMarks(parent, markStale) 144 + stack = append(stack, parent) 145 + pushed = true 146 + 147 + break 148 + } 149 + 150 + if !pushed { 151 + stack = stack[:len(stack)-1] 152 + } 153 + } 154 + } 155 + 156 + out := make([]NodeIndex, 0, len(candidates)) 157 + for _, idx := range candidates { 158 + if !ctx.HasAnyMarks(idx, markStale) { 159 + out = append(out, idx) 160 + } 161 + } 162 + 163 + ctx.ClearTouchedMarks(markStale | markResult) 164 + 165 + return out 166 + }
+50
internal/peel/peel.go
··· 1 + // Package peel peels Git object references through annotated tags. 2 + package peel 3 + 4 + import ( 5 + stderrors "errors" 6 + 7 + giterrors "codeberg.org/lindenii/furgit/errors" 8 + "codeberg.org/lindenii/furgit/object" 9 + "codeberg.org/lindenii/furgit/objectid" 10 + "codeberg.org/lindenii/furgit/objectstore" 11 + "codeberg.org/lindenii/furgit/objecttype" 12 + ) 13 + 14 + // ToCommit peels annotated tags transitively until a commit is reached. 15 + func ToCommit(store objectstore.Store, id objectid.ObjectID) (objectid.ObjectID, error) { 16 + for { 17 + ty, _, err := store.ReadHeader(id) 18 + if err != nil { 19 + if stderrors.Is(err, objectstore.ErrObjectNotFound) { 20 + return objectid.ObjectID{}, &giterrors.ObjectMissingError{OID: id} 21 + } 22 + 23 + return objectid.ObjectID{}, err 24 + } 25 + 26 + if ty != objecttype.TypeTag { 27 + if ty != objecttype.TypeCommit { 28 + return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} 29 + } 30 + 31 + return id, nil 32 + } 33 + 34 + _, content, err := store.ReadBytesContent(id) 35 + if err != nil { 36 + if stderrors.Is(err, objectstore.ErrObjectNotFound) { 37 + return objectid.ObjectID{}, &giterrors.ObjectMissingError{OID: id} 38 + } 39 + 40 + return objectid.ObjectID{}, err 41 + } 42 + 43 + tag, err := object.ParseTag(content, id.Algorithm()) 44 + if err != nil { 45 + return objectid.ObjectID{}, err 46 + } 47 + 48 + id = tag.Target 49 + } 50 + }
+51
internal/testgit/repo_commit_tree_env.go
··· 1 + package testgit 2 + 3 + import ( 4 + "slices" 5 + "strings" 6 + "testing" 7 + 8 + "codeberg.org/lindenii/furgit/objectid" 9 + ) 10 + 11 + // CommitTreeWithEnv creates one commit from a tree and message, optionally with 12 + // parents, using additional environment variables for the git subprocess. 13 + func (testRepo *TestRepo) CommitTreeWithEnv( 14 + tb testing.TB, 15 + extraEnv []string, 16 + tree objectid.ObjectID, 17 + message string, 18 + parents ...objectid.ObjectID, 19 + ) objectid.ObjectID { 20 + tb.Helper() 21 + 22 + args := make([]string, 0, 2+2*len(parents)+2) 23 + 24 + args = append(args, "commit-tree", tree.String()) 25 + for _, parent := range parents { 26 + args = append(args, "-p", parent.String()) 27 + } 28 + 29 + args = append(args, "-m", message) 30 + hex := testRepo.runWithExtraEnv(tb, extraEnv, args...) 31 + 32 + id, err := objectid.ParseHex(testRepo.algo, hex) 33 + if err != nil { 34 + tb.Fatalf("parse commit-tree output %q: %v", hex, err) 35 + } 36 + 37 + return id 38 + } 39 + 40 + func (testRepo *TestRepo) runWithExtraEnv(tb testing.TB, extraEnv []string, args ...string) string { 41 + tb.Helper() 42 + 43 + env := slices.Concat(testRepo.env, extraEnv) 44 + 45 + out, err := testRepo.runBytesWithEnv(tb, nil, testRepo.dir, env, args...) 46 + if err != nil { 47 + tb.Fatalf("git %v failed: %v\n%s", args, err, out) 48 + } 49 + 50 + return strings.TrimSpace(string(out)) 51 + }
+86
internal/testgit/repo_fs.go
··· 1 + package testgit 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + ) 8 + 9 + // OpenFile opens one file relative to the repository root. 10 + func (testRepo *TestRepo) OpenFile(tb testing.TB, name string) *os.File { 11 + tb.Helper() 12 + 13 + root := testRepo.OpenRoot(tb) 14 + 15 + file, err := root.Open(name) 16 + if err != nil { 17 + tb.Fatalf("Open(%q): %v", name, err) 18 + } 19 + 20 + return file 21 + } 22 + 23 + // ReadFile reads one file relative to the repository root. 24 + func (testRepo *TestRepo) ReadFile(tb testing.TB, name string) []byte { 25 + tb.Helper() 26 + 27 + root := testRepo.OpenRoot(tb) 28 + 29 + data, err := root.ReadFile(name) 30 + if err != nil { 31 + tb.Fatalf("ReadFile(%q): %v", name, err) 32 + } 33 + 34 + return data 35 + } 36 + 37 + // WriteFile writes one file relative to the repository root. 38 + func (testRepo *TestRepo) WriteFile(tb testing.TB, name string, data []byte, perm os.FileMode) { 39 + tb.Helper() 40 + 41 + root := testRepo.OpenRoot(tb) 42 + 43 + err := root.WriteFile(name, data, perm) 44 + if err != nil { 45 + tb.Fatalf("WriteFile(%q): %v", name, err) 46 + } 47 + } 48 + 49 + // WriteFileAll writes one file relative to the repository root, creating any 50 + // missing parent directories first. 51 + func (testRepo *TestRepo) WriteFileAll( 52 + tb testing.TB, 53 + name string, 54 + data []byte, 55 + dirPerm os.FileMode, 56 + filePerm os.FileMode, 57 + ) { 58 + tb.Helper() 59 + 60 + root := testRepo.OpenRoot(tb) 61 + 62 + dir := filepath.Dir(name) 63 + if dir != "." { 64 + err := root.MkdirAll(dir, dirPerm) 65 + if err != nil { 66 + tb.Fatalf("MkdirAll(%q): %v", dir, err) 67 + } 68 + } 69 + 70 + err := root.WriteFile(name, data, filePerm) 71 + if err != nil { 72 + tb.Fatalf("WriteFile(%q): %v", name, err) 73 + } 74 + } 75 + 76 + // Remove removes one path relative to the repository root. 77 + func (testRepo *TestRepo) Remove(tb testing.TB, name string) { 78 + tb.Helper() 79 + 80 + root := testRepo.OpenRoot(tb) 81 + 82 + err := root.Remove(name) 83 + if err != nil { 84 + tb.Fatalf("Remove(%q): %v", name, err) 85 + } 86 + }
+26
internal/testgit/repo_open_commit_graph.go
··· 1 + package testgit 2 + 3 + import ( 4 + "testing" 5 + 6 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 7 + ) 8 + 9 + // OpenCommitGraph opens the repository commit-graph and registers cleanup on 10 + // the caller. 11 + func (testRepo *TestRepo) OpenCommitGraph(tb testing.TB) *commitgraphread.Reader { 12 + tb.Helper() 13 + 14 + objectsRoot := testRepo.OpenObjectsRoot(tb) 15 + 16 + graph, err := commitgraphread.Open(objectsRoot, testRepo.Algorithm(), commitgraphread.OpenSingle) 17 + if err != nil { 18 + tb.Fatalf("commitgraphread.Open: %v", err) 19 + } 20 + 21 + tb.Cleanup(func() { 22 + _ = graph.Close() 23 + }) 24 + 25 + return graph 26 + }
+29
internal/testgit/repo_open_object_store.go
··· 1 + package testgit 2 + 3 + import ( 4 + "testing" 5 + 6 + "codeberg.org/lindenii/furgit/objectstore" 7 + "codeberg.org/lindenii/furgit/repository" 8 + ) 9 + 10 + // OpenObjectStore opens the repository object store and registers cleanup on 11 + // the caller. 12 + // 13 + //nolint:ireturn 14 + func (testRepo *TestRepo) OpenObjectStore(tb testing.TB) objectstore.Store { 15 + tb.Helper() 16 + 17 + root := testRepo.OpenGitRoot(tb) 18 + 19 + repo, err := repository.Open(root) 20 + if err != nil { 21 + tb.Fatalf("repository.Open: %v", err) 22 + } 23 + 24 + tb.Cleanup(func() { 25 + _ = repo.Close() 26 + }) 27 + 28 + return repo.Objects() 29 + }
+25
internal/testgit/repo_open_repository.go
··· 1 + package testgit 2 + 3 + import ( 4 + "testing" 5 + 6 + "codeberg.org/lindenii/furgit/repository" 7 + ) 8 + 9 + // OpenRepository opens the repository and registers cleanup on the caller. 10 + func (testRepo *TestRepo) OpenRepository(tb testing.TB) *repository.Repository { 11 + tb.Helper() 12 + 13 + root := testRepo.OpenGitRoot(tb) 14 + 15 + repo, err := repository.Open(root) 16 + if err != nil { 17 + tb.Fatalf("repository.Open: %v", err) 18 + } 19 + 20 + tb.Cleanup(func() { 21 + _ = repo.Close() 22 + }) 23 + 24 + return repo 25 + }
+87
internal/testgit/repo_open_root.go
··· 1 + package testgit 2 + 3 + import ( 4 + "errors" 5 + "os" 6 + "testing" 7 + ) 8 + 9 + // OpenRoot opens the repository root directory and registers cleanup on the 10 + // caller. 11 + func (testRepo *TestRepo) OpenRoot(tb testing.TB) *os.Root { 12 + tb.Helper() 13 + 14 + root, err := os.OpenRoot(testRepo.dir) 15 + if err != nil { 16 + tb.Fatalf("os.OpenRoot: %v", err) 17 + } 18 + 19 + tb.Cleanup(func() { 20 + _ = root.Close() 21 + }) 22 + 23 + return root 24 + } 25 + 26 + // OpenGitRoot opens the repository gitdir and registers cleanup on the caller. 27 + // 28 + // For bare repositories, this is the repository root itself. For non-bare 29 + // repositories, this is the .git directory under the worktree root. 30 + func (testRepo *TestRepo) OpenGitRoot(tb testing.TB) *os.Root { 31 + tb.Helper() 32 + 33 + repoRoot := testRepo.OpenRoot(tb) 34 + 35 + gitRoot, err := repoRoot.OpenRoot(".git") 36 + if err == nil { 37 + tb.Cleanup(func() { 38 + _ = gitRoot.Close() 39 + }) 40 + 41 + return gitRoot 42 + } 43 + 44 + if !errors.Is(err, os.ErrNotExist) { 45 + tb.Fatalf("OpenRoot(.git): %v", err) 46 + } 47 + 48 + return repoRoot 49 + } 50 + 51 + // OpenObjectsRoot opens the objects directory and registers cleanup on the 52 + // caller. 53 + func (testRepo *TestRepo) OpenObjectsRoot(tb testing.TB) *os.Root { 54 + tb.Helper() 55 + 56 + gitRoot := testRepo.OpenGitRoot(tb) 57 + 58 + objectsRoot, err := gitRoot.OpenRoot("objects") 59 + if err != nil { 60 + tb.Fatalf("OpenRoot(objects): %v", err) 61 + } 62 + 63 + tb.Cleanup(func() { 64 + _ = objectsRoot.Close() 65 + }) 66 + 67 + return objectsRoot 68 + } 69 + 70 + // OpenPackRoot opens the objects/pack directory and registers cleanup on the 71 + // caller. 72 + func (testRepo *TestRepo) OpenPackRoot(tb testing.TB) *os.Root { 73 + tb.Helper() 74 + 75 + objectsRoot := testRepo.OpenObjectsRoot(tb) 76 + 77 + packRoot, err := objectsRoot.OpenRoot("pack") 78 + if err != nil { 79 + tb.Fatalf("OpenRoot(pack): %v", err) 80 + } 81 + 82 + tb.Cleanup(func() { 83 + _ = packRoot.Close() 84 + }) 85 + 86 + return packRoot 87 + }
-5
internal/testgit/repo_properties.go
··· 2 2 3 3 import "codeberg.org/lindenii/furgit/objectid" 4 4 5 - // Dir returns the repository directory path. 6 - func (testRepo *TestRepo) Dir() string { 7 - return testRepo.dir 8 - } 9 - 10 5 // Algorithm returns the object ID algorithm configured for this repository. 11 6 func (testRepo *TestRepo) Algorithm() objectid.Algorithm { 12 7 return testRepo.algo
+22
internal/testgit/repo_remove_loose_object.go
··· 1 + package testgit 2 + 3 + import ( 4 + "fmt" 5 + "testing" 6 + 7 + "codeberg.org/lindenii/furgit/objectid" 8 + ) 9 + 10 + // RemoveLooseObject removes one loose object file from the repository. 11 + func (testRepo *TestRepo) RemoveLooseObject(tb testing.TB, id objectid.ObjectID) { 12 + tb.Helper() 13 + 14 + root := testRepo.OpenObjectsRoot(tb) 15 + hex := id.String() 16 + path := fmt.Sprintf("%s/%s", hex[:2], hex[2:]) 17 + 18 + err := root.Remove(path) 19 + if err != nil { 20 + tb.Fatalf("remove loose object %s: %v", id, err) 21 + } 22 + }
+45 -7
internal/testgit/repo_run.go
··· 22 22 return testRepo.runBytes(tb, nil, testRepo.dir, args...) 23 23 } 24 24 25 + // RunE executes git and returns trimmed textual output plus any command error. 26 + func (testRepo *TestRepo) RunE(tb testing.TB, args ...string) (string, error) { 27 + tb.Helper() 28 + 29 + out, err := testRepo.runBytesE(nil, testRepo.dir, args...) 30 + 31 + return strings.TrimSpace(string(out)), err 32 + } 33 + 25 34 // RunInput executes git with stdin and returns trimmed textual output. 26 35 func (testRepo *TestRepo) RunInput(tb testing.TB, stdin []byte, args ...string) string { 27 36 tb.Helper() ··· 39 48 40 49 func (testRepo *TestRepo) runBytes(tb testing.TB, stdin []byte, dir string, args ...string) []byte { 41 50 tb.Helper() 51 + 52 + out, err := testRepo.runBytesE(stdin, dir, args...) 53 + if err != nil { 54 + tb.Fatalf("git %v failed: %v\n%s", args, err, out) 55 + } 56 + 57 + return out 58 + } 59 + 60 + func (testRepo *TestRepo) runBytesE(stdin []byte, dir string, args ...string) ([]byte, error) { 61 + return testRepo.runBytesWithEnvNoHelper(stdin, dir, testRepo.env, args...) 62 + } 63 + 64 + // runBytesWithEnv executes git using the supplied environment. 65 + func (testRepo *TestRepo) runBytesWithEnv( 66 + tb testing.TB, 67 + stdin []byte, 68 + dir string, 69 + env []string, 70 + args ...string, 71 + ) ([]byte, error) { 72 + tb.Helper() 73 + 74 + return testRepo.runBytesWithEnvNoHelper(stdin, dir, env, args...) 75 + } 76 + 77 + // runBytesWithEnvNoHelper executes git using the supplied environment without 78 + // touching testing helper state. 79 + func (testRepo *TestRepo) runBytesWithEnvNoHelper( 80 + stdin []byte, 81 + dir string, 82 + env []string, 83 + args ...string, 84 + ) ([]byte, error) { 42 85 //nolint:noctx 43 86 cmd := exec.Command("git", args...) //#nosec G204 44 87 cmd.Dir = dir 45 88 46 - cmd.Env = testRepo.env 89 + cmd.Env = env 47 90 if stdin != nil { 48 91 cmd.Stdin = bytes.NewReader(stdin) 49 92 } 50 93 51 - out, err := cmd.CombinedOutput() 52 - if err != nil { 53 - tb.Fatalf("git %v failed: %v\n%s", args, err, out) 54 - } 55 - 56 - return out 94 + return cmd.CombinedOutput() 57 95 }
+43
mergebase/base.go
··· 1 + package mergebase 2 + 3 + import ( 4 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + "codeberg.org/lindenii/furgit/objectstore" 7 + ) 8 + 9 + // Base reports one merge base between left and right, if any. 10 + // 11 + // Both inputs are peeled through annotated tags before commit traversal. 12 + func Base( 13 + store objectstore.Store, 14 + graph *commitgraphread.Reader, 15 + left objectid.ObjectID, 16 + right objectid.ObjectID, 17 + ) (objectid.ObjectID, bool, error) { 18 + query := Query(store, graph, left, right) 19 + seq := query.Seq() 20 + 21 + var ( 22 + first objectid.ObjectID 23 + ok bool 24 + ) 25 + 26 + seq(func(id objectid.ObjectID) bool { 27 + first = id 28 + ok = true 29 + 30 + return false 31 + }) 32 + 33 + err := query.Err() 34 + if err != nil { 35 + return objectid.ObjectID{}, false, err 36 + } 37 + 38 + if !ok { 39 + return objectid.ObjectID{}, false, nil 40 + } 41 + 42 + return first, true, nil 43 + }
+56
mergebase/compute.go
··· 1 + package mergebase 2 + 3 + import ( 4 + "slices" 5 + 6 + "codeberg.org/lindenii/furgit/internal/commitquery" 7 + "codeberg.org/lindenii/furgit/internal/peel" 8 + "codeberg.org/lindenii/furgit/objectid" 9 + ) 10 + 11 + func (query *Bases) compute() ([]objectid.ObjectID, error) { 12 + leftCommit, err := peel.ToCommit(query.store, query.left) 13 + if err != nil { 14 + return nil, err 15 + } 16 + 17 + rightCommit, err := peel.ToCommit(query.store, query.right) 18 + if err != nil { 19 + return nil, err 20 + } 21 + 22 + ctx := commitquery.NewContext(query.store, query.graph) 23 + 24 + leftIdx, err := ctx.ResolveOID(leftCommit) 25 + if err != nil { 26 + return nil, err 27 + } 28 + 29 + rightIdx, err := ctx.ResolveOID(rightCommit) 30 + if err != nil { 31 + return nil, err 32 + } 33 + 34 + candidates, err := commitquery.MergeBases(ctx, leftIdx, rightIdx) 35 + if err != nil { 36 + return nil, err 37 + } 38 + 39 + slices.SortFunc(candidates, func(left, right commitquery.NodeIndex) int { 40 + switch { 41 + case ctx.CommitTime(left) > ctx.CommitTime(right): 42 + return -1 43 + case ctx.CommitTime(left) < ctx.CommitTime(right): 44 + return 1 45 + default: 46 + return objectid.Compare(ctx.ID(left), ctx.ID(right)) 47 + } 48 + }) 49 + 50 + out := make([]objectid.ObjectID, 0, len(candidates)) 51 + for _, idx := range candidates { 52 + out = append(out, ctx.ID(idx)) 53 + } 54 + 55 + return out, nil 56 + }
+308
mergebase/integration_test.go
··· 1 + package mergebase_test 2 + 3 + import ( 4 + "maps" 5 + "slices" 6 + "strings" 7 + "testing" 8 + 9 + "codeberg.org/lindenii/furgit/internal/testgit" 10 + "codeberg.org/lindenii/furgit/mergebase" 11 + "codeberg.org/lindenii/furgit/objectid" 12 + ) 13 + 14 + func TestQueryMatchesGitMergeBaseAll(t *testing.T) { 15 + t.Parallel() 16 + 17 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 18 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 19 + ObjectFormat: algo, 20 + Bare: true, 21 + RefFormat: "files", 22 + }) 23 + 24 + _, tree1 := testRepo.MakeSingleFileTree(t, "base.txt", []byte("base\n")) 25 + base := testRepo.CommitTree(t, tree1, "base") 26 + 27 + _, tree2 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) 28 + left := testRepo.CommitTree(t, tree2, "left", base) 29 + 30 + _, tree3 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) 31 + right := testRepo.CommitTree(t, tree3, "right", base) 32 + 33 + tag := testRepo.TagAnnotated(t, "right-tag", right, "right-tag") 34 + 35 + store := testRepo.OpenObjectStore(t) 36 + 37 + query := mergebase.Query(store, nil, left, tag) 38 + got := oidSetFromSeq(query.Seq()) 39 + 40 + err := query.Err() 41 + if err != nil { 42 + t.Fatalf("query.Err(): %v", err) 43 + } 44 + 45 + want := gitMergeBaseAllSet(t, testRepo, left, tag) 46 + if !maps.Equal(got, want) { 47 + t.Fatalf("Query(left, tag) mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) 48 + } 49 + }) 50 + } 51 + 52 + func TestQueryCrissCrossMatchesGitMergeBaseAll(t *testing.T) { 53 + t.Parallel() 54 + 55 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 56 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 57 + ObjectFormat: algo, 58 + Bare: true, 59 + RefFormat: "files", 60 + }) 61 + 62 + _, tree1 := testRepo.MakeSingleFileTree(t, "root.txt", []byte("root\n")) 63 + root := testRepo.CommitTree(t, tree1, "root") 64 + 65 + _, tree2 := testRepo.MakeSingleFileTree(t, "base1.txt", []byte("base1\n")) 66 + base1 := testRepo.CommitTree(t, tree2, "base1", root) 67 + 68 + _, tree3 := testRepo.MakeSingleFileTree(t, "base2.txt", []byte("base2\n")) 69 + base2 := testRepo.CommitTree(t, tree3, "base2", root) 70 + 71 + _, tree4 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) 72 + left := testRepo.CommitTree(t, tree4, "left", base1, base2) 73 + 74 + _, tree5 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) 75 + right := testRepo.CommitTree(t, tree5, "right", base2, base1) 76 + 77 + store := testRepo.OpenObjectStore(t) 78 + 79 + query := mergebase.Query(store, nil, left, right) 80 + got := oidSetFromSeq(query.Seq()) 81 + 82 + err := query.Err() 83 + if err != nil { 84 + t.Fatalf("query.Err(): %v", err) 85 + } 86 + 87 + want := gitMergeBaseAllSet(t, testRepo, left, right) 88 + if !maps.Equal(got, want) { 89 + t.Fatalf("Query(left, right) mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) 90 + } 91 + 92 + first, ok, err := mergebase.Base(store, nil, left, right) 93 + if err != nil { 94 + t.Fatalf("Base(left, right): %v", err) 95 + } 96 + 97 + if !ok { 98 + t.Fatal("Base(left, right) unexpectedly reported no base") 99 + } 100 + 101 + if !containsID(want, first) { 102 + t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) 103 + } 104 + }) 105 + } 106 + 107 + func TestQueryMatchesGitMergeBaseAllWithCommitGraph(t *testing.T) { 108 + t.Parallel() 109 + 110 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 111 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 112 + ObjectFormat: algo, 113 + Bare: true, 114 + RefFormat: "files", 115 + }) 116 + 117 + _, tree1 := testRepo.MakeSingleFileTree(t, "root.txt", []byte("root\n")) 118 + root := testRepo.CommitTree(t, tree1, "root") 119 + 120 + _, tree2 := testRepo.MakeSingleFileTree(t, "base1.txt", []byte("base1\n")) 121 + base1 := testRepo.CommitTree(t, tree2, "base1", root) 122 + 123 + _, tree3 := testRepo.MakeSingleFileTree(t, "base2.txt", []byte("base2\n")) 124 + base2 := testRepo.CommitTree(t, tree3, "base2", root) 125 + 126 + _, tree4 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) 127 + left := testRepo.CommitTree(t, tree4, "left", base1, base2) 128 + 129 + _, tree5 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) 130 + right := testRepo.CommitTree(t, tree5, "right", base2, base1) 131 + 132 + testRepo.UpdateRef(t, "refs/heads/main", right) 133 + testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") 134 + testRepo.CommitGraphWrite(t, "--reachable") 135 + 136 + store := testRepo.OpenObjectStore(t) 137 + graph := testRepo.OpenCommitGraph(t) 138 + 139 + query := mergebase.Query(store, graph, left, right) 140 + got := oidSetFromSeq(query.Seq()) 141 + 142 + err := query.Err() 143 + if err != nil { 144 + t.Fatalf("query.Err(): %v", err) 145 + } 146 + 147 + want := gitMergeBaseAllSet(t, testRepo, left, right) 148 + if !maps.Equal(got, want) { 149 + t.Fatalf("Query(left, right) with commit-graph mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want)) 150 + } 151 + 152 + first, ok, err := mergebase.Base(store, graph, left, right) 153 + if err != nil { 154 + t.Fatalf("Base(left, right): %v", err) 155 + } 156 + 157 + if !ok { 158 + t.Fatal("Base(left, right) unexpectedly reported no base") 159 + } 160 + 161 + if !containsID(want, first) { 162 + t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) 163 + } 164 + }) 165 + } 166 + 167 + func TestBaseMatchesGitMergeBaseWithoutAll(t *testing.T) { 168 + t.Parallel() 169 + 170 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 171 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 172 + ObjectFormat: algo, 173 + Bare: true, 174 + RefFormat: "files", 175 + }) 176 + 177 + _, tree1 := testRepo.MakeSingleFileTree(t, "root.txt", []byte("root\n")) 178 + root := testRepo.CommitTree(t, tree1, "root") 179 + 180 + _, tree2 := testRepo.MakeSingleFileTree(t, "base1.txt", []byte("base1\n")) 181 + base1 := testRepo.CommitTreeWithEnv(t, []string{ 182 + "GIT_AUTHOR_DATE=1234567890 +0000", 183 + "GIT_COMMITTER_DATE=1234567890 +0000", 184 + }, tree2, "base1", root) 185 + 186 + _, tree3 := testRepo.MakeSingleFileTree(t, "base2.txt", []byte("base2\n")) 187 + base2 := testRepo.CommitTreeWithEnv(t, []string{ 188 + "GIT_AUTHOR_DATE=1234567990 +0000", 189 + "GIT_COMMITTER_DATE=1234567990 +0000", 190 + }, tree3, "base2", root) 191 + 192 + _, tree4 := testRepo.MakeSingleFileTree(t, "left.txt", []byte("left\n")) 193 + left := testRepo.CommitTree(t, tree4, "left", base1, base2) 194 + 195 + _, tree5 := testRepo.MakeSingleFileTree(t, "right.txt", []byte("right\n")) 196 + right := testRepo.CommitTree(t, tree5, "right", base2, base1) 197 + 198 + store := testRepo.OpenObjectStore(t) 199 + 200 + got, ok, err := mergebase.Base(store, nil, left, right) 201 + if err != nil { 202 + t.Fatalf("Base(left, right): %v", err) 203 + } 204 + 205 + if !ok { 206 + t.Fatal("Base(left, right) unexpectedly reported no base") 207 + } 208 + 209 + want := gitMergeBaseOne(t, testRepo, left, right) 210 + if got != want { 211 + t.Fatalf("Base(left, right)=%s, want %s", got, want) 212 + } 213 + 214 + testRepo.UpdateRef(t, "refs/heads/main", right) 215 + testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") 216 + testRepo.CommitGraphWrite(t, "--reachable") 217 + 218 + graph := testRepo.OpenCommitGraph(t) 219 + 220 + got, ok, err = mergebase.Base(store, graph, left, right) 221 + if err != nil { 222 + t.Fatalf("Base(left, right) with commit-graph: %v", err) 223 + } 224 + 225 + if !ok { 226 + t.Fatal("Base(left, right) with commit-graph unexpectedly reported no base") 227 + } 228 + 229 + if got != want { 230 + t.Fatalf("Base(left, right) with commit-graph=%s, want %s", got, want) 231 + } 232 + }) 233 + } 234 + 235 + // oidSetFromSeq collects one object ID sequence into a set. 236 + func oidSetFromSeq(seq func(func(objectid.ObjectID) bool)) map[objectid.ObjectID]struct{} { 237 + out := make(map[objectid.ObjectID]struct{}) 238 + 239 + seq(func(id objectid.ObjectID) bool { 240 + out[id] = struct{}{} 241 + 242 + return true 243 + }) 244 + 245 + return out 246 + } 247 + 248 + // gitMergeBaseAllSet returns Git's merge-base --all output as a set. 249 + func gitMergeBaseAllSet( 250 + t *testing.T, 251 + testRepo *testgit.TestRepo, 252 + left objectid.ObjectID, 253 + right objectid.ObjectID, 254 + ) map[objectid.ObjectID]struct{} { 255 + t.Helper() 256 + 257 + out := testRepo.Run(t, "merge-base", "--all", left.String(), right.String()) 258 + set := make(map[objectid.ObjectID]struct{}) 259 + 260 + for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { 261 + line = strings.TrimSpace(line) 262 + if line == "" { 263 + continue 264 + } 265 + 266 + id, err := objectid.ParseHex(testRepo.Algorithm(), line) 267 + if err != nil { 268 + t.Fatalf("parse merge-base oid %q: %v", line, err) 269 + } 270 + 271 + set[id] = struct{}{} 272 + } 273 + 274 + return set 275 + } 276 + 277 + // gitMergeBaseOne returns Git's merge-base output without --all. 278 + func gitMergeBaseOne( 279 + t *testing.T, 280 + testRepo *testgit.TestRepo, 281 + left objectid.ObjectID, 282 + right objectid.ObjectID, 283 + ) objectid.ObjectID { 284 + t.Helper() 285 + 286 + out := strings.TrimSpace(testRepo.Run(t, "merge-base", left.String(), right.String())) 287 + if out == "" { 288 + t.Fatal("git merge-base returned no output") 289 + } 290 + 291 + id, err := objectid.ParseHex(testRepo.Algorithm(), out) 292 + if err != nil { 293 + t.Fatalf("parse merge-base oid %q: %v", out, err) 294 + } 295 + 296 + return id 297 + } 298 + 299 + func sortedOIDStrings(set map[objectid.ObjectID]struct{}) []string { 300 + out := make([]string, 0, len(set)) 301 + for id := range set { 302 + out = append(out, id.String()) 303 + } 304 + 305 + slices.Sort(out) 306 + 307 + return out 308 + }
+19
mergebase/mergebase.go
··· 1 + // Package mergebase computes best common ancestors between commits. 2 + package mergebase 3 + 4 + import ( 5 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 6 + "codeberg.org/lindenii/furgit/objectid" 7 + "codeberg.org/lindenii/furgit/objectstore" 8 + ) 9 + 10 + // Bases is one iterator merge-base query. 11 + type Bases struct { 12 + store objectstore.Store 13 + graph *commitgraphread.Reader 14 + left objectid.ObjectID 15 + right objectid.ObjectID 16 + 17 + seqUsed bool 18 + err error 19 + }
+24
mergebase/query.go
··· 1 + package mergebase 2 + 3 + import ( 4 + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + "codeberg.org/lindenii/furgit/objectstore" 7 + ) 8 + 9 + // Query builds one single-use merge-base query over two commit roots. 10 + // 11 + // Both inputs are peeled through annotated tags before commit traversal. 12 + func Query( 13 + store objectstore.Store, 14 + graph *commitgraphread.Reader, 15 + left objectid.ObjectID, 16 + right objectid.ObjectID, 17 + ) *Bases { 18 + return &Bases{ 19 + store: store, 20 + graph: graph, 21 + left: left, 22 + right: right, 23 + } 24 + }
+47
mergebase/seq.go
··· 1 + package mergebase 2 + 3 + import ( 4 + "errors" 5 + "iter" 6 + 7 + "codeberg.org/lindenii/furgit/objectid" 8 + ) 9 + 10 + // Seq returns the merge-base sequence. It is single-use. 11 + func (query *Bases) Seq() iter.Seq[objectid.ObjectID] { 12 + if query.seqUsed { 13 + return func(yield func(objectid.ObjectID) bool) { 14 + _ = yield 15 + 16 + if query.err == nil { 17 + query.err = errors.New("mergebase: sequence already consumed") 18 + } 19 + } 20 + } 21 + 22 + query.seqUsed = true 23 + 24 + return func(yield func(objectid.ObjectID) bool) { 25 + if query.err != nil { 26 + return 27 + } 28 + 29 + bases, err := query.compute() 30 + if err != nil { 31 + query.err = err 32 + 33 + return 34 + } 35 + 36 + for _, id := range bases { 37 + if !yield(id) { 38 + return 39 + } 40 + } 41 + } 42 + } 43 + 44 + // Err returns the terminal error, if any, once Seq has been consumed. 45 + func (query *Bases) Err() error { 46 + return query.err 47 + }
+335
mergebase/unit_test.go
··· 1 + package mergebase_test 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "maps" 7 + "slices" 8 + "testing" 9 + 10 + giterrors "codeberg.org/lindenii/furgit/errors" 11 + "codeberg.org/lindenii/furgit/internal/testgit" 12 + "codeberg.org/lindenii/furgit/mergebase" 13 + "codeberg.org/lindenii/furgit/object" 14 + "codeberg.org/lindenii/furgit/objectid" 15 + "codeberg.org/lindenii/furgit/objectstore/memory" 16 + "codeberg.org/lindenii/furgit/objecttype" 17 + ) 18 + 19 + // commitBody serializes one minimal commit body. 20 + func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { 21 + buf := fmt.Appendf(nil, "tree %s\n", tree.String()) 22 + for _, parent := range parents { 23 + buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) 24 + } 25 + 26 + buf = append(buf, []byte("\nmsg\n")...) 27 + 28 + return buf 29 + } 30 + 31 + // tagBody serializes one minimal annotated tag body. 32 + func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { 33 + targetName, ok := objecttype.Name(targetType) 34 + if !ok { 35 + panic("invalid tag target type") 36 + } 37 + 38 + return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) 39 + } 40 + 41 + // collectSeq collects one object ID sequence into a slice. 42 + func collectSeq(seq func(func(objectid.ObjectID) bool)) []objectid.ObjectID { 43 + var out []objectid.ObjectID 44 + 45 + seq(func(id objectid.ObjectID) bool { 46 + out = append(out, id) 47 + 48 + return true 49 + }) 50 + 51 + return out 52 + } 53 + 54 + // toSet converts one slice of object IDs into a set. 55 + func toSet(ids []objectid.ObjectID) map[objectid.ObjectID]struct{} { 56 + set := make(map[objectid.ObjectID]struct{}, len(ids)) 57 + for _, id := range ids { 58 + set[id] = struct{}{} 59 + } 60 + 61 + return set 62 + } 63 + 64 + // containsID reports whether one set contains one object ID. 65 + func containsID(set map[objectid.ObjectID]struct{}, id objectid.ObjectID) bool { 66 + _, ok := set[id] 67 + 68 + return ok 69 + } 70 + 71 + // mustSerializeTree serializes one tree or fails the test. 72 + func mustSerializeTree(tb testing.TB, tree *object.Tree) []byte { 73 + tb.Helper() 74 + 75 + body, err := tree.SerializeWithoutHeader() 76 + if err != nil { 77 + tb.Fatalf("SerializeWithoutHeader: %v", err) 78 + } 79 + 80 + return body 81 + } 82 + 83 + // TestQueryLinearHistory reports one linear-history merge base. 84 + func TestQueryLinearHistory(t *testing.T) { 85 + t.Parallel() 86 + 87 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 88 + store := memory.New(algo) 89 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 90 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 91 + Mode: object.FileModeRegular, 92 + Name: []byte("f"), 93 + ID: blob, 94 + }}})) 95 + base := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 96 + left := store.AddObject(objecttype.TypeCommit, commitBody(tree, base)) 97 + right := store.AddObject(objecttype.TypeCommit, commitBody(tree, left)) 98 + 99 + query := mergebase.Query(store, nil, left, right) 100 + got := collectSeq(query.Seq()) 101 + 102 + err := query.Err() 103 + if err != nil { 104 + t.Fatalf("query.Err(): %v", err) 105 + } 106 + 107 + if !slices.Equal(got, []objectid.ObjectID{left}) { 108 + t.Fatalf("Query(left, right)=%v, want [%s]", got, left) 109 + } 110 + 111 + first, ok, err := mergebase.Base(store, nil, left, right) 112 + if err != nil { 113 + t.Fatalf("Base(left, right): %v", err) 114 + } 115 + 116 + if !ok { 117 + t.Fatal("Base(left, right) unexpectedly reported no base") 118 + } 119 + 120 + if first != left { 121 + t.Fatalf("Base(left, right)=%s, want %s", first, left) 122 + } 123 + }) 124 + } 125 + 126 + func TestQueryPeelsAnnotatedTags(t *testing.T) { 127 + t.Parallel() 128 + 129 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 130 + store := memory.New(algo) 131 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 132 + leftTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 133 + Mode: object.FileModeRegular, 134 + Name: []byte("left"), 135 + ID: blob, 136 + }}})) 137 + rightTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 138 + Mode: object.FileModeRegular, 139 + Name: []byte("right"), 140 + ID: blob, 141 + }}})) 142 + base := store.AddObject(objecttype.TypeCommit, commitBody(leftTree)) 143 + left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree, base)) 144 + right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree, base)) 145 + tag := store.AddObject(objecttype.TypeTag, tagBody(right, objecttype.TypeCommit)) 146 + 147 + query := mergebase.Query(store, nil, left, tag) 148 + got := collectSeq(query.Seq()) 149 + 150 + err := query.Err() 151 + if err != nil { 152 + t.Fatalf("query.Err(): %v", err) 153 + } 154 + 155 + if !slices.Equal(got, []objectid.ObjectID{base}) { 156 + t.Fatalf("Query(left, tag)=%v, want [%s]", got, base) 157 + } 158 + }) 159 + } 160 + 161 + func TestQueryCrissCrossReturnsAllBestCommonAncestors(t *testing.T) { 162 + t.Parallel() 163 + 164 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 165 + store := memory.New(algo) 166 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 167 + rootTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 168 + Mode: object.FileModeRegular, 169 + Name: []byte("root"), 170 + ID: blob, 171 + }}})) 172 + base1Tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 173 + Mode: object.FileModeRegular, 174 + Name: []byte("base1"), 175 + ID: blob, 176 + }}})) 177 + base2Tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 178 + Mode: object.FileModeRegular, 179 + Name: []byte("base2"), 180 + ID: blob, 181 + }}})) 182 + leftTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 183 + Mode: object.FileModeRegular, 184 + Name: []byte("left"), 185 + ID: blob, 186 + }}})) 187 + rightTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 188 + Mode: object.FileModeRegular, 189 + Name: []byte("right"), 190 + ID: blob, 191 + }}})) 192 + root := store.AddObject(objecttype.TypeCommit, commitBody(rootTree)) 193 + base1 := store.AddObject(objecttype.TypeCommit, commitBody(base1Tree, root)) 194 + base2 := store.AddObject(objecttype.TypeCommit, commitBody(base2Tree, root)) 195 + left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree, base1, base2)) 196 + right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree, base2, base1)) 197 + 198 + query := mergebase.Query(store, nil, left, right) 199 + got := toSet(collectSeq(query.Seq())) 200 + 201 + err := query.Err() 202 + if err != nil { 203 + t.Fatalf("query.Err(): %v", err) 204 + } 205 + 206 + want := map[objectid.ObjectID]struct{}{base1: {}, base2: {}} 207 + if !maps.Equal(got, want) { 208 + t.Fatalf("Query(left, right)=%v, want %v", slices.Collect(maps.Keys(got)), slices.Collect(maps.Keys(want))) 209 + } 210 + 211 + first, ok, err := mergebase.Base(store, nil, left, right) 212 + if err != nil { 213 + t.Fatalf("Base(left, right): %v", err) 214 + } 215 + 216 + if !ok { 217 + t.Fatal("Base(left, right) unexpectedly reported no base") 218 + } 219 + 220 + if !containsID(want, first) { 221 + t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) 222 + } 223 + }) 224 + } 225 + 226 + func TestQueryReturnsNoResultWhenNoCommonAncestorExists(t *testing.T) { 227 + t.Parallel() 228 + 229 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 230 + store := memory.New(algo) 231 + leftBlob := store.AddObject(objecttype.TypeBlob, []byte("left\n")) 232 + leftTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 233 + Mode: object.FileModeRegular, 234 + Name: []byte("left"), 235 + ID: leftBlob, 236 + }}})) 237 + rightBlob := store.AddObject(objecttype.TypeBlob, []byte("right\n")) 238 + rightTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 239 + Mode: object.FileModeRegular, 240 + Name: []byte("right"), 241 + ID: rightBlob, 242 + }}})) 243 + left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree)) 244 + right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree)) 245 + 246 + query := mergebase.Query(store, nil, left, right) 247 + got := collectSeq(query.Seq()) 248 + 249 + err := query.Err() 250 + if err != nil { 251 + t.Fatalf("query.Err(): %v", err) 252 + } 253 + 254 + if len(got) != 0 { 255 + t.Fatalf("Query(left, right)=%v, want no results", got) 256 + } 257 + 258 + _, ok, err := mergebase.Base(store, nil, left, right) 259 + if err != nil { 260 + t.Fatalf("Base(left, right): %v", err) 261 + } 262 + 263 + if ok { 264 + t.Fatal("Base(left, right) unexpectedly reported a base") 265 + } 266 + }) 267 + } 268 + 269 + func TestQueryRejectsNonCommitAfterPeel(t *testing.T) { 270 + t.Parallel() 271 + 272 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 273 + store := memory.New(algo) 274 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 275 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 276 + Mode: object.FileModeRegular, 277 + Name: []byte("f"), 278 + ID: blob, 279 + }}})) 280 + commit := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 281 + tagToTree := store.AddObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) 282 + 283 + query := mergebase.Query(store, nil, commit, tagToTree) 284 + _ = collectSeq(query.Seq()) 285 + 286 + err := query.Err() 287 + if err == nil { 288 + t.Fatal("expected error") 289 + } 290 + 291 + var typeErr *giterrors.ObjectTypeError 292 + if !errors.As(err, &typeErr) { 293 + t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) 294 + } 295 + 296 + if typeErr.Got != objecttype.TypeTree || typeErr.Want != objecttype.TypeCommit { 297 + t.Fatalf("unexpected type error: %+v", typeErr) 298 + } 299 + }) 300 + } 301 + 302 + func TestQuerySeqSingleUse(t *testing.T) { 303 + t.Parallel() 304 + 305 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 306 + store := memory.New(algo) 307 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 308 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 309 + Mode: object.FileModeRegular, 310 + Name: []byte("f"), 311 + ID: blob, 312 + }}})) 313 + base := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 314 + left := store.AddObject(objecttype.TypeCommit, commitBody(tree, base)) 315 + right := store.AddObject(objecttype.TypeCommit, commitBody(tree, left)) 316 + 317 + query := mergebase.Query(store, nil, left, right) 318 + 319 + _ = collectSeq(query.Seq()) 320 + again := collectSeq(query.Seq()) 321 + 322 + if len(again) != 0 { 323 + t.Fatalf("second Seq() unexpectedly yielded %v", again) 324 + } 325 + 326 + err := query.Err() 327 + if err == nil { 328 + t.Fatal("expected error after second Seq()") 329 + } 330 + 331 + if err.Error() != "mergebase: sequence already consumed" { 332 + t.Fatalf("unexpected error: %v", err) 333 + } 334 + }) 335 + }
+7
objectid/objectid.go
··· 4 4 import ( 5 5 //#nosec G505 6 6 7 + "bytes" 7 8 "encoding/hex" 8 9 "fmt" 9 10 ) ··· 50 51 size := id.Size() 51 52 52 53 return id.data[:size:size] 54 + } 55 + 56 + // Compare lexicographically compares two object IDs by their canonical byte 57 + // representation. 58 + func Compare(left, right ObjectID) int { 59 + return bytes.Compare(left.RawBytes(), right.RawBytes()) 53 60 } 54 61 55 62 // ParseHex parses an object ID from hex for the specified algorithm.
+2 -11
objectstore/loose/helpers_test.go
··· 2 2 3 3 import ( 4 4 "io" 5 - "os" 6 - "path/filepath" 7 5 "testing" 8 6 9 7 "codeberg.org/lindenii/furgit/internal/testgit" ··· 13 11 "codeberg.org/lindenii/furgit/objecttype" 14 12 ) 15 13 16 - func openLooseStore(t *testing.T, repoPath string, algo objectid.Algorithm) *loose.Store { 14 + func openLooseStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { 17 15 t.Helper() 18 16 19 - objectsPath := filepath.Join(repoPath, "objects") 20 - 21 - root, err := os.OpenRoot(objectsPath) 22 - if err != nil { 23 - t.Fatalf("OpenRoot(%q): %v", objectsPath, err) 24 - } 25 - 26 - t.Cleanup(func() { _ = root.Close() }) 17 + root := testRepo.OpenObjectsRoot(t) 27 18 28 19 store, err := loose.New(root, algo) 29 20 if err != nil {
+2 -2
objectstore/loose/read_test.go
··· 21 21 _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody") 22 22 tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message") 23 23 24 - store := openLooseStore(t, testRepo.Dir(), algo) 24 + store := openLooseStore(t, testRepo, algo) 25 25 26 26 tests := []struct { 27 27 name string ··· 108 108 t.Parallel() 109 109 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 110 110 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 111 - store := openLooseStore(t, testRepo.Dir(), algo) 111 + store := openLooseStore(t, testRepo, algo) 112 112 113 113 notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) 114 114 if err != nil {
+6 -6
objectstore/loose/write_test.go
··· 14 14 t.Parallel() 15 15 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 16 16 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 17 - store := openLooseStore(t, testRepo.Dir(), algo) 17 + store := openLooseStore(t, testRepo, algo) 18 18 19 19 content := []byte("written-by-content-reader\n") 20 20 expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin") ··· 54 54 t.Parallel() 55 55 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 56 56 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 57 - store := openLooseStore(t, testRepo.Dir(), algo) 57 + store := openLooseStore(t, testRepo, algo) 58 58 59 59 body := []byte("full-reader-body\n") 60 60 ··· 91 91 t.Run("content overflow", func(t *testing.T) { 92 92 t.Parallel() 93 93 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 94 - store := openLooseStore(t, testRepo.Dir(), algo) 94 + store := openLooseStore(t, testRepo, algo) 95 95 96 96 _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello"))) 97 97 if err == nil { ··· 102 102 t.Run("content short", func(t *testing.T) { 103 103 t.Parallel() 104 104 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 105 - store := openLooseStore(t, testRepo.Dir(), algo) 105 + store := openLooseStore(t, testRepo, algo) 106 106 107 107 _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x"))) 108 108 if err == nil { ··· 113 113 t.Run("full malformed header", func(t *testing.T) { 114 114 t.Parallel() 115 115 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 116 - store := openLooseStore(t, testRepo.Dir(), algo) 116 + store := openLooseStore(t, testRepo, algo) 117 117 118 118 _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header"))) 119 119 if err == nil { ··· 124 124 t.Run("full size mismatch", func(t *testing.T) { 125 125 t.Parallel() 126 126 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 127 - store := openLooseStore(t, testRepo.Dir(), algo) 127 + store := openLooseStore(t, testRepo, algo) 128 128 129 129 raw := []byte("blob 1\x00hello") 130 130
+21
objectstore/memory/add.go
··· 1 + package memory 2 + 3 + import ( 4 + "codeberg.org/lindenii/furgit/objectheader" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + "codeberg.org/lindenii/furgit/objecttype" 7 + ) 8 + 9 + // AddObject stores one object body and returns its object ID. 10 + func (store *Store) AddObject(ty objecttype.Type, body []byte) objectid.ObjectID { 11 + header, ok := objectheader.Encode(ty, int64(len(body))) 12 + if !ok { 13 + panic("failed to encode object header") 14 + } 15 + 16 + raw := append(append([]byte(nil), header...), body...) 17 + id := store.algo.Sum(raw) 18 + store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), body...)} 19 + 20 + return id 21 + }
+8
objectstore/memory/algorithm.go
··· 1 + package memory 2 + 3 + import "codeberg.org/lindenii/furgit/objectid" 4 + 5 + // Algorithm returns the object ID algorithm used by the store. 6 + func (store *Store) Algorithm() objectid.Algorithm { 7 + return store.algo 8 + }
+2
objectstore/memory/doc.go
··· 1 + // Package memory provides one in-memory object store. 2 + package memory
+9
objectstore/memory/object.go
··· 1 + package memory 2 + 3 + import "codeberg.org/lindenii/furgit/objecttype" 4 + 5 + // storedObject is one in-memory object entry. 6 + type storedObject struct { 7 + ty objecttype.Type 8 + content []byte 9 + }
+37
objectstore/memory/read_bytes.go
··· 1 + package memory 2 + 3 + import ( 4 + "codeberg.org/lindenii/furgit/objectheader" 5 + "codeberg.org/lindenii/furgit/objectid" 6 + "codeberg.org/lindenii/furgit/objectstore" 7 + "codeberg.org/lindenii/furgit/objecttype" 8 + ) 9 + 10 + // ReadBytesFull reads one full object, including the object header. 11 + func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 12 + obj, ok := store.objects[id] 13 + if !ok { 14 + return nil, objectstore.ErrObjectNotFound 15 + } 16 + 17 + header, ok := objectheader.Encode(obj.ty, int64(len(obj.content))) 18 + if !ok { 19 + panic("failed to encode object header") 20 + } 21 + 22 + raw := make([]byte, len(header)+len(obj.content)) 23 + copy(raw, header) 24 + copy(raw[len(header):], obj.content) 25 + 26 + return raw, nil 27 + } 28 + 29 + // ReadBytesContent reads one object body. 30 + func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 31 + obj, ok := store.objects[id] 32 + if !ok { 33 + return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound 34 + } 35 + 36 + return obj.ty, append([]byte(nil), obj.content...), nil 37 + }
+17
objectstore/memory/read_header.go
··· 1 + package memory 2 + 3 + import ( 4 + "codeberg.org/lindenii/furgit/objectid" 5 + "codeberg.org/lindenii/furgit/objectstore" 6 + "codeberg.org/lindenii/furgit/objecttype" 7 + ) 8 + 9 + // ReadHeader reads one object header. 10 + func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 11 + obj, ok := store.objects[id] 12 + if !ok { 13 + return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound 14 + } 15 + 16 + return obj.ty, int64(len(obj.content)), nil 17 + }
+29
objectstore/memory/read_reader.go
··· 1 + package memory 2 + 3 + import ( 4 + "bytes" 5 + "io" 6 + 7 + "codeberg.org/lindenii/furgit/objectid" 8 + "codeberg.org/lindenii/furgit/objecttype" 9 + ) 10 + 11 + // ReadReaderFull reads one full object through a reader. 12 + func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 13 + raw, err := store.ReadBytesFull(id) 14 + if err != nil { 15 + return nil, err 16 + } 17 + 18 + return io.NopCloser(bytes.NewReader(raw)), nil 19 + } 20 + 21 + // ReadReaderContent reads one object body through a reader. 22 + func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 23 + ty, content, err := store.ReadBytesContent(id) 24 + if err != nil { 25 + return objecttype.TypeInvalid, 0, nil, err 26 + } 27 + 28 + return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil 29 + }
+13
objectstore/memory/read_size.go
··· 1 + package memory 2 + 3 + import "codeberg.org/lindenii/furgit/objectid" 4 + 5 + // ReadSize reads one object size. 6 + func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) { 7 + _, size, err := store.ReadHeader(id) 8 + if err != nil { 9 + return 0, err 10 + } 11 + 12 + return size, nil 13 + }
+24
objectstore/memory/store.go
··· 1 + package memory 2 + 3 + import ( 4 + "codeberg.org/lindenii/furgit/objectid" 5 + ) 6 + 7 + // Store is one in-memory object store. 8 + type Store struct { 9 + algo objectid.Algorithm 10 + objects map[objectid.ObjectID]storedObject 11 + } 12 + 13 + // New builds one empty in-memory store for one object format. 14 + func New(algo objectid.Algorithm) *Store { 15 + return &Store{ 16 + algo: algo, 17 + objects: make(map[objectid.ObjectID]storedObject), 18 + } 19 + } 20 + 21 + // Close closes the in-memory store. 22 + func (store *Store) Close() error { 23 + return nil 24 + }
+1
objectstore/objectstore.go
··· 11 11 12 12 // ErrObjectNotFound indicates that an object does not exist in a backend. 13 13 // TODO: This might need to be an interface or otherwise be able to encapsulate multiple concrete backends'. 14 + // XXX: Don't remove this in favor of errors.ObjectMissingError yet due to pressure of allocation large error structs. 14 15 var ErrObjectNotFound = errors.New("objectstore: object not found") 15 16 16 17 // Store reads Git objects by object ID.
+2 -11
objectstore/packed/helpers_test.go
··· 3 3 import ( 4 4 "fmt" 5 5 "io" 6 - "os" 7 - "path/filepath" 8 6 "strconv" 9 7 "strings" 10 8 "testing" ··· 16 14 "codeberg.org/lindenii/furgit/objecttype" 17 15 ) 18 16 19 - func openPackedStore(t *testing.T, repoPath string, algo objectid.Algorithm) *packed.Store { 17 + func openPackedStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *packed.Store { 20 18 t.Helper() 21 19 22 - packPath := filepath.Join(repoPath, "objects", "pack") 23 - 24 - root, err := os.OpenRoot(packPath) 25 - if err != nil { 26 - t.Fatalf("OpenRoot(%q): %v", packPath, err) 27 - } 28 - 29 - t.Cleanup(func() { _ = root.Close() }) 20 + root := testRepo.OpenPackRoot(t) 30 21 31 22 store, err := packed.New(root, algo) 32 23 if err != nil {
+23 -17
objectstore/packed/read_test.go
··· 4 4 "bytes" 5 5 "errors" 6 6 "fmt" 7 - "os" 8 - "path/filepath" 7 + "io/fs" 9 8 "strconv" 10 9 "strings" 11 10 "testing" ··· 20 19 t.Parallel() 21 20 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 22 21 testRepo, ids := createPackedFixtureRepo(t, algo) 23 - store := openPackedStore(t, testRepo.Dir(), algo) 22 + store := openPackedStore(t, testRepo, algo) 24 23 25 24 for _, id := range ids { 26 25 t.Run(id.String(), func(t *testing.T) { ··· 106 105 t.Parallel() 107 106 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 108 107 testRepo, _ := createPackedFixtureRepo(t, algo) 109 - store := openPackedStore(t, testRepo.Dir(), algo) 108 + store := openPackedStore(t, testRepo, algo) 110 109 111 110 notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen())) 112 111 if err != nil { ··· 172 171 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 173 172 testRepo, _ := createPackedFixtureRepo(t, algo) 174 173 175 - store := openPackedStore(t, testRepo.Dir(), algo) 174 + store := openPackedStore(t, testRepo, algo) 176 175 177 176 err := store.Close() 178 177 if err != nil { ··· 190 189 t.Parallel() 191 190 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true}) 192 191 193 - root, err := os.OpenRoot(testRepo.Dir()) 194 - if err != nil { 195 - t.Fatalf("OpenRoot(%q): %v", testRepo.Dir(), err) 196 - } 197 - 198 - t.Cleanup(func() { _ = root.Close() }) 192 + root := testRepo.OpenPackRoot(t) 199 193 200 - _, err = packed.New(root, objectid.AlgorithmUnknown) 194 + _, err := packed.New(root, objectid.AlgorithmUnknown) 201 195 if !errors.Is(err, objectid.ErrInvalidAlgorithm) { 202 196 t.Fatalf("packed.New invalid algorithm error = %v", err) 203 197 } ··· 227 221 testRepo.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128") 228 222 229 223 deltaID, wantResolvedSize := findDeltaObjectWithResolvedSizeMismatch(t, testRepo, algo) 230 - store := openPackedStore(t, testRepo.Dir(), algo) 224 + store := openPackedStore(t, testRepo, algo) 231 225 232 226 _, gotSize, err := store.ReadHeader(deltaID) 233 227 if err != nil { ··· 252 246 func findDeltaObjectWithResolvedSizeMismatch(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) (objectid.ObjectID, int64) { 253 247 t.Helper() 254 248 255 - idxFiles, err := filepath.Glob(filepath.Join(testRepo.Dir(), "objects", "pack", "*.idx")) 249 + packRoot := testRepo.OpenPackRoot(t) 250 + 251 + entries, err := fs.ReadDir(packRoot.FS(), ".") 256 252 if err != nil { 257 - t.Fatalf("Glob idx: %v", err) 253 + t.Fatalf("ReadDir(pack): %v", err) 258 254 } 259 255 260 - if len(idxFiles) == 0 { 256 + var idxName string 257 + 258 + for _, entry := range entries { 259 + if strings.HasSuffix(entry.Name(), ".idx") { 260 + idxName = entry.Name() 261 + 262 + break 263 + } 264 + } 265 + 266 + if idxName == "" { 261 267 t.Fatalf("no idx files found") 262 268 } 263 269 264 - verifyOut := testRepo.Run(t, "verify-pack", "-v", idxFiles[0]) 270 + verifyOut := testRepo.Run(t, "verify-pack", "-v", "objects/pack/"+idxName) 265 271 for line := range strings.SplitSeq(strings.TrimSpace(verifyOut), "\n") { 266 272 fields := strings.Fields(line) 267 273 if len(fields) < 7 {
-122
reachability/ancestor.go
··· 1 - package reachability 2 - 3 - import ( 4 - "errors" 5 - 6 - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" 7 - "codeberg.org/lindenii/furgit/objectid" 8 - ) 9 - 10 - // IsAncestor reports whether ancestor is reachable from descendant via commit 11 - // parent edges. 12 - // 13 - // Both inputs are peeled through annotated tags before commit traversal. 14 - func (r *Reachability) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) { 15 - ancestorCommit, err := r.peelRootToCommit(ancestor) 16 - if err != nil { 17 - return false, err 18 - } 19 - 20 - descendantCommit, err := r.peelRootToCommit(descendant) 21 - if err != nil { 22 - return false, err 23 - } 24 - 25 - if ancestorCommit == descendantCommit { 26 - return true, nil 27 - } 28 - 29 - graphResult, graphUsed, err := r.isAncestorGraph(ancestorCommit, descendantCommit) 30 - if err != nil { 31 - return false, err 32 - } 33 - 34 - if graphUsed { 35 - return graphResult, nil 36 - } 37 - 38 - walk := r.Walk(DomainCommits, nil, map[objectid.ObjectID]struct{}{descendantCommit: {}}) 39 - for id := range walk.Seq() { 40 - if id == ancestorCommit { 41 - return true, nil 42 - } 43 - } 44 - 45 - err = walk.Err() 46 - if err != nil { 47 - return false, err 48 - } 49 - 50 - return false, nil 51 - } 52 - 53 - func (r *Reachability) isAncestorGraph(ancestor, descendant objectid.ObjectID) (bool, bool, error) { 54 - if r.graph == nil { 55 - return false, false, nil 56 - } 57 - 58 - ancestorPos, err := r.graph.Lookup(ancestor) 59 - if err != nil { 60 - var notFound *commitgraphread.NotFoundError 61 - if errors.As(err, &notFound) { 62 - return false, false, nil 63 - } 64 - 65 - return false, true, err 66 - } 67 - 68 - descendantPos, err := r.graph.Lookup(descendant) 69 - if err != nil { 70 - var notFound *commitgraphread.NotFoundError 71 - if errors.As(err, &notFound) { 72 - return false, false, nil 73 - } 74 - 75 - return false, true, err 76 - } 77 - 78 - ancestorCommit, err := r.graph.CommitAt(ancestorPos) 79 - if err != nil { 80 - return false, true, err 81 - } 82 - 83 - ancestorGeneration := ancestorCommit.GenerationV2 84 - stack := []commitgraphread.Position{descendantPos} 85 - visited := make(map[commitgraphread.Position]struct{}, 64) 86 - 87 - for len(stack) > 0 { 88 - pos := stack[len(stack)-1] 89 - stack = stack[:len(stack)-1] 90 - 91 - if _, ok := visited[pos]; ok { 92 - continue 93 - } 94 - 95 - visited[pos] = struct{}{} 96 - 97 - if pos == ancestorPos { 98 - return true, true, nil 99 - } 100 - 101 - commit, err := r.graph.CommitAt(pos) 102 - if err != nil { 103 - return false, true, err 104 - } 105 - 106 - if commit.GenerationV2 < ancestorGeneration { 107 - continue 108 - } 109 - 110 - if commit.Parent1.Valid { 111 - stack = append(stack, commit.Parent1.Pos) 112 - } 113 - 114 - if commit.Parent2.Valid { 115 - stack = append(stack, commit.Parent2.Pos) 116 - } 117 - 118 - stack = append(stack, commit.ExtraParents...) 119 - } 120 - 121 - return false, true, nil 122 - }
-39
reachability/errors.go
··· 1 - package reachability 2 - 3 - import ( 4 - "fmt" 5 - 6 - "codeberg.org/lindenii/furgit/objectid" 7 - "codeberg.org/lindenii/furgit/objecttype" 8 - ) 9 - 10 - // ObjectMissingError indicates that a referenced object is absent from the store. 11 - type ObjectMissingError struct { 12 - OID objectid.ObjectID 13 - } 14 - 15 - func (e *ObjectMissingError) Error() string { 16 - return fmt.Sprintf("reachability: missing object %s", e.OID) 17 - } 18 - 19 - // ObjectTypeError indicates that a referenced object has a different type than 20 - // what traversal expected on that edge. 21 - type ObjectTypeError struct { 22 - OID objectid.ObjectID 23 - Got objecttype.Type 24 - Want objecttype.Type 25 - } 26 - 27 - func (e *ObjectTypeError) Error() string { 28 - gotName, gotOK := objecttype.Name(e.Got) 29 - if !gotOK { 30 - gotName = fmt.Sprintf("type(%d)", e.Got) 31 - } 32 - 33 - wantName, wantOK := objecttype.Name(e.Want) 34 - if !wantOK { 35 - wantName = fmt.Sprintf("type(%d)", e.Want) 36 - } 37 - 38 - return fmt.Sprintf("reachability: object %s has type %s, want %s", e.OID, gotName, wantName) 39 - }
+3 -2
reachability/helpers.go
··· 4 4 "errors" 5 5 "fmt" 6 6 7 + giterrors "codeberg.org/lindenii/furgit/errors" 7 8 "codeberg.org/lindenii/furgit/objectid" 8 9 "codeberg.org/lindenii/furgit/objectstore" 9 10 "codeberg.org/lindenii/furgit/objecttype" ··· 39 40 ty, _, err := r.store.ReadHeader(id) 40 41 if err != nil { 41 42 if errors.Is(err, objectstore.ErrObjectNotFound) { 42 - return objecttype.TypeInvalid, &ObjectMissingError{OID: id} 43 + return objecttype.TypeInvalid, &giterrors.ObjectMissingError{OID: id} 43 44 } 44 45 45 46 return objecttype.TypeInvalid, err ··· 61 62 _, content, err := r.store.ReadBytesContent(id) 62 63 if err != nil { 63 64 if errors.Is(err, objectstore.ErrObjectNotFound) { 64 - return nil, &ObjectMissingError{OID: id} 65 + return nil, &giterrors.ObjectMissingError{OID: id} 65 66 } 66 67 67 68 return nil, err
+12 -89
reachability/integration_test.go
··· 3 3 import ( 4 4 "errors" 5 5 "fmt" 6 + "io/fs" 6 7 "maps" 7 - "os" 8 - "path/filepath" 9 8 "slices" 10 9 "strings" 11 10 "testing" 12 11 12 + giterrors "codeberg.org/lindenii/furgit/errors" 13 13 "codeberg.org/lindenii/furgit/internal/testgit" 14 14 "codeberg.org/lindenii/furgit/objectid" 15 15 "codeberg.org/lindenii/furgit/reachability" 16 - "codeberg.org/lindenii/furgit/repository" 17 16 ) 18 17 19 18 func TestWalkCommitsMatchesGitRevList(t *testing.T) { ··· 163 162 }) 164 163 } 165 164 166 - func TestIsAncestorMatchesGitMergeBase(t *testing.T) { 167 - t.Parallel() 168 - 169 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 170 - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ 171 - ObjectFormat: algo, 172 - Bare: true, 173 - RefFormat: "files", 174 - }) 175 - 176 - _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) 177 - c1 := testRepo.CommitTree(t, tree1, "c1") 178 - 179 - _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) 180 - c2 := testRepo.CommitTree(t, tree2, "c2", c1) 181 - 182 - _, tree3 := testRepo.MakeSingleFileTree(t, "three.txt", []byte("three\n")) 183 - c3 := testRepo.CommitTree(t, tree3, "c3", c2) 184 - 185 - tag := testRepo.TagAnnotated(t, "tip", c2, "tip") 186 - 187 - r := openReachabilityFromTestRepo(t, testRepo) 188 - 189 - got, err := r.IsAncestor(c1, tag) 190 - if err != nil { 191 - t.Fatalf("IsAncestor(c1, tag): %v", err) 192 - } 193 - 194 - want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) 195 - if got != want { 196 - t.Fatalf("IsAncestor(c1, tag)=%v, want %v", got, want) 197 - } 198 - 199 - got, err = r.IsAncestor(c3, c2) 200 - if err != nil { 201 - t.Fatalf("IsAncestor(c3, c2): %v", err) 202 - } 203 - 204 - want = gitMergeBaseIsAncestor(t, testRepo, c3, c2) 205 - if got != want { 206 - t.Fatalf("IsAncestor(c3, c2)=%v, want %v", got, want) 207 - } 208 - }) 209 - } 210 - 211 165 func TestCheckConnectedMissingObject(t *testing.T) { 212 166 t.Parallel() 213 167 ··· 220 174 221 175 _, treeID, commitID := testRepo.MakeCommit(t, "missing") 222 176 223 - err := os.Remove(looseObjectPath(testRepo.Dir(), treeID)) 224 - if err != nil { 225 - t.Fatalf("remove tree object: %v", err) 226 - } 177 + testRepo.RemoveLooseObject(t, treeID) 227 178 228 179 r := openReachabilityFromTestRepo(t, testRepo) 229 180 230 - err = r.CheckConnected( 181 + err := r.CheckConnected( 231 182 reachability.DomainObjects, 232 183 nil, 233 184 map[objectid.ObjectID]struct{}{commitID: {}}, ··· 236 187 t.Fatal("expected error") 237 188 } 238 189 239 - var missing *reachability.ObjectMissingError 190 + var missing *giterrors.ObjectMissingError 240 191 if !errors.As(err, &missing) { 241 192 t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) 242 193 } ··· 267 218 testRepo.Repack(t, "-ad") 268 219 testRepo.Run(t, "prune-packed") 269 220 270 - assertPackedOnly(t, testRepo.Dir()) 221 + assertPackedOnly(t, testRepo) 271 222 272 223 r := openReachabilityFromTestRepo(t, testRepo) 273 224 walk := r.Walk( ··· 298 249 func openReachabilityFromTestRepo(t *testing.T, testRepo *testgit.TestRepo) *reachability.Reachability { 299 250 t.Helper() 300 251 301 - root, err := os.OpenRoot(testRepo.Dir()) 302 - if err != nil { 303 - t.Fatalf("os.OpenRoot: %v", err) 304 - } 305 - 306 - t.Cleanup(func() { _ = root.Close() }) 307 - 308 - repo, err := repository.Open(root) 309 - if err != nil { 310 - t.Fatalf("repository.Open: %v", err) 311 - } 312 - 313 - t.Cleanup(func() { _ = repo.Close() }) 314 - 315 - return reachability.New(repo.Objects()) 252 + return reachability.New(testRepo.OpenObjectStore(t)) 316 253 } 317 254 318 255 func oidSetFromSeq(seq func(func(objectid.ObjectID) bool)) map[objectid.ObjectID]struct{} { ··· 379 316 return set 380 317 } 381 318 382 - func gitMergeBaseIsAncestor(t *testing.T, testRepo *testgit.TestRepo, a, b objectid.ObjectID) bool { 383 - t.Helper() 384 - // testgit.Run fatals on non-zero status, so we compare merge-base output. 385 - mb := testRepo.Run(t, "merge-base", a.String(), b.String()) 386 - 387 - return mb == a.String() 388 - } 389 - 390 319 func sortedOIDStrings(set map[objectid.ObjectID]struct{}) []string { 391 320 out := make([]string, 0, len(set)) 392 321 for id := range set { ··· 398 327 return out 399 328 } 400 329 401 - func looseObjectPath(repoDir string, id objectid.ObjectID) string { 402 - hex := id.String() 403 - 404 - return filepath.Join(repoDir, "objects", hex[:2], hex[2:]) 405 - } 406 - 407 - func assertPackedOnly(t *testing.T, repoDir string) { 330 + func assertPackedOnly(t *testing.T, testRepo *testgit.TestRepo) { 408 331 t.Helper() 409 332 410 - objectsDir := filepath.Join(repoDir, "objects") 333 + objectsRoot := testRepo.OpenObjectsRoot(t) 411 334 412 - entries, err := os.ReadDir(objectsDir) 335 + entries, err := fs.ReadDir(objectsRoot.FS(), ".") 413 336 if err != nil { 414 337 t.Fatalf("ReadDir(objects): %v", err) 415 338 } ··· 421 344 } 422 345 423 346 if len(name) == 2 && isHexDirName(name) { 424 - subEntries, err := os.ReadDir(filepath.Join(objectsDir, name)) 347 + subEntries, err := fs.ReadDir(objectsRoot.FS(), name) 425 348 if err != nil { 426 349 t.Fatalf("ReadDir(objects/%s): %v", name, err) 427 350 } 428 351 429 352 if len(subEntries) != 0 { 430 - t.Fatalf("found loose objects in %s", filepath.Join(objectsDir, name)) 353 + t.Fatalf("found loose objects in objects/%s", name) 431 354 } 432 355 } 433 356 }
-37
reachability/peel.go
··· 1 - package reachability 2 - 3 - import ( 4 - "codeberg.org/lindenii/furgit/object" 5 - "codeberg.org/lindenii/furgit/objectid" 6 - "codeberg.org/lindenii/furgit/objecttype" 7 - ) 8 - 9 - // peelRootToCommit peels annotated tags transitively until a commit is reached. 10 - func (r *Reachability) peelRootToCommit(id objectid.ObjectID) (objectid.ObjectID, error) { 11 - for { 12 - ty, err := r.readHeaderType(id) 13 - if err != nil { 14 - return objectid.ObjectID{}, err 15 - } 16 - 17 - if ty != objecttype.TypeTag { 18 - if ty != objecttype.TypeCommit { 19 - return objectid.ObjectID{}, &ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} 20 - } 21 - 22 - return id, nil 23 - } 24 - 25 - content, err := r.readBytesContent(id) 26 - if err != nil { 27 - return objectid.ObjectID{}, err 28 - } 29 - 30 - tag, err := object.ParseTag(content, id.Algorithm()) 31 - if err != nil { 32 - return objectid.ObjectID{}, err 33 - } 34 - 35 - id = tag.Target 36 - } 37 - }
+46 -200
reachability/unit_test.go
··· 1 1 package reachability_test 2 2 3 3 import ( 4 - "bytes" 5 4 "errors" 6 5 "fmt" 7 - "io" 8 6 "maps" 9 7 "slices" 10 8 "testing" 11 9 10 + giterrors "codeberg.org/lindenii/furgit/errors" 12 11 "codeberg.org/lindenii/furgit/internal/testgit" 13 12 "codeberg.org/lindenii/furgit/object" 14 - "codeberg.org/lindenii/furgit/objectheader" 15 13 "codeberg.org/lindenii/furgit/objectid" 16 - "codeberg.org/lindenii/furgit/objectstore" 14 + "codeberg.org/lindenii/furgit/objectstore/memory" 17 15 "codeberg.org/lindenii/furgit/objecttype" 18 16 "codeberg.org/lindenii/furgit/reachability" 19 17 ) 20 18 21 - type storeObject struct { 22 - ty objecttype.Type 23 - content []byte 24 - } 25 - 26 19 type memStore struct { 27 - algo objectid.Algorithm 28 - objects map[objectid.ObjectID]storeObject 20 + *memory.Store 21 + 29 22 readBytesByObjectID map[objectid.ObjectID]int 30 23 } 31 24 32 - func newMemStore(algo objectid.Algorithm) *memStore { 25 + // newCountingMemStore builds one in-memory store that records content-read 26 + // counts by object ID. 27 + func newCountingMemStore(algo objectid.Algorithm) *memStore { 33 28 return &memStore{ 34 - algo: algo, 35 - objects: make(map[objectid.ObjectID]storeObject), 29 + Store: memory.New(algo), 36 30 readBytesByObjectID: make(map[objectid.ObjectID]int), 37 31 } 38 32 } 39 33 40 - func (store *memStore) ReadBytesFull(id objectid.ObjectID) ([]byte, error) { 41 - obj, ok := store.objects[id] 42 - if !ok { 43 - return nil, objectstore.ErrObjectNotFound 44 - } 45 - 46 - header, ok := objectheader.Encode(obj.ty, int64(len(obj.content))) 47 - if !ok { 48 - panic("failed to encode object header") 49 - } 50 - 51 - raw := make([]byte, len(header)+len(obj.content)) 52 - copy(raw, header) 53 - copy(raw[len(header):], obj.content) 54 - 55 - return raw, nil 56 - } 57 - 58 34 func (store *memStore) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { 59 - obj, ok := store.objects[id] 60 - if !ok { 61 - return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound 62 - } 63 - 64 35 store.readBytesByObjectID[id]++ 65 36 66 - return obj.ty, append([]byte(nil), obj.content...), nil 67 - } 68 - 69 - func (store *memStore) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { 70 - raw, err := store.ReadBytesFull(id) 71 - if err != nil { 72 - return nil, err 73 - } 74 - 75 - return io.NopCloser(bytes.NewReader(raw)), nil 76 - } 77 - 78 - func (store *memStore) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { 79 - ty, content, err := store.ReadBytesContent(id) 80 - if err != nil { 81 - return objecttype.TypeInvalid, 0, nil, err 82 - } 83 - 84 - return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil 85 - } 86 - 87 - func (store *memStore) ReadSize(id objectid.ObjectID) (int64, error) { 88 - _, size, err := store.ReadHeader(id) 89 - if err != nil { 90 - return 0, err 91 - } 92 - 93 - return size, nil 94 - } 95 - 96 - func (store *memStore) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { 97 - obj, ok := store.objects[id] 98 - if !ok { 99 - return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound 100 - } 101 - 102 - return obj.ty, int64(len(obj.content)), nil 103 - } 104 - 105 - func (store *memStore) Close() error { 106 - return nil 37 + return store.Store.ReadBytesContent(id) 107 38 } 108 39 109 40 func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { ··· 151 82 t.Parallel() 152 83 153 84 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 154 - store := newMemStore(algo) 155 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 156 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 85 + store := newCountingMemStore(algo) 86 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 87 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 157 88 Mode: object.FileModeRegular, 158 89 Name: []byte("f"), 159 90 ID: blob, 160 91 }}})) 161 - commit1 := store.addObject(objecttype.TypeCommit, commitBody(tree)) 162 - commit2 := store.addObject(objecttype.TypeCommit, commitBody(tree, commit1)) 163 - tag1 := store.addObject(objecttype.TypeTag, tagBody(commit2, objecttype.TypeCommit)) 164 - tag2 := store.addObject(objecttype.TypeTag, tagBody(tag1, objecttype.TypeTag)) 92 + commit1 := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 93 + commit2 := store.AddObject(objecttype.TypeCommit, commitBody(tree, commit1)) 94 + tag1 := store.AddObject(objecttype.TypeTag, tagBody(commit2, objecttype.TypeCommit)) 95 + tag2 := store.AddObject(objecttype.TypeTag, tagBody(tag1, objecttype.TypeTag)) 165 96 166 97 r := reachability.New(store) 167 98 walk := r.Walk(reachability.DomainCommits, nil, map[objectid.ObjectID]struct{}{tag2: {}}) ··· 186 117 t.Parallel() 187 118 188 119 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 189 - store := newMemStore(algo) 190 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 191 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 120 + store := newCountingMemStore(algo) 121 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 122 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 192 123 Mode: object.FileModeRegular, 193 124 Name: []byte("f"), 194 125 ID: blob, 195 126 }}})) 196 - commit := store.addObject(objecttype.TypeCommit, commitBody(tree)) 127 + commit := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 197 128 198 129 r := reachability.New(store) 199 130 walk := r.Walk(reachability.DomainCommits, map[objectid.ObjectID]struct{}{commit: {}}, map[objectid.ObjectID]struct{}{commit: {}}) ··· 215 146 t.Parallel() 216 147 217 148 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 218 - store := newMemStore(algo) 219 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 220 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 149 + store := newCountingMemStore(algo) 150 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 151 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 221 152 Mode: object.FileModeRegular, 222 153 Name: []byte("f"), 223 154 ID: blob, 224 155 }}})) 225 - tag := store.addObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) 156 + tag := store.AddObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) 226 157 227 158 r := reachability.New(store) 228 159 walk := r.Walk(reachability.DomainCommits, nil, map[objectid.ObjectID]struct{}{tag: {}}) ··· 233 164 t.Fatal("expected error") 234 165 } 235 166 236 - var typeErr *reachability.ObjectTypeError 167 + var typeErr *giterrors.ObjectTypeError 237 168 if !errors.As(err, &typeErr) { 238 169 t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) 239 170 } ··· 248 179 t.Parallel() 249 180 250 181 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 251 - store := newMemStore(algo) 252 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 253 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 182 + store := newCountingMemStore(algo) 183 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 184 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 254 185 Mode: object.FileModeRegular, 255 186 Name: []byte("f"), 256 187 ID: blob, 257 188 }}})) 258 - commit1 := store.addObject(objecttype.TypeCommit, commitBody(tree)) 259 - commit2 := store.addObject(objecttype.TypeCommit, commitBody(tree, commit1)) 260 - tag1 := store.addObject(objecttype.TypeTag, tagBody(commit2, objecttype.TypeCommit)) 261 - tag2 := store.addObject(objecttype.TypeTag, tagBody(tag1, objecttype.TypeTag)) 189 + commit1 := store.AddObject(objecttype.TypeCommit, commitBody(tree)) 190 + commit2 := store.AddObject(objecttype.TypeCommit, commitBody(tree, commit1)) 191 + tag1 := store.AddObject(objecttype.TypeTag, tagBody(commit2, objecttype.TypeCommit)) 192 + tag2 := store.AddObject(objecttype.TypeTag, tagBody(tag1, objecttype.TypeTag)) 262 193 263 194 r := reachability.New(store) 264 195 walk := r.Walk( ··· 287 218 t.Parallel() 288 219 289 220 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 290 - store := newMemStore(algo) 221 + store := newCountingMemStore(algo) 291 222 292 - blob1 := store.addObject(objecttype.TypeBlob, []byte("b1\n")) 293 - blob2 := store.addObject(objecttype.TypeBlob, []byte("b2\n")) 294 - gitlinkTarget := store.algo.Sum([]byte("external-submodule")) 223 + blob1 := store.AddObject(objecttype.TypeBlob, []byte("b1\n")) 224 + blob2 := store.AddObject(objecttype.TypeBlob, []byte("b2\n")) 225 + gitlinkTarget := store.Algorithm().Sum([]byte("external-submodule")) 295 226 296 - subtree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 227 + subtree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 297 228 Mode: object.FileModeRegular, 298 229 Name: []byte("nested"), 299 230 ID: blob2, 300 231 }}})) 301 - rootTree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{ 232 + rootTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{ 302 233 {Mode: object.FileModeRegular, Name: []byte("a"), ID: blob1}, 303 234 {Mode: object.FileModeDir, Name: []byte("dir"), ID: subtree}, 304 235 {Mode: object.FileModeGitlink, Name: []byte("submodule"), ID: gitlinkTarget}, 305 236 }})) 306 - commit := store.addObject(objecttype.TypeCommit, commitBody(rootTree)) 237 + commit := store.AddObject(objecttype.TypeCommit, commitBody(rootTree)) 307 238 308 239 r := reachability.New(store) 309 240 walk := r.Walk(reachability.DomainObjects, nil, map[objectid.ObjectID]struct{}{commit: {}}) ··· 332 263 t.Parallel() 333 264 334 265 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 335 - store := newMemStore(algo) 336 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 337 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 266 + store := newCountingMemStore(algo) 267 + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) 268 + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 338 269 Mode: object.FileModeRegular, 339 270 Name: []byte("f"), 340 271 ID: blob, 341 272 }}})) 342 - missingParent := store.algo.Sum([]byte("missing-parent")) 343 - commit := store.addObject(objecttype.TypeCommit, commitBody(tree, missingParent)) 273 + missingParent := store.Algorithm().Sum([]byte("missing-parent")) 274 + commit := store.AddObject(objecttype.TypeCommit, commitBody(tree, missingParent)) 344 275 345 276 r := reachability.New(store) 346 277 ··· 349 280 t.Fatal("expected error") 350 281 } 351 282 352 - var missing *reachability.ObjectMissingError 283 + var missing *giterrors.ObjectMissingError 353 284 if !errors.As(err, &missing) { 354 285 t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) 355 286 } ··· 364 295 t.Parallel() 365 296 366 297 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 367 - r := reachability.New(newMemStore(algo)) 298 + r := reachability.New(newCountingMemStore(algo)) 368 299 walk := r.Walk(reachability.Domain(99), nil, nil) 369 300 370 301 _ = collectSeq(walk.Seq()) ··· 376 307 }) 377 308 } 378 309 379 - func TestIsAncestor(t *testing.T) { 380 - t.Parallel() 381 - 382 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 383 - store := newMemStore(algo) 384 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 385 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 386 - Mode: object.FileModeRegular, 387 - Name: []byte("f"), 388 - ID: blob, 389 - }}})) 390 - c1 := store.addObject(objecttype.TypeCommit, commitBody(tree)) 391 - c2 := store.addObject(objecttype.TypeCommit, commitBody(tree, c1)) 392 - otherBlob := store.addObject(objecttype.TypeBlob, []byte("other-blob\n")) 393 - otherTree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 394 - Mode: object.FileModeRegular, 395 - Name: []byte("g"), 396 - ID: otherBlob, 397 - }}})) 398 - c3 := store.addObject(objecttype.TypeCommit, commitBody(otherTree)) 399 - tag := store.addObject(objecttype.TypeTag, tagBody(c2, objecttype.TypeCommit)) 400 - 401 - r := reachability.New(store) 402 - 403 - ok, err := r.IsAncestor(c1, tag) 404 - if err != nil { 405 - t.Fatalf("IsAncestor(c1, tag): %v", err) 406 - } 407 - 408 - if !ok { 409 - t.Fatal("expected c1 to be ancestor of tag->c2") 410 - } 411 - 412 - ok, err = r.IsAncestor(c3, c2) 413 - if err != nil { 414 - t.Fatalf("IsAncestor(c3, c2): %v", err) 415 - } 416 - 417 - if ok { 418 - t.Fatal("did not expect c3 to be ancestor of c2") 419 - } 420 - }) 421 - } 422 - 423 - func TestIsAncestorRejectsNonCommitAfterPeel(t *testing.T) { 424 - t.Parallel() 425 - 426 - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 427 - store := newMemStore(algo) 428 - blob := store.addObject(objecttype.TypeBlob, []byte("blob\n")) 429 - tree := store.addObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ 430 - Mode: object.FileModeRegular, 431 - Name: []byte("f"), 432 - ID: blob, 433 - }}})) 434 - commit := store.addObject(objecttype.TypeCommit, commitBody(tree)) 435 - tagToTree := store.addObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) 436 - 437 - r := reachability.New(store) 438 - 439 - _, err := r.IsAncestor(commit, tagToTree) 440 - if err == nil { 441 - t.Fatal("expected error") 442 - } 443 - 444 - var typeErr *reachability.ObjectTypeError 445 - if !errors.As(err, &typeErr) { 446 - t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) 447 - } 448 - }) 449 - } 450 - 451 310 func mustSerializeTree(tb testing.TB, tree *object.Tree) []byte { 452 311 tb.Helper() 453 312 ··· 458 317 459 318 return body 460 319 } 461 - 462 - func (store *memStore) addObject(ty objecttype.Type, body []byte) objectid.ObjectID { 463 - header, ok := objectheader.Encode(ty, int64(len(body))) 464 - if !ok { 465 - panic("failed to encode object header") 466 - } 467 - 468 - raw := append(append([]byte(nil), header...), body...) 469 - id := store.algo.Sum(raw) 470 - store.objects[id] = storeObject{ty: ty, content: append([]byte(nil), body...)} 471 - 472 - return id 473 - }
+1 -1
reachability/walk.go
··· 4 4 "codeberg.org/lindenii/furgit/objectid" 5 5 ) 6 6 7 - // Walk is one single-use iterator-style traversal. 7 + // Walk is one single-use iterator traversal. 8 8 type Walk struct { 9 9 reachability *Reachability 10 10 domain Domain
+2 -1
reachability/walk_expand_commits.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 + "codeberg.org/lindenii/furgit/errors" 6 7 "codeberg.org/lindenii/furgit/object" 7 8 "codeberg.org/lindenii/furgit/objecttype" 8 9 ) ··· 63 64 return []walkItem{{id: tag.Target, want: objecttype.TypeInvalid}}, nil 64 65 case objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeInvalid, 65 66 objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta: 66 - return nil, &ObjectTypeError{OID: item.id, Got: ty, Want: objecttype.TypeCommit} 67 + return nil, &errors.ObjectTypeError{OID: item.id, Got: ty, Want: objecttype.TypeCommit} 67 68 } 68 69 69 70 return nil, fmt.Errorf("reachability: unreachable object type %d", ty)
+3 -2
reachability/walk_expand_objects.go
··· 3 3 import ( 4 4 "fmt" 5 5 6 + "codeberg.org/lindenii/furgit/errors" 6 7 "codeberg.org/lindenii/furgit/object" 7 8 "codeberg.org/lindenii/furgit/objecttype" 8 9 ) ··· 14 15 } 15 16 16 17 if item.want != objecttype.TypeInvalid && ty != item.want { 17 - return nil, &ObjectTypeError{OID: item.id, Got: ty, Want: item.want} 18 + return nil, &errors.ObjectTypeError{OID: item.id, Got: ty, Want: item.want} 18 19 } 19 20 20 21 switch ty { ··· 76 77 77 78 return []walkItem{{id: tag.Target, want: tag.TargetType}}, nil 78 79 case objecttype.TypeInvalid, objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta: 79 - return nil, &ObjectTypeError{OID: item.id, Got: ty, Want: item.want} 80 + return nil, &errors.ObjectTypeError{OID: item.id, Got: ty, Want: item.want} 80 81 } 81 82 82 83 return nil, fmt.Errorf("reachability: unreachable object type %d", ty)
+2 -1
reachability/walk_verify.go
··· 1 1 package reachability 2 2 3 3 import ( 4 + "codeberg.org/lindenii/furgit/errors" 4 5 "codeberg.org/lindenii/furgit/object" 5 6 "codeberg.org/lindenii/furgit/objectid" 6 7 "codeberg.org/lindenii/furgit/objecttype" ··· 13 14 } 14 15 15 16 if ty != objecttype.TypeCommit { 16 - return &ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} 17 + return &errors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit} 17 18 } 18 19 19 20 content, err := walk.readBytesContent(id)
+10 -27
refstore/loose/loose_test.go
··· 2 2 3 3 import ( 4 4 "errors" 5 - "os" 6 - "path/filepath" 7 5 "slices" 8 6 "testing" 9 7 ··· 14 12 "codeberg.org/lindenii/furgit/refstore/loose" 15 13 ) 16 14 17 - func openLooseStore(t *testing.T, repoPath string, algo objectid.Algorithm) *loose.Store { 15 + func openLooseStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store { 18 16 t.Helper() 19 17 20 - root, err := os.OpenRoot(repoPath) 21 - if err != nil { 22 - t.Fatalf("OpenRoot(%q): %v", repoPath, err) 23 - } 24 - 25 - t.Cleanup(func() { _ = root.Close() }) 18 + root := testRepo.OpenGitRoot(t) 26 19 27 20 store, err := loose.New(root, algo) 28 21 if err != nil { ··· 40 33 testRepo.UpdateRef(t, "refs/heads/main", commitID) 41 34 testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") 42 35 43 - store := openLooseStore(t, testRepo.Dir(), algo) 36 + store := openLooseStore(t, testRepo, algo) 44 37 45 38 resolvedHead, err := store.Resolve("HEAD") 46 39 if err != nil { ··· 93 86 testRepo.SymbolicRef(t, "refs/heads/a", "refs/heads/b") 94 87 testRepo.SymbolicRef(t, "refs/heads/b", "refs/heads/a") 95 88 96 - store := openLooseStore(t, testRepo.Dir(), algo) 89 + store := openLooseStore(t, testRepo, algo) 97 90 98 91 _, err := store.ResolveFully("refs/heads/a") 99 92 if err == nil { ··· 112 105 testRepo.UpdateRef(t, "refs/tags/v1.0.0", commitID) 113 106 testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") 114 107 115 - store := openLooseStore(t, testRepo.Dir(), algo) 108 + store := openLooseStore(t, testRepo, algo) 116 109 117 110 allRefs, err := store.List("") 118 111 if err != nil { ··· 161 154 testRepo.UpdateRef(t, "refs/tags/v1", commitID) 162 155 testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") 163 156 164 - store := openLooseStore(t, testRepo.Dir(), algo) 157 + store := openLooseStore(t, testRepo, algo) 165 158 166 159 tests := []struct { 167 160 pattern string ··· 223 216 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 224 217 testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 225 218 226 - refPath := filepath.Join(testRepo.Dir(), "refs", "heads", "bad") 227 - 228 - err := os.MkdirAll(filepath.Dir(refPath), 0o755) 229 - if err != nil { 230 - t.Fatalf("MkdirAll: %v", err) 231 - } 232 - 233 - err = os.WriteFile(refPath, []byte("not-a-hash\n"), 0o644) 234 - if err != nil { 235 - t.Fatalf("WriteFile: %v", err) 236 - } 219 + testRepo.WriteFileAll(t, "refs/heads/bad", []byte("not-a-hash\n"), 0o755, 0o644) 237 220 238 - store := openLooseStore(t, testRepo.Dir(), algo) 221 + store := openLooseStore(t, testRepo, algo) 239 222 240 - _, err = store.Resolve("refs/heads/bad") 223 + _, err := store.Resolve("refs/heads/bad") 241 224 if err == nil { 242 225 t.Fatalf("Resolve(malformed) expected error") 243 226 } ··· 253 236 testRepo.UpdateRef(t, "refs/tags/main", commitID) 254 237 testRepo.UpdateRef(t, "refs/remotes/origin/main", commitID) 255 238 256 - store := openLooseStore(t, testRepo.Dir(), algo) 239 + store := openLooseStore(t, testRepo, algo) 257 240 258 241 shortHead, err := store.Shorten("refs/heads/main") 259 242 if err != nil {
+10 -15
refstore/packed/packed_test.go
··· 14 14 "codeberg.org/lindenii/furgit/refstore/packed" 15 15 ) 16 16 17 - func openPackedRefStoreFromRepo(t *testing.T, repoPath string, algo objectid.Algorithm) *packed.Store { 17 + func openPackedRefStoreFromRepo(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *packed.Store { 18 18 t.Helper() 19 19 20 - root, err := os.OpenRoot(repoPath) 21 - if err != nil { 22 - t.Fatalf("OpenRoot(repo): %v", err) 23 - } 24 - 25 - defer func() { _ = root.Close() }() 20 + root := testRepo.OpenGitRoot(t) 26 21 27 22 store, err := packed.New(root, algo) 28 23 if err != nil { ··· 37 32 38 33 dir := t.TempDir() 39 34 40 - err := os.WriteFile(dir+"/packed-refs", []byte(content), 0o644) 41 - if err != nil { 42 - t.Fatalf("WriteFile(packed-refs): %v", err) 43 - } 44 - 45 35 root, err := os.OpenRoot(dir) 46 36 if err != nil { 47 37 t.Fatalf("OpenRoot(temp): %v", err) 48 38 } 49 39 50 40 defer func() { _ = root.Close() }() 41 + 42 + err = root.WriteFile("packed-refs", []byte(content), 0o644) 43 + if err != nil { 44 + t.Fatalf("WriteFile(packed-refs): %v", err) 45 + } 51 46 52 47 return packed.New(root, algo) 53 48 } ··· 61 56 tagID := testRepo.TagAnnotated(t, "v1.0.0", commitID, "annotated tag") 62 57 testRepo.PackRefs(t, "--all", "--prune") 63 58 64 - store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo) 59 + store := openPackedRefStoreFromRepo(t, testRepo, algo) 65 60 66 61 resolvedMain, err := store.Resolve("refs/heads/main") 67 62 if err != nil { ··· 125 120 testRepo.UpdateRef(t, "refs/remotes/origin/main", commitID) 126 121 testRepo.PackRefs(t, "--all", "--prune") 127 122 128 - store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo) 123 + store := openPackedRefStoreFromRepo(t, testRepo, algo) 129 124 130 125 all, err := store.List("") 131 126 if err != nil { ··· 180 175 testRepo.UpdateRef(t, "refs/tags/v1", commitID) 181 176 testRepo.PackRefs(t, "--all", "--prune") 182 177 183 - store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo) 178 + store := openPackedRefStoreFromRepo(t, testRepo, algo) 184 179 185 180 tests := []struct { 186 181 pattern string
+2 -28
repository/refs_test.go
··· 1 1 package repository_test 2 2 3 3 import ( 4 - "os" 5 4 "testing" 6 5 7 6 "codeberg.org/lindenii/furgit/internal/testgit" 8 7 "codeberg.org/lindenii/furgit/objectid" 9 8 "codeberg.org/lindenii/furgit/objecttype" 10 9 "codeberg.org/lindenii/furgit/ref" 11 - "codeberg.org/lindenii/furgit/repository" 12 10 ) 13 11 14 12 func TestOpenFilesRefFormat(t *testing.T) { ··· 25 23 repoHarness.UpdateRef(t, "refs/heads/main", commitID) 26 24 repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") 27 25 28 - root, err := os.OpenRoot(repoHarness.Dir()) 29 - if err != nil { 30 - t.Fatalf("os.OpenRoot: %v", err) 31 - } 32 - 33 - defer func() { _ = root.Close() }() 34 - 35 - repo, err := repository.Open(root) 36 - if err != nil { 37 - t.Fatalf("repository.Open: %v", err) 38 - } 39 - 40 - defer func() { _ = repo.Close() }() 26 + repo := repoHarness.OpenRepository(t) 41 27 42 28 if repo.Algorithm() != algo { 43 29 t.Fatalf("Algorithm = %v, want %v", repo.Algorithm(), algo) ··· 114 100 func assertResolveFully(t *testing.T, repoHarness *testgit.TestRepo, name string, want objectid.ObjectID) { 115 101 t.Helper() 116 102 117 - root, err := os.OpenRoot(repoHarness.Dir()) 118 - if err != nil { 119 - t.Fatalf("os.OpenRoot: %v", err) 120 - } 121 - 122 - defer func() { _ = root.Close() }() 123 - 124 - repo, err := repository.Open(root) 125 - if err != nil { 126 - t.Fatalf("repository.Open: %v", err) 127 - } 128 - 129 - defer func() { _ = repo.Close() }() 103 + repo := repoHarness.OpenRepository(t) 130 104 131 105 resolved, err := repo.Refs().ResolveFully(name) 132 106 if err != nil {
+6 -80
repository/stored_test.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 - "os" 6 5 "strings" 7 6 "testing" 8 7 9 8 "codeberg.org/lindenii/furgit/internal/testgit" 10 9 "codeberg.org/lindenii/furgit/object" 11 10 "codeberg.org/lindenii/furgit/objectid" 12 - "codeberg.org/lindenii/furgit/repository" 13 11 ) 14 12 15 13 func TestReadStoredTyped(t *testing.T) { ··· 24 22 25 23 blobID, treeID, commitID := repoHarness.MakeCommit(t, "stored types") 26 24 27 - root, err := os.OpenRoot(repoHarness.Dir()) 28 - if err != nil { 29 - t.Fatalf("os.OpenRoot: %v", err) 30 - } 31 - 32 - defer func() { _ = root.Close() }() 33 - 34 - repo, err := repository.Open(root) 35 - if err != nil { 36 - t.Fatalf("repository.Open: %v", err) 37 - } 38 - 39 - defer func() { _ = repo.Close() }() 25 + repo := repoHarness.OpenRepository(t) 40 26 41 27 blob, err := repo.ReadStoredBlob(blobID) 42 28 if err != nil { ··· 93 79 childTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", blobID)) 94 80 rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("040000 tree %s\tdir\n", childTreeID)) 95 81 96 - root, err := os.OpenRoot(repoHarness.Dir()) 97 - if err != nil { 98 - t.Fatalf("os.OpenRoot: %v", err) 99 - } 100 - 101 - defer func() { _ = root.Close() }() 102 - 103 - repo, err := repository.Open(root) 104 - if err != nil { 105 - t.Fatalf("repository.Open: %v", err) 106 - } 107 - 108 - defer func() { _ = repo.Close() }() 82 + repo := repoHarness.OpenRepository(t) 109 83 110 84 rootTree, err := repo.ReadStoredTree(rootTreeID) 111 85 if err != nil { ··· 141 115 blobID := repoHarness.HashObject(t, "blob", []byte("body\n")) 142 116 rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tfile.txt\n", blobID)) 143 117 144 - root, err := os.OpenRoot(repoHarness.Dir()) 145 - if err != nil { 146 - t.Fatalf("os.OpenRoot: %v", err) 147 - } 148 - 149 - defer func() { _ = root.Close() }() 150 - 151 - repo, err := repository.Open(root) 152 - if err != nil { 153 - t.Fatalf("repository.Open: %v", err) 154 - } 155 - 156 - defer func() { _ = repo.Close() }() 118 + repo := repoHarness.OpenRepository(t) 157 119 158 120 rootTree, err := repo.ReadStoredTree(rootTreeID) 159 121 if err != nil { ··· 176 138 blobID := repoHarness.HashObject(t, "blob", []byte("body\n")) 177 139 rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tdir\n", blobID)) 178 140 179 - root, err := os.OpenRoot(repoHarness.Dir()) 180 - if err != nil { 181 - t.Fatalf("os.OpenRoot: %v", err) 182 - } 183 - 184 - defer func() { _ = root.Close() }() 185 - 186 - repo, err := repository.Open(root) 187 - if err != nil { 188 - t.Fatalf("repository.Open: %v", err) 189 - } 190 - 191 - defer func() { _ = repo.Close() }() 141 + repo := repoHarness.OpenRepository(t) 192 142 193 143 rootTree, err := repo.ReadStoredTree(rootTreeID) 194 144 if err != nil { ··· 227 177 228 178 parts = append(parts, []byte("leaf.txt")) 229 179 230 - root, err := os.OpenRoot(repoHarness.Dir()) 231 - if err != nil { 232 - t.Fatalf("os.OpenRoot: %v", err) 233 - } 234 - 235 - defer func() { _ = root.Close() }() 236 - 237 - repo, err := repository.Open(root) 238 - if err != nil { 239 - t.Fatalf("repository.Open: %v", err) 240 - } 241 - 242 - defer func() { _ = repo.Close() }() 180 + repo := repoHarness.OpenRepository(t) 243 181 244 182 rootTree, err := repo.ReadStoredTree(currentTree) 245 183 if err != nil { ··· 287 225 ), 288 226 ) 289 227 290 - root, err := os.OpenRoot(repoHarness.Dir()) 291 - if err != nil { 292 - t.Fatalf("os.OpenRoot: %v", err) 293 - } 294 - 295 - defer func() { _ = root.Close() }() 296 - 297 - repo, err := repository.Open(root) 298 - if err != nil { 299 - t.Fatalf("repository.Open: %v", err) 300 - } 301 - 302 - defer func() { _ = repo.Close() }() 228 + repo := repoHarness.OpenRepository(t) 303 229 304 230 rootTree, err := repo.ReadStoredTree(rootTreeID) 305 231 if err != nil {
+51 -25
repository/traversal_test.go
··· 31 31 repoHarness.UpdateRef(t, "refs/heads/main", commit2) 32 32 repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") 33 33 34 - walkRepositoryFromHead(t, repoHarness.Dir()) 34 + root := repoHarness.OpenGitRoot(t) 35 + walkRepositoryFromRoot(t, root, "test repo") 35 36 }) 36 37 } 37 38 ··· 39 40 t.Parallel() 40 41 41 42 worktreeRoot := filepath.Clean("..") 42 - gitPath := filepath.Join(worktreeRoot, ".git") 43 + 44 + worktreeFS, err := os.OpenRoot(worktreeRoot) 45 + if err != nil { 46 + t.Fatalf("os.OpenRoot(%q): %v", worktreeRoot, err) 47 + } 48 + 49 + defer func() { _ = worktreeFS.Close() }() 43 50 44 - info, err := os.Stat(gitPath) 51 + info, err := worktreeFS.Stat(".git") 45 52 if err != nil { 46 - t.Fatalf("stat %q: %v", gitPath, err) 53 + t.Fatalf("stat %q: %v", filepath.Join(worktreeRoot, ".git"), err) 47 54 } 48 55 49 56 if info.IsDir() { 50 - walkRepositoryFromHead(t, gitPath) 57 + gitRoot, err := worktreeFS.OpenRoot(".git") 58 + if err != nil { 59 + t.Fatalf("OpenRoot(.git): %v", err) 60 + } 61 + 62 + defer func() { _ = gitRoot.Close() }() 63 + 64 + walkRepositoryFromRoot(t, gitRoot, filepath.Join(worktreeRoot, ".git")) 51 65 52 66 return 53 67 } 54 68 55 69 if !info.Mode().IsRegular() { 56 - t.Fatalf("%q is neither a directory nor a regular file", gitPath) 70 + t.Fatalf("%q is neither a directory nor a regular file", filepath.Join(worktreeRoot, ".git")) 57 71 } 58 72 59 - content, err := os.ReadFile(gitPath) //#nosec G304 73 + content, err := worktreeFS.ReadFile(".git") 60 74 if err != nil { 61 - t.Fatalf("read %q: %v", gitPath, err) 75 + t.Fatalf("read %q: %v", filepath.Join(worktreeRoot, ".git"), err) 62 76 } 63 77 64 78 line := strings.TrimSpace(string(content)) 65 79 66 80 prefix := "gitdir: " 67 81 if !strings.HasPrefix(line, prefix) { 68 - t.Fatalf("%q file does not begin with %q", gitPath, prefix) 82 + t.Fatalf("%q file does not begin with %q", filepath.Join(worktreeRoot, ".git"), prefix) 69 83 } 70 84 71 85 gitdirRel := strings.TrimSpace(line[len(prefix):]) 72 86 if gitdirRel == "" { 73 - t.Fatalf("%q contains empty gitdir path", gitPath) 87 + t.Fatalf("%q contains empty gitdir path", filepath.Join(worktreeRoot, ".git")) 74 88 } 75 89 76 90 gitdirPath := gitdirRel ··· 78 92 gitdirPath = filepath.Join(worktreeRoot, gitdirPath) 79 93 } 80 94 81 - commondirPath := filepath.Join(gitdirPath, "commondir") 95 + gitRoot, err := os.OpenRoot(gitdirPath) 96 + if err != nil { 97 + t.Fatalf("os.OpenRoot(%q): %v", gitdirPath, err) 98 + } 99 + 100 + defer func() { _ = gitRoot.Close() }() 82 101 83 - commondirContent, err := os.ReadFile(commondirPath) //#nosec G304 102 + commondirContent, err := gitRoot.ReadFile("commondir") 84 103 if err != nil { 85 - t.Fatalf("read %q: %v", commondirPath, err) 104 + t.Fatalf("read %q: %v", filepath.Join(gitdirPath, "commondir"), err) 86 105 } 87 106 88 107 repoPath := strings.TrimSpace(string(commondirContent)) 89 108 if repoPath == "" { 90 - t.Fatalf("%q contains empty repo path", commondirPath) 109 + t.Fatalf("%q contains empty repo path", filepath.Join(gitdirPath, "commondir")) 91 110 } 92 111 93 112 if filepath.IsAbs(repoPath) { 94 - walkRepositoryFromHead(t, repoPath) 113 + repoRoot, err := os.OpenRoot(repoPath) 114 + if err != nil { 115 + t.Fatalf("os.OpenRoot(%q): %v", repoPath, err) 116 + } 117 + 118 + defer func() { _ = repoRoot.Close() }() 119 + 120 + walkRepositoryFromRoot(t, repoRoot, repoPath) 95 121 96 122 return 97 123 } 98 124 99 125 repoPath = filepath.Join(gitdirPath, repoPath) 100 126 101 - walkRepositoryFromHead(t, repoPath) 102 - } 103 - 104 - func walkRepositoryFromHead(t *testing.T, repoPath string) { 105 - t.Helper() 106 - 107 - root, err := os.OpenRoot(repoPath) 127 + repoRoot, err := os.OpenRoot(repoPath) 108 128 if err != nil { 109 129 t.Fatalf("os.OpenRoot(%q): %v", repoPath, err) 110 130 } 111 131 112 - defer func() { _ = root.Close() }() 132 + defer func() { _ = repoRoot.Close() }() 133 + 134 + walkRepositoryFromRoot(t, repoRoot, repoPath) 135 + } 136 + 137 + func walkRepositoryFromRoot(t *testing.T, root *os.Root, label string) { 138 + t.Helper() 113 139 114 140 repo, err := repository.Open(root) 115 141 if err != nil { 116 - t.Fatalf("repository.Open(root for %q): %v", repoPath, err) 142 + t.Fatalf("repository.Open(root for %q): %v", label, err) 117 143 } 118 144 119 145 defer func() { _ = repo.Close() }() ··· 129 155 } 130 156 131 157 if objectsRead <= 0 { 132 - t.Fatalf("no objects were enumerated from HEAD (%s)", fmt.Sprintf("%q", repoPath)) 158 + t.Fatalf("no objects were enumerated from HEAD (%s)", fmt.Sprintf("%q", label)) 133 159 } 134 160 } 135 161
+3 -41
repository/write_loose_test.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "os" 6 5 "testing" 7 6 8 7 "codeberg.org/lindenii/furgit/internal/testgit" 9 8 "codeberg.org/lindenii/furgit/objectid" 10 9 "codeberg.org/lindenii/furgit/objecttype" 11 - "codeberg.org/lindenii/furgit/repository" 12 10 ) 13 11 14 12 func TestWriteLooseBytesContent(t *testing.T) { ··· 21 19 RefFormat: "files", 22 20 }) 23 21 24 - root, err := os.OpenRoot(repoHarness.Dir()) 25 - if err != nil { 26 - t.Fatalf("os.OpenRoot: %v", err) 27 - } 28 - 29 - defer func() { _ = root.Close() }() 30 - 31 - repo, err := repository.Open(root) 32 - if err != nil { 33 - t.Fatalf("repository.Open: %v", err) 34 - } 35 - 36 - defer func() { _ = repo.Close() }() 22 + repo := repoHarness.OpenRepository(t) 37 23 38 24 content := []byte("write-loose-bytes-content\n") 39 25 ··· 72 58 RefFormat: "files", 73 59 }) 74 60 75 - root, err := os.OpenRoot(repoHarness.Dir()) 76 - if err != nil { 77 - t.Fatalf("os.OpenRoot: %v", err) 78 - } 79 - 80 - defer func() { _ = root.Close() }() 81 - 82 - repo, err := repository.Open(root) 83 - if err != nil { 84 - t.Fatalf("repository.Open: %v", err) 85 - } 86 - 87 - defer func() { _ = repo.Close() }() 61 + repo := repoHarness.OpenRepository(t) 88 62 89 63 content := []byte("write-loose-reader-content\n") 90 64 ··· 111 85 }) 112 86 _, _, commitID := repoHarness.MakeCommit(t, "write-loose-full") 113 87 114 - root, err := os.OpenRoot(repoHarness.Dir()) 115 - if err != nil { 116 - t.Fatalf("os.OpenRoot: %v", err) 117 - } 118 - 119 - defer func() { _ = root.Close() }() 120 - 121 - repo, err := repository.Open(root) 122 - if err != nil { 123 - t.Fatalf("repository.Open: %v", err) 124 - } 125 - 126 - defer func() { _ = repo.Close() }() 88 + repo := repoHarness.OpenRepository(t) 127 89 128 90 raw, err := repo.Objects().ReadBytesFull(commitID) 129 91 if err != nil {