diff options
Diffstat (limited to 'ancestor')
| -rw-r--r-- | ancestor/ancestor.go | 45 | ||||
| -rw-r--r-- | ancestor/integration_test.go | 133 | ||||
| -rw-r--r-- | ancestor/unit_test.go | 118 |
3 files changed, 296 insertions, 0 deletions
diff --git a/ancestor/ancestor.go b/ancestor/ancestor.go new file mode 100644 index 00000000..ad90bd33 --- /dev/null +++ b/ancestor/ancestor.go @@ -0,0 +1,45 @@ +// Package ancestor answers commit ancestry queries. +package ancestor + +import ( + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" + "codeberg.org/lindenii/furgit/internal/commitquery" + "codeberg.org/lindenii/furgit/internal/peel" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objectstore" +) + +// Is reports whether ancestor is reachable from descendant through commit +// parent edges. +// +// Both inputs are peeled through annotated tags before commit traversal. +func Is( + store objectstore.Store, + graph *commitgraphread.Reader, + ancestor objectid.ObjectID, + descendant objectid.ObjectID, +) (bool, error) { + ancestorCommit, err := peel.ToCommit(store, ancestor) + if err != nil { + return false, err + } + + descendantCommit, err := peel.ToCommit(store, descendant) + if err != nil { + return false, err + } + + ctx := commitquery.NewContext(store, graph) + + ancestorIdx, err := ctx.ResolveOID(ancestorCommit) + if err != nil { + return false, err + } + + descendantIdx, err := ctx.ResolveOID(descendantCommit) + if err != nil { + return false, err + } + + return commitquery.IsAncestor(ctx, ancestorIdx, descendantIdx) +} diff --git a/ancestor/integration_test.go b/ancestor/integration_test.go new file mode 100644 index 00000000..d13c86ed --- /dev/null +++ b/ancestor/integration_test.go @@ -0,0 +1,133 @@ +package ancestor_test + +import ( + "errors" + "testing" + + giterrors "codeberg.org/lindenii/furgit/errors" + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" + + "codeberg.org/lindenii/furgit/ancestor" +) + +func TestIsMatchesGitMergeBase(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) + c1 := testRepo.CommitTree(t, tree1, "c1") + + _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) + c2 := testRepo.CommitTree(t, tree2, "c2", c1) + + _, tree3 := testRepo.MakeSingleFileTree(t, "three.txt", []byte("three\n")) + c3 := testRepo.CommitTree(t, tree3, "c3", c2) + + tag := testRepo.TagAnnotated(t, "tip", c2, "tip") + + store := testRepo.OpenObjectStore(t) + + got, err := ancestor.Is(store, nil, c1, tag) + if err != nil { + t.Fatalf("Is(c1, tag): %v", err) + } + + want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) + if got != want { + t.Fatalf("Is(c1, tag)=%v, want %v", got, want) + } + + got, err = ancestor.Is(store, nil, c3, c2) + if err != nil { + t.Fatalf("Is(c3, c2): %v", err) + } + + want = gitMergeBaseIsAncestor(t, testRepo, c3, c2) + if got != want { + t.Fatalf("Is(c3, c2)=%v, want %v", got, want) + } + }) +} + +func TestIsMatchesGitMergeBaseWithCommitGraph(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + _, tree1 := testRepo.MakeSingleFileTree(t, "one.txt", []byte("one\n")) + c1 := testRepo.CommitTree(t, tree1, "c1") + + _, tree2 := testRepo.MakeSingleFileTree(t, "two.txt", []byte("two\n")) + c2 := testRepo.CommitTree(t, tree2, "c2", c1) + + testRepo.UpdateRef(t, "refs/heads/main", c2) + testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") + testRepo.CommitGraphWrite(t, "--reachable") + + store := testRepo.OpenObjectStore(t) + graph := testRepo.OpenCommitGraph(t) + + got, err := ancestor.Is(store, graph, c1, c2) + if err != nil { + t.Fatalf("Is(c1, c2): %v", err) + } + + want := gitMergeBaseIsAncestor(t, testRepo, c1, c2) + if got != want { + t.Fatalf("Is(c1, c2)=%v, want %v", got, want) + } + }) +} + +func TestIsMissingObject(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + _, treeID, commitID := testRepo.MakeCommit(t, "missing") + + testRepo.RemoveLooseObject(t, treeID) + + store := testRepo.OpenObjectStore(t) + + _, err := ancestor.Is(store, nil, treeID, commitID) + if err == nil { + t.Fatal("expected error") + } + + var missing *giterrors.ObjectMissingError + if !errors.As(err, &missing) { + t.Fatalf("expected ObjectMissingError, got %T (%v)", err, err) + } + + if missing.OID != treeID { + t.Fatalf("missing oid = %s, want %s", missing.OID, treeID) + } + }) +} + +// gitMergeBaseIsAncestor reports Git's merge-base ancestry answer. +func gitMergeBaseIsAncestor(t *testing.T, testRepo *testgit.TestRepo, left, right objectid.ObjectID) bool { + t.Helper() + + out := testRepo.Run(t, "merge-base", left.String(), right.String()) + + return out == left.String() +} diff --git a/ancestor/unit_test.go b/ancestor/unit_test.go new file mode 100644 index 00000000..827aeb8f --- /dev/null +++ b/ancestor/unit_test.go @@ -0,0 +1,118 @@ +package ancestor_test + +import ( + "errors" + "fmt" + "testing" + + giterrors "codeberg.org/lindenii/furgit/errors" + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objectstore/memory" + "codeberg.org/lindenii/furgit/objecttype" + + "codeberg.org/lindenii/furgit/ancestor" +) + +// commitBody serializes one minimal commit body. +func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { + buf := fmt.Appendf(nil, "tree %s\n", tree.String()) + for _, parent := range parents { + buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) + } + + buf = append(buf, []byte("\nmsg\n")...) + + return buf +} + +// tagBody serializes one minimal annotated tag body. +func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { + targetName, ok := objecttype.Name(targetType) + if !ok { + panic("invalid tag target type") + } + + return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) +} + +// mustSerializeTree serializes one tree or fails the test. +func mustSerializeTree(tb testing.TB, tree *object.Tree) []byte { + tb.Helper() + + body, err := tree.SerializeWithoutHeader() + if err != nil { + tb.Fatalf("SerializeWithoutHeader: %v", err) + } + + return body +} + +func TestIs(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ + Mode: object.FileModeRegular, + Name: []byte("f"), + ID: blob, + }}})) + c1 := store.AddObject(objecttype.TypeCommit, commitBody(tree)) + c2 := store.AddObject(objecttype.TypeCommit, commitBody(tree, c1)) + otherBlob := store.AddObject(objecttype.TypeBlob, []byte("other-blob\n")) + otherTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ + Mode: object.FileModeRegular, + Name: []byte("g"), + ID: otherBlob, + }}})) + c3 := store.AddObject(objecttype.TypeCommit, commitBody(otherTree)) + tag := store.AddObject(objecttype.TypeTag, tagBody(c2, objecttype.TypeCommit)) + + ok, err := ancestor.Is(store, nil, c1, tag) + if err != nil { + t.Fatalf("Is(c1, tag): %v", err) + } + + if !ok { + t.Fatal("expected c1 to be ancestor of tag->c2") + } + + ok, err = ancestor.Is(store, nil, c3, c2) + if err != nil { + t.Fatalf("Is(c3, c2): %v", err) + } + + if ok { + t.Fatal("did not expect c3 to be ancestor of c2") + } + }) +} + +func TestIsRejectsNonCommitAfterPeel(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{ + Mode: object.FileModeRegular, + Name: []byte("f"), + ID: blob, + }}})) + commit := store.AddObject(objecttype.TypeCommit, commitBody(tree)) + tagToTree := store.AddObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) + + _, err := ancestor.Is(store, nil, commit, tagToTree) + if err == nil { + t.Fatal("expected error") + } + + var typeErr *giterrors.ObjectTypeError + if !errors.As(err, &typeErr) { + t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) + } + }) +} |
