aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ancestor/ancestor.go45
-rw-r--r--commitquery/ancestor.go48
-rw-r--r--commitquery/ancestor_integration_test.go (renamed from ancestor/integration_test.go)13
-rw-r--r--commitquery/ancestor_unit_test.go (renamed from ancestor/unit_test.go)40
-rw-r--r--commitquery/bits.go (renamed from internal/commitquery/bits.go)0
-rw-r--r--commitquery/commit.go (renamed from internal/commitquery/commit.go)6
-rw-r--r--commitquery/compare.go25
-rw-r--r--commitquery/context.go34
-rw-r--r--commitquery/errors.go (renamed from internal/commitquery/errors.go)0
-rw-r--r--commitquery/generation.go43
-rw-r--r--commitquery/graph_pos.go107
-rw-r--r--commitquery/load.go14
-rw-r--r--commitquery/marks.go71
-rw-r--r--commitquery/merge_bases.go171
-rw-r--r--commitquery/mergebase_integration_test.go (renamed from mergebase/integration_test.go)26
-rw-r--r--commitquery/mergebase_unit_test.go (renamed from mergebase/unit_test.go)48
-rw-r--r--commitquery/node.go (renamed from internal/commitquery/node.go)14
-rw-r--r--commitquery/oid.go (renamed from internal/commitquery/oid.go)55
-rw-r--r--commitquery/parent.go (renamed from internal/commitquery/parent.go)14
-rw-r--r--commitquery/populate.go42
-rw-r--r--commitquery/priority_queue.go (renamed from internal/commitquery/priority_queue.go)22
-rw-r--r--commitquery/reduce.go166
-rw-r--r--internal/commitquery/ancestor.go30
-rw-r--r--internal/commitquery/compare.go25
-rw-r--r--internal/commitquery/context.go32
-rw-r--r--internal/commitquery/generation.go43
-rw-r--r--internal/commitquery/graph_pos.go107
-rw-r--r--internal/commitquery/load.go14
-rw-r--r--internal/commitquery/marks.go67
-rw-r--r--internal/commitquery/merge_bases.go116
-rw-r--r--internal/commitquery/populate.go42
-rw-r--r--internal/commitquery/reduce.go166
-rw-r--r--mergebase/base.go30
-rw-r--r--mergebase/compute.go73
-rw-r--r--mergebase/mergebase.go20
-rw-r--r--mergebase/query.go24
-rw-r--r--receivepack/hooks/reject_force_push.go4
37 files changed, 846 insertions, 951 deletions
diff --git a/ancestor/ancestor.go b/ancestor/ancestor.go
deleted file mode 100644
index 51684250..00000000
--- a/ancestor/ancestor.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Package ancestor answers commit ancestry queries.
-package ancestor
-
-import (
- commitgraphread "codeberg.org/lindenii/furgit/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/commitquery/ancestor.go b/commitquery/ancestor.go
new file mode 100644
index 00000000..671ea460
--- /dev/null
+++ b/commitquery/ancestor.go
@@ -0,0 +1,48 @@
+package commitquery
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// IsAncestor reports whether ancestor is reachable from descendant through
+// commit parent edges.
+//
+// Both inputs are peeled through annotated tags before commit traversal.
+func (query *Query) IsAncestor(ancestor, descendant objectid.ObjectID) (bool, error) {
+ ancestorIdx, err := query.resolveCommitish(ancestor)
+ if err != nil {
+ return false, err
+ }
+
+ descendantIdx, err := query.resolveCommitish(descendant)
+ if err != nil {
+ return false, err
+ }
+
+ return query.isAncestor(ancestorIdx, descendantIdx)
+}
+
+func (query *Query) isAncestor(ancestor, descendant nodeIndex) (bool, error) {
+ if ancestor == descendant {
+ return true, nil
+ }
+
+ ancestorGeneration := query.effectiveGeneration(ancestor)
+ descendantGeneration := query.effectiveGeneration(descendant)
+
+ if ancestorGeneration != generationInfinity &&
+ descendantGeneration != generationInfinity &&
+ ancestorGeneration > descendantGeneration {
+ return false, nil
+ }
+
+ minGeneration := uint64(0)
+ if ancestorGeneration != generationInfinity {
+ minGeneration = ancestorGeneration
+ }
+
+ err := query.paintDownToCommon(ancestor, []nodeIndex{descendant}, minGeneration)
+ if err != nil {
+ return false, err
+ }
+
+ return query.hasAnyMarks(ancestor, markRight), nil
+}
diff --git a/ancestor/integration_test.go b/commitquery/ancestor_integration_test.go
index fa630f57..a25697aa 100644
--- a/ancestor/integration_test.go
+++ b/commitquery/ancestor_integration_test.go
@@ -1,14 +1,13 @@
-package ancestor_test
+package commitquery_test
import (
"errors"
"testing"
+ "codeberg.org/lindenii/furgit/commitquery"
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) {
@@ -34,7 +33,7 @@ func TestIsMatchesGitMergeBase(t *testing.T) {
store := testRepo.OpenObjectStore(t)
- got, err := ancestor.Is(store, nil, c1, tag)
+ got, err := commitquery.New(store, nil).IsAncestor(c1, tag)
if err != nil {
t.Fatalf("Is(c1, tag): %v", err)
}
@@ -44,7 +43,7 @@ func TestIsMatchesGitMergeBase(t *testing.T) {
t.Fatalf("Is(c1, tag)=%v, want %v", got, want)
}
- got, err = ancestor.Is(store, nil, c3, c2)
+ got, err = commitquery.New(store, nil).IsAncestor(c3, c2)
if err != nil {
t.Fatalf("Is(c3, c2): %v", err)
}
@@ -79,7 +78,7 @@ func TestIsMatchesGitMergeBaseWithCommitGraph(t *testing.T) {
store := testRepo.OpenObjectStore(t)
graph := testRepo.OpenCommitGraph(t)
- got, err := ancestor.Is(store, graph, c1, c2)
+ got, err := commitquery.New(store, graph).IsAncestor(c1, c2)
if err != nil {
t.Fatalf("Is(c1, c2): %v", err)
}
@@ -107,7 +106,7 @@ func TestIsMissingObject(t *testing.T) {
store := testRepo.OpenObjectStore(t)
- _, err := ancestor.Is(store, nil, treeID, commitID)
+ _, err := commitquery.New(store, nil).IsAncestor(treeID, commitID)
if err == nil {
t.Fatal("expected error")
}
diff --git a/ancestor/unit_test.go b/commitquery/ancestor_unit_test.go
index b6ca7b58..6edb05be 100644
--- a/ancestor/unit_test.go
+++ b/commitquery/ancestor_unit_test.go
@@ -1,4 +1,4 @@
-package ancestor_test
+package commitquery_test
import (
"errors"
@@ -12,11 +12,11 @@ import (
"codeberg.org/lindenii/furgit/objectstore/memory"
"codeberg.org/lindenii/furgit/objecttype"
- "codeberg.org/lindenii/furgit/ancestor"
+ "codeberg.org/lindenii/furgit/commitquery"
)
-// commitBody serializes one minimal commit body.
-func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte {
+// ancestorCommitBody serializes one minimal commit body.
+func ancestorCommitBody(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())...)
@@ -27,8 +27,8 @@ func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte {
return buf
}
-// tagBody serializes one minimal annotated tag body.
-func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte {
+// ancestorTagBody serializes one minimal annotated tag body.
+func ancestorTagBody(target objectid.ObjectID, targetType objecttype.Type) []byte {
targetName, ok := objecttype.Name(targetType)
if !ok {
panic("invalid tag target type")
@@ -37,8 +37,8 @@ func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte {
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 {
+// mustSerializeAncestorTree serializes one tree or fails the test.
+func mustSerializeAncestorTree(tb testing.TB, tree *object.Tree) []byte {
tb.Helper()
body, err := tree.SerializeWithoutHeader()
@@ -55,23 +55,23 @@ func TestIs(t *testing.T) {
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{{
+ tree := store.AddObject(objecttype.TypeTree, mustSerializeAncestorTree(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))
+ c1 := store.AddObject(objecttype.TypeCommit, ancestorCommitBody(tree))
+ c2 := store.AddObject(objecttype.TypeCommit, ancestorCommitBody(tree, c1))
otherBlob := store.AddObject(objecttype.TypeBlob, []byte("other-blob\n"))
- otherTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &object.Tree{Entries: []object.TreeEntry{{
+ otherTree := store.AddObject(objecttype.TypeTree, mustSerializeAncestorTree(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))
+ c3 := store.AddObject(objecttype.TypeCommit, ancestorCommitBody(otherTree))
+ tag := store.AddObject(objecttype.TypeTag, ancestorTagBody(c2, objecttype.TypeCommit))
- ok, err := ancestor.Is(store, nil, c1, tag)
+ ok, err := commitquery.New(store, nil).IsAncestor(c1, tag)
if err != nil {
t.Fatalf("Is(c1, tag): %v", err)
}
@@ -80,7 +80,7 @@ func TestIs(t *testing.T) {
t.Fatal("expected c1 to be ancestor of tag->c2")
}
- ok, err = ancestor.Is(store, nil, c3, c2)
+ ok, err = commitquery.New(store, nil).IsAncestor(c3, c2)
if err != nil {
t.Fatalf("Is(c3, c2): %v", err)
}
@@ -97,15 +97,15 @@ func TestIsRejectsNonCommitAfterPeel(t *testing.T) {
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{{
+ tree := store.AddObject(objecttype.TypeTree, mustSerializeAncestorTree(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))
+ commit := store.AddObject(objecttype.TypeCommit, ancestorCommitBody(tree))
+ tagToTree := store.AddObject(objecttype.TypeTag, ancestorTagBody(tree, objecttype.TypeTree))
- _, err := ancestor.Is(store, nil, commit, tagToTree)
+ _, err := commitquery.New(store, nil).IsAncestor(commit, tagToTree)
if err == nil {
t.Fatal("expected error")
}
diff --git a/internal/commitquery/bits.go b/commitquery/bits.go
index 36ffff29..36ffff29 100644
--- a/internal/commitquery/bits.go
+++ b/commitquery/bits.go
diff --git a/internal/commitquery/commit.go b/commitquery/commit.go
index fccd52b2..f4709270 100644
--- a/internal/commitquery/commit.go
+++ b/commitquery/commit.go
@@ -5,10 +5,10 @@ import (
"codeberg.org/lindenii/furgit/objectid"
)
-// Commit stores the metadata needed by commit-domain queries.
-type Commit struct {
+// commitData stores the metadata needed by commit-domain queries.
+type commitData struct {
ID objectid.ObjectID
- Parents []Parent
+ Parents []parentRef
CommitTime int64
Generation uint64
HasGeneration bool
diff --git a/commitquery/compare.go b/commitquery/compare.go
new file mode 100644
index 00000000..f64b5b3f
--- /dev/null
+++ b/commitquery/compare.go
@@ -0,0 +1,25 @@
+package commitquery
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// Compare compares two internal nodes using merge-base queue ordering.
+func (query *Query) compare(left, right nodeIndex) int {
+ leftGeneration := query.effectiveGeneration(left)
+ rightGeneration := query.effectiveGeneration(right)
+
+ switch {
+ case leftGeneration < rightGeneration:
+ return -1
+ case leftGeneration > rightGeneration:
+ return 1
+ }
+
+ switch {
+ case query.nodes[left].commitTime < query.nodes[right].commitTime:
+ return -1
+ case query.nodes[left].commitTime > query.nodes[right].commitTime:
+ return 1
+ }
+
+ return objectid.Compare(query.nodes[left].id, query.nodes[right].id)
+}
diff --git a/commitquery/context.go b/commitquery/context.go
new file mode 100644
index 00000000..8e19138e
--- /dev/null
+++ b/commitquery/context.go
@@ -0,0 +1,34 @@
+// Package commitquery answers commit ancestry and merge-base queries.
+package commitquery
+
+import (
+ commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstore"
+)
+
+// Query owns the mutable node arena for commit-domain queries over one object
+// store.
+type Query struct {
+ store objectstore.Store
+ graph *commitgraphread.Reader
+
+ nodes []node
+
+ byOID map[objectid.ObjectID]nodeIndex
+ byGraphPos map[commitgraphread.Position]nodeIndex
+
+ markPhase uint32
+ touched []nodeIndex
+}
+
+// New builds one reusable commit query arena over one object store and optional
+// commit-graph reader.
+func New(store objectstore.Store, graph *commitgraphread.Reader) *Query {
+ return &Query{
+ store: store,
+ graph: graph,
+ byOID: make(map[objectid.ObjectID]nodeIndex),
+ byGraphPos: make(map[commitgraphread.Position]nodeIndex),
+ }
+}
diff --git a/internal/commitquery/errors.go b/commitquery/errors.go
index e99011d0..e99011d0 100644
--- a/internal/commitquery/errors.go
+++ b/commitquery/errors.go
diff --git a/commitquery/generation.go b/commitquery/generation.go
new file mode 100644
index 00000000..05228b29
--- /dev/null
+++ b/commitquery/generation.go
@@ -0,0 +1,43 @@
+package commitquery
+
+import (
+ "math"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// EffectiveGeneration returns one node's generation value.
+func (query *Query) effectiveGeneration(idx nodeIndex) uint64 {
+ if !query.nodes[idx].hasGeneration {
+ return generationInfinity
+ }
+
+ return query.nodes[idx].generation
+}
+
+const (
+ generationInfinity = uint64(math.MaxUint64)
+)
+
+func compareByGeneration(query *Query) func(nodeIndex, nodeIndex) int {
+ return func(left, right nodeIndex) int {
+ leftGeneration := query.effectiveGeneration(left)
+ rightGeneration := query.effectiveGeneration(right)
+
+ switch {
+ case leftGeneration < rightGeneration:
+ return -1
+ case leftGeneration > rightGeneration:
+ return 1
+ }
+
+ switch {
+ case query.nodes[left].commitTime < query.nodes[right].commitTime:
+ return -1
+ case query.nodes[left].commitTime > query.nodes[right].commitTime:
+ return 1
+ }
+
+ return objectid.Compare(query.nodes[left].id, query.nodes[right].id)
+ }
+}
diff --git a/commitquery/graph_pos.go b/commitquery/graph_pos.go
new file mode 100644
index 00000000..b1d27129
--- /dev/null
+++ b/commitquery/graph_pos.go
@@ -0,0 +1,107 @@
+package commitquery
+
+import commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
+
+// resolveGraphPos resolves one commit-graph position to one internal query node.
+func (query *Query) resolveGraphPos(pos commitgraphread.Position) (nodeIndex, error) {
+ idx, ok := query.byGraphPos[pos]
+ if ok {
+ err := query.ensureLoaded(idx)
+ if err != nil {
+ return 0, err
+ }
+
+ return idx, nil
+ }
+
+ commit, err := query.graph.CommitAt(pos)
+ if err != nil {
+ return 0, err
+ }
+
+ idx, ok = query.byOID[commit.OID]
+ if !ok {
+ idx = query.newNode(commit.OID)
+ query.byOID[commit.OID] = idx
+ }
+
+ query.byGraphPos[pos] = idx
+ query.nodes[idx].graphPos = pos
+ query.nodes[idx].hasGraphPos = true
+
+ err = query.loadCommitAtGraphPos(idx, pos)
+ if err != nil {
+ delete(query.byGraphPos, pos)
+
+ return 0, err
+ }
+
+ return idx, nil
+}
+
+// loadByGraphPos populates one node from a commit-graph position.
+func (query *Query) loadByGraphPos(idx nodeIndex) error {
+ pos := query.nodes[idx].graphPos
+
+ return query.loadCommitAtGraphPos(idx, pos)
+}
+
+func (query *Query) loadCommitAtGraphPos(idx nodeIndex, pos commitgraphread.Position) error {
+ commit, err := query.graph.CommitAt(pos)
+ if err != nil {
+ return err
+ }
+
+ parents := make([]parentRef, 0, 2+len(commit.ExtraParents))
+
+ if commit.Parent1.Valid {
+ parentOID, err := query.graph.OIDAt(commit.Parent1.Pos)
+ if err != nil {
+ return err
+ }
+
+ parents = append(parents, parentRef{
+ ID: parentOID,
+ GraphPos: commit.Parent1.Pos,
+ HasGraphPos: true,
+ })
+ }
+
+ if commit.Parent2.Valid {
+ parentOID, err := query.graph.OIDAt(commit.Parent2.Pos)
+ if err != nil {
+ return err
+ }
+
+ parents = append(parents, parentRef{
+ ID: parentOID,
+ GraphPos: commit.Parent2.Pos,
+ HasGraphPos: true,
+ })
+ }
+
+ for _, parentPos := range commit.ExtraParents {
+ parentOID, err := query.graph.OIDAt(parentPos)
+ if err != nil {
+ return err
+ }
+
+ parents = append(parents, parentRef{
+ ID: parentOID,
+ GraphPos: parentPos,
+ HasGraphPos: true,
+ })
+ }
+
+ data := commitData{
+ ID: commit.OID,
+ Parents: parents,
+ CommitTime: commit.CommitTimeUnix,
+ Generation: commit.GenerationV2,
+ HasGeneration: commit.GenerationV2 != 0,
+ GraphPos: pos,
+ HasGraphPos: true,
+ }
+
+ return query.populateNode(idx, data)
+}
diff --git a/commitquery/load.go b/commitquery/load.go
new file mode 100644
index 00000000..58ed7bf3
--- /dev/null
+++ b/commitquery/load.go
@@ -0,0 +1,14 @@
+package commitquery
+
+// ensureLoaded completes one node's metadata load if it has not been loaded yet.
+func (query *Query) ensureLoaded(idx nodeIndex) error {
+ if query.nodes[idx].loaded {
+ return nil
+ }
+
+ if query.nodes[idx].hasGraphPos {
+ return query.loadByGraphPos(idx)
+ }
+
+ return query.loadByOID(idx)
+}
diff --git a/commitquery/marks.go b/commitquery/marks.go
new file mode 100644
index 00000000..de008789
--- /dev/null
+++ b/commitquery/marks.go
@@ -0,0 +1,71 @@
+package commitquery
+
+// Marks returns the mark bits of one internal node.
+func (query *Query) marks(idx nodeIndex) markBits {
+ return query.nodes[idx].marks
+}
+
+// HasAnyMarks reports whether one internal node has any requested bit.
+func (query *Query) hasAnyMarks(idx nodeIndex, bits markBits) bool {
+ return query.nodes[idx].marks&bits != 0
+}
+
+// HasAllMarks reports whether one internal node already has all requested bits.
+func (query *Query) hasAllMarks(idx nodeIndex, bits markBits) bool {
+ return query.nodes[idx].marks&bits == bits
+}
+
+// SetMarks ORs one set of mark bits into one internal node.
+func (query *Query) setMarks(idx nodeIndex, bits markBits) {
+ newBits := bits &^ query.nodes[idx].marks
+ if newBits == 0 {
+ return
+ }
+
+ query.trackTouched(idx)
+ query.nodes[idx].marks |= bits
+}
+
+// ClearMarks removes one set of mark bits from one internal node.
+func (query *Query) clearMarks(idx nodeIndex, bits markBits) {
+ if query.nodes[idx].marks&bits == 0 {
+ return
+ }
+
+ query.trackTouched(idx)
+ query.nodes[idx].marks &^= bits
+}
+
+// BeginMarkPhase starts one tracked mark-mutation phase.
+func (query *Query) beginMarkPhase() {
+ for _, idx := range query.touched {
+ query.nodes[idx].marks = 0
+ }
+
+ query.markPhase++
+ if query.markPhase == 0 {
+ query.markPhase++
+ for i := range query.nodes {
+ query.nodes[i].touchedPhase = 0
+ }
+ }
+
+ query.touched = query.touched[:0]
+}
+
+// ClearTouchedMarks clears the provided bits from all nodes touched in the
+// current mark phase.
+func (query *Query) clearTouchedMarks(bits markBits) {
+ for _, idx := range query.touched {
+ query.nodes[idx].marks &^= bits
+ }
+}
+
+func (query *Query) trackTouched(idx nodeIndex) {
+ if query.nodes[idx].touchedPhase == query.markPhase {
+ return
+ }
+
+ query.nodes[idx].touchedPhase = query.markPhase
+ query.touched = append(query.touched, idx)
+}
diff --git a/commitquery/merge_bases.go b/commitquery/merge_bases.go
new file mode 100644
index 00000000..c63bb82f
--- /dev/null
+++ b/commitquery/merge_bases.go
@@ -0,0 +1,171 @@
+package commitquery
+
+import (
+ "slices"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// MergeBases reports all merge bases in Git's merge-base --all order.
+//
+// Both inputs are peeled through annotated tags before commit traversal.
+func (query *Query) MergeBases(left, right objectid.ObjectID) ([]objectid.ObjectID, error) {
+ leftIdx, err := query.resolveCommitish(left)
+ if err != nil {
+ return nil, err
+ }
+
+ rightIdx, err := query.resolveCommitish(right)
+ if err != nil {
+ return nil, err
+ }
+
+ candidates, err := query.mergeBases(leftIdx, rightIdx)
+ if err != nil {
+ return nil, err
+ }
+
+ slices.SortFunc(candidates, func(left, right nodeIndex) int {
+ switch {
+ case query.commitTime(left) > query.commitTime(right):
+ return -1
+ case query.commitTime(left) < query.commitTime(right):
+ return 1
+ default:
+ return objectid.Compare(query.id(left), query.id(right))
+ }
+ })
+
+ out := make([]objectid.ObjectID, 0, len(candidates))
+ for _, idx := range candidates {
+ out = append(out, query.id(idx))
+ }
+
+ return out, nil
+}
+
+// MergeBase reports one merge base between left and right, if any.
+func (query *Query) MergeBase(left, right objectid.ObjectID) (objectid.ObjectID, bool, error) {
+ bases, err := query.MergeBases(left, right)
+ if err != nil {
+ return objectid.ObjectID{}, false, err
+ }
+
+ if len(bases) == 0 {
+ return objectid.ObjectID{}, false, nil
+ }
+
+ return bases[0], true, nil
+}
+
+func (query *Query) mergeBases(left, right nodeIndex) ([]nodeIndex, error) {
+ if left == right {
+ return []nodeIndex{left}, nil
+ }
+
+ err := query.paintDownToCommon(left, []nodeIndex{right}, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ candidates := query.collectMarkedResults()
+
+ if len(candidates) <= 1 {
+ slices.SortFunc(candidates, query.compare)
+
+ return candidates, nil
+ }
+
+ query.clearTouchedMarks(allMarks)
+
+ reduced, err := removeRedundant(query, candidates)
+ if err != nil {
+ return nil, err
+ }
+
+ slices.SortFunc(reduced, query.compare)
+
+ return reduced, nil
+}
+
+func (query *Query) paintDownToCommon(left nodeIndex, rights []nodeIndex, minGeneration uint64) error {
+ query.beginMarkPhase()
+
+ query.setMarks(left, markLeft)
+
+ if len(rights) == 0 {
+ query.setMarks(left, markResult)
+
+ return nil
+ }
+
+ queue := newPriorityQueue(query)
+ queue.PushNode(left)
+
+ for _, right := range rights {
+ query.setMarks(right, markRight)
+ queue.PushNode(right)
+ }
+
+ lastGeneration := generationInfinity
+
+ for query.queueHasNonStale(queue) {
+ idx := queue.PopNode()
+
+ generation := query.effectiveGeneration(idx)
+ if generation > lastGeneration {
+ return errBadGenerationOrder
+ }
+
+ lastGeneration = generation
+ if generation < minGeneration {
+ break
+ }
+
+ flags := query.marks(idx) & (markLeft | markRight | markStale)
+ if flags == (markLeft | markRight) {
+ query.setMarks(idx, markResult)
+
+ flags |= markStale
+ }
+
+ for _, parent := range query.parents(idx) {
+ if query.hasAllMarks(parent, flags) {
+ continue
+ }
+
+ query.setMarks(parent, flags)
+ queue.PushNode(parent)
+ }
+ }
+
+ return nil
+}
+
+func (query *Query) queueHasNonStale(queue *priorityQueue) bool {
+ for _, idx := range queue.items {
+ if !query.hasAnyMarks(idx, markStale) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (query *Query) collectMarkedResults() []nodeIndex {
+ out := make([]nodeIndex, 0, 4)
+
+ for _, idx := range query.touched {
+ if !query.hasAnyMarks(idx, markResult) {
+ continue
+ }
+
+ if query.hasAnyMarks(idx, markStale) {
+ continue
+ }
+
+ out = append(out, idx)
+ }
+
+ return out
+}
diff --git a/mergebase/integration_test.go b/commitquery/mergebase_integration_test.go
index 3d85409d..30477454 100644
--- a/mergebase/integration_test.go
+++ b/commitquery/mergebase_integration_test.go
@@ -1,4 +1,4 @@
-package mergebase_test
+package commitquery_test
import (
"maps"
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
+ "codeberg.org/lindenii/furgit/commitquery"
"codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/mergebase"
"codeberg.org/lindenii/furgit/objectid"
)
@@ -34,9 +34,9 @@ func TestQueryMatchesGitMergeBaseAll(t *testing.T) {
store := testRepo.OpenObjectStore(t)
- query := mergebase.Query(store, nil, left, tag)
+ query := commitquery.New(store, nil)
- all, err := query.All()
+ all, err := query.MergeBases(left, tag)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -77,9 +77,9 @@ func TestQueryCrissCrossMatchesGitMergeBaseAll(t *testing.T) {
store := testRepo.OpenObjectStore(t)
- query := mergebase.Query(store, nil, left, right)
+ query := commitquery.New(store, nil)
- all, err := query.All()
+ all, err := query.MergeBases(left, right)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -91,7 +91,7 @@ func TestQueryCrissCrossMatchesGitMergeBaseAll(t *testing.T) {
t.Fatalf("Query(left, right) mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want))
}
- first, ok, err := mergebase.Base(store, nil, left, right)
+ first, ok, err := query.MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right): %v", err)
}
@@ -138,9 +138,9 @@ func TestQueryMatchesGitMergeBaseAllWithCommitGraph(t *testing.T) {
store := testRepo.OpenObjectStore(t)
graph := testRepo.OpenCommitGraph(t)
- query := mergebase.Query(store, graph, left, right)
+ query := commitquery.New(store, graph)
- all, err := query.All()
+ all, err := query.MergeBases(left, right)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -152,7 +152,7 @@ func TestQueryMatchesGitMergeBaseAllWithCommitGraph(t *testing.T) {
t.Fatalf("Query(left, right) with commit-graph mismatch:\n got=%v\nwant=%v", sortedOIDStrings(got), sortedOIDStrings(want))
}
- first, ok, err := mergebase.Base(store, graph, left, right)
+ first, ok, err := query.MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right): %v", err)
}
@@ -200,7 +200,9 @@ func TestBaseMatchesGitMergeBaseWithoutAll(t *testing.T) {
store := testRepo.OpenObjectStore(t)
- got, ok, err := mergebase.Base(store, nil, left, right)
+ query := commitquery.New(store, nil)
+
+ got, ok, err := query.MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right): %v", err)
}
@@ -220,7 +222,7 @@ func TestBaseMatchesGitMergeBaseWithoutAll(t *testing.T) {
graph := testRepo.OpenCommitGraph(t)
- got, ok, err = mergebase.Base(store, graph, left, right)
+ got, ok, err = commitquery.New(store, graph).MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right) with commit-graph: %v", err)
}
diff --git a/mergebase/unit_test.go b/commitquery/mergebase_unit_test.go
index 7ba1ed66..daa3c3c6 100644
--- a/mergebase/unit_test.go
+++ b/commitquery/mergebase_unit_test.go
@@ -1,4 +1,4 @@
-package mergebase_test
+package commitquery_test
import (
"errors"
@@ -7,9 +7,9 @@ import (
"slices"
"testing"
+ "codeberg.org/lindenii/furgit/commitquery"
giterrors "codeberg.org/lindenii/furgit/errors"
"codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/mergebase"
"codeberg.org/lindenii/furgit/object"
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/objectstore/memory"
@@ -83,9 +83,9 @@ func TestQueryLinearHistory(t *testing.T) {
left := store.AddObject(objecttype.TypeCommit, commitBody(tree, base))
right := store.AddObject(objecttype.TypeCommit, commitBody(tree, left))
- query := mergebase.Query(store, nil, left, right)
+ query := commitquery.New(store, nil)
- got, err := query.All()
+ got, err := query.MergeBases(left, right)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -94,7 +94,7 @@ func TestQueryLinearHistory(t *testing.T) {
t.Fatalf("Query(left, right)=%v, want [%s]", got, left)
}
- first, ok, err := mergebase.Base(store, nil, left, right)
+ first, ok, err := query.MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right): %v", err)
}
@@ -130,9 +130,9 @@ func TestQueryPeelsAnnotatedTags(t *testing.T) {
right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree, base))
tag := store.AddObject(objecttype.TypeTag, tagBody(right, objecttype.TypeCommit))
- query := mergebase.Query(store, nil, left, tag)
+ query := commitquery.New(store, nil)
- got, err := query.All()
+ got, err := query.MergeBases(left, tag)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -180,9 +180,9 @@ func TestQueryCrissCrossReturnsAllBestCommonAncestors(t *testing.T) {
left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree, base1, base2))
right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree, base2, base1))
- query := mergebase.Query(store, nil, left, right)
+ query := commitquery.New(store, nil)
- all, err := query.All()
+ all, err := query.MergeBases(left, right)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -194,7 +194,7 @@ func TestQueryCrissCrossReturnsAllBestCommonAncestors(t *testing.T) {
t.Fatalf("Query(left, right)=%v, want %v", slices.Collect(maps.Keys(got)), slices.Collect(maps.Keys(want)))
}
- first, ok, err := mergebase.Base(store, nil, left, right)
+ first, ok, err := query.MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right): %v", err)
}
@@ -229,9 +229,9 @@ func TestQueryReturnsNoResultWhenNoCommonAncestorExists(t *testing.T) {
left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree))
right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree))
- query := mergebase.Query(store, nil, left, right)
+ query := commitquery.New(store, nil)
- got, err := query.All()
+ got, err := query.MergeBases(left, right)
if err != nil {
t.Fatalf("query.All(): %v", err)
}
@@ -240,7 +240,7 @@ func TestQueryReturnsNoResultWhenNoCommonAncestorExists(t *testing.T) {
t.Fatalf("Query(left, right)=%v, want no results", got)
}
- _, ok, err := mergebase.Base(store, nil, left, right)
+ _, ok, err := query.MergeBase(left, right)
if err != nil {
t.Fatalf("Base(left, right): %v", err)
}
@@ -265,9 +265,9 @@ func TestQueryRejectsNonCommitAfterPeel(t *testing.T) {
commit := store.AddObject(objecttype.TypeCommit, commitBody(tree))
tagToTree := store.AddObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree))
- query := mergebase.Query(store, nil, commit, tagToTree)
+ query := commitquery.New(store, nil)
- _, err := query.All()
+ _, err := query.MergeBases(commit, tagToTree)
if err == nil {
t.Fatal("expected error")
}
@@ -298,16 +298,16 @@ func TestQueryAllIsRepeatable(t *testing.T) {
left := store.AddObject(objecttype.TypeCommit, commitBody(tree, base))
right := store.AddObject(objecttype.TypeCommit, commitBody(tree, left))
- query := mergebase.Query(store, nil, left, right)
+ query := commitquery.New(store, nil)
- first, err := query.All()
+ first, err := query.MergeBases(left, right)
if err != nil {
- t.Fatalf("query.All() first call: %v", err)
+ t.Fatalf("query.MergeBases() first call: %v", err)
}
- again, err := query.All()
+ again, err := query.MergeBases(left, right)
if err != nil {
- t.Fatalf("query.All() second call: %v", err)
+ t.Fatalf("query.MergeBases() second call: %v", err)
}
if !slices.Equal(again, first) {
@@ -315,18 +315,18 @@ func TestQueryAllIsRepeatable(t *testing.T) {
}
if len(first) == 0 {
- t.Fatal("first All() unexpectedly returned no results")
+ t.Fatal("first MergeBases() unexpectedly returned no results")
}
first[0] = objectid.ObjectID{}
- third, err := query.All()
+ third, err := query.MergeBases(left, right)
if err != nil {
- t.Fatalf("query.All() third call: %v", err)
+ t.Fatalf("query.MergeBases() third call: %v", err)
}
if third[0] == (objectid.ObjectID{}) {
- t.Fatal("query.All() exposed internal slice state")
+ t.Fatal("query.MergeBases() exposed internal slice state")
}
})
}
diff --git a/internal/commitquery/node.go b/commitquery/node.go
index f4979bfe..cd357631 100644
--- a/internal/commitquery/node.go
+++ b/commitquery/node.go
@@ -5,14 +5,14 @@ import (
"codeberg.org/lindenii/furgit/objectid"
)
-// NodeIndex identifies one internal query node.
-type NodeIndex int
+// nodeIndex identifies one internal query node.
+type nodeIndex int
// node stores one mutable commit traversal node.
type node struct {
id objectid.ObjectID
- parents []NodeIndex
+ parents []nodeIndex
commitTime int64
generation uint64
@@ -28,12 +28,12 @@ type node struct {
}
// newNode allocates one empty internal node.
-func (ctx *Context) newNode(id objectid.ObjectID) NodeIndex {
- count := len(ctx.nodes)
+func (query *Query) newNode(id objectid.ObjectID) nodeIndex {
+ count := len(query.nodes)
- idx := NodeIndex(count)
+ idx := nodeIndex(count)
- ctx.nodes = append(ctx.nodes, node{id: id})
+ query.nodes = append(query.nodes, node{id: id})
return idx
}
diff --git a/internal/commitquery/oid.go b/commitquery/oid.go
index 927c0220..68adbf5d 100644
--- a/internal/commitquery/oid.go
+++ b/commitquery/oid.go
@@ -5,27 +5,25 @@ import (
commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
giterrors "codeberg.org/lindenii/furgit/errors"
+ "codeberg.org/lindenii/furgit/internal/peel"
"codeberg.org/lindenii/furgit/object"
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/objectstore"
"codeberg.org/lindenii/furgit/objecttype"
)
-// ID returns the canonical object ID of one internal node.
-func (ctx *Context) ID(idx NodeIndex) objectid.ObjectID {
- return ctx.nodes[idx].id
+func (query *Query) id(idx nodeIndex) objectid.ObjectID {
+ return query.nodes[idx].id
}
-// CommitTime returns the committer timestamp used for one internal node.
-func (ctx *Context) CommitTime(idx NodeIndex) int64 {
- return ctx.nodes[idx].commitTime
+func (query *Query) commitTime(idx nodeIndex) int64 {
+ return query.nodes[idx].commitTime
}
-// ResolveOID resolves one commit object ID to one internal query node.
-func (ctx *Context) ResolveOID(id objectid.ObjectID) (NodeIndex, error) {
- idx, ok := ctx.byOID[id]
+func (query *Query) resolveOID(id objectid.ObjectID) (nodeIndex, error) {
+ idx, ok := query.byOID[id]
if ok {
- err := ctx.ensureLoaded(idx)
+ err := query.ensureLoaded(idx)
if err != nil {
return 0, err
}
@@ -33,12 +31,12 @@ func (ctx *Context) ResolveOID(id objectid.ObjectID) (NodeIndex, error) {
return idx, nil
}
- idx = ctx.newNode(id)
- ctx.byOID[id] = idx
+ idx = query.newNode(id)
+ query.byOID[id] = idx
- err := ctx.loadByOID(idx)
+ err := query.loadByOID(idx)
if err != nil {
- delete(ctx.byOID, id)
+ delete(query.byOID, id)
return 0, err
}
@@ -46,22 +44,31 @@ func (ctx *Context) ResolveOID(id objectid.ObjectID) (NodeIndex, error) {
return idx, nil
}
+func (query *Query) resolveCommitish(id objectid.ObjectID) (nodeIndex, error) {
+ commitID, err := peel.ToCommit(query.store, id)
+ if err != nil {
+ return 0, err
+ }
+
+ return query.resolveOID(commitID)
+}
+
// loadByOID populates one node from an object ID.
-func (ctx *Context) loadByOID(idx NodeIndex) error {
- id := ctx.nodes[idx].id
+func (query *Query) loadByOID(idx nodeIndex) error {
+ id := query.nodes[idx].id
- if ctx.graph != nil {
- pos, err := ctx.graph.Lookup(id)
+ if query.graph != nil {
+ pos, err := query.graph.Lookup(id)
if err != nil {
if _, ok := stderrors.AsType[*commitgraphread.NotFoundError](err); !ok {
return err
}
} else {
- return ctx.loadCommitAtGraphPos(idx, pos)
+ return query.loadCommitAtGraphPos(idx, pos)
}
}
- ty, content, err := ctx.store.ReadBytesContent(id)
+ ty, content, err := query.store.ReadBytesContent(id)
if err != nil {
if stderrors.Is(err, objectstore.ErrObjectNotFound) {
return &giterrors.ObjectMissingError{OID: id}
@@ -79,16 +86,16 @@ func (ctx *Context) loadByOID(idx NodeIndex) error {
return err
}
- parents := make([]Parent, 0, len(commitObj.Parents))
+ parents := make([]parentRef, 0, len(commitObj.Parents))
for _, parentID := range commitObj.Parents {
- parents = append(parents, Parent{ID: parentID})
+ parents = append(parents, parentRef{ID: parentID})
}
- commit := Commit{
+ commit := commitData{
ID: id,
Parents: parents,
CommitTime: commitObj.Committer.WhenUnix,
}
- return ctx.populateNode(idx, commit)
+ return query.populateNode(idx, commit)
}
diff --git a/internal/commitquery/parent.go b/commitquery/parent.go
index 809a1fd5..1c59e102 100644
--- a/internal/commitquery/parent.go
+++ b/commitquery/parent.go
@@ -5,23 +5,23 @@ import (
"codeberg.org/lindenii/furgit/objectid"
)
-// Parent references one commit parent.
-type Parent struct {
+// parentRef references one commit parent.
+type parentRef struct {
ID objectid.ObjectID
GraphPos commitgraphread.Position
HasGraphPos bool
}
// Parents returns resolved parent node indices for one internal node.
-func (ctx *Context) Parents(idx NodeIndex) []NodeIndex {
- return ctx.nodes[idx].parents
+func (query *Query) parents(idx nodeIndex) []nodeIndex {
+ return query.nodes[idx].parents
}
// resolveParent resolves one parent descriptor to one internal node.
-func (ctx *Context) resolveParent(parent Parent) (NodeIndex, error) {
+func (query *Query) resolveParent(parent parentRef) (nodeIndex, error) {
if parent.HasGraphPos {
- return ctx.ResolveGraphPos(parent.GraphPos)
+ return query.resolveGraphPos(parent.GraphPos)
}
- return ctx.ResolveOID(parent.ID)
+ return query.resolveOID(parent.ID)
}
diff --git a/commitquery/populate.go b/commitquery/populate.go
new file mode 100644
index 00000000..cc71955f
--- /dev/null
+++ b/commitquery/populate.go
@@ -0,0 +1,42 @@
+package commitquery
+
+import "fmt"
+
+// populateNode fills one node's metadata and resolves its parents.
+func (query *Query) populateNode(idx nodeIndex, commit commitData) error {
+ if query.nodes[idx].loaded {
+ if query.nodes[idx].id != commit.ID {
+ return fmt.Errorf("commitquery: node identity mismatch: have %s, got %s", query.nodes[idx].id, commit.ID)
+ }
+
+ return nil
+ }
+
+ query.nodes[idx].id = commit.ID
+ query.nodes[idx].commitTime = commit.CommitTime
+ query.nodes[idx].generation = commit.Generation
+ query.nodes[idx].hasGeneration = commit.HasGeneration
+
+ if commit.HasGraphPos {
+ query.nodes[idx].graphPos = commit.GraphPos
+ query.nodes[idx].hasGraphPos = true
+ query.byGraphPos[commit.GraphPos] = idx
+ }
+
+ query.nodes[idx].loaded = true
+ query.nodes[idx].parents = query.nodes[idx].parents[:0]
+
+ for _, parent := range commit.Parents {
+ parentIdx, err := query.resolveParent(parent)
+ if err != nil {
+ query.nodes[idx].loaded = false
+ query.nodes[idx].parents = nil
+
+ return err
+ }
+
+ query.nodes[idx].parents = append(query.nodes[idx].parents, parentIdx)
+ }
+
+ return nil
+}
diff --git a/internal/commitquery/priority_queue.go b/commitquery/priority_queue.go
index d6dbf54f..0ca57f7d 100644
--- a/internal/commitquery/priority_queue.go
+++ b/commitquery/priority_queue.go
@@ -4,13 +4,13 @@ import "container/heap"
// priorityQueue orders internal nodes using one query context's comparator.
type priorityQueue struct {
- ctx *Context
- items []NodeIndex
+ query *Query
+ items []nodeIndex
}
// newPriorityQueue builds one empty priority queue over one query context.
-func newPriorityQueue(ctx *Context) *priorityQueue {
- queue := &priorityQueue{ctx: ctx}
+func newPriorityQueue(query *Query) *priorityQueue {
+ queue := &priorityQueue{query: query}
heap.Init(queue)
return queue
@@ -23,7 +23,7 @@ func (queue *priorityQueue) Len() int {
// Less reports whether one heap slot sorts ahead of another.
func (queue *priorityQueue) Less(left, right int) bool {
- return queue.ctx.Compare(queue.items[left], queue.items[right]) > 0
+ return queue.query.compare(queue.items[left], queue.items[right]) > 0
}
// Swap exchanges two heap slots.
@@ -33,9 +33,9 @@ func (queue *priorityQueue) Swap(left, right int) {
// Push appends one heap element.
func (queue *priorityQueue) Push(item any) {
- idx, ok := item.(NodeIndex)
+ idx, ok := item.(nodeIndex)
if !ok {
- panic("commitquery: heap push item is not a NodeIndex")
+ panic("commitquery: heap push item is not a nodeIndex")
}
queue.items = append(queue.items, idx)
@@ -51,17 +51,17 @@ func (queue *priorityQueue) Pop() any {
}
// PushNode inserts one internal node.
-func (queue *priorityQueue) PushNode(idx NodeIndex) {
+func (queue *priorityQueue) PushNode(idx nodeIndex) {
heap.Push(queue, idx)
}
// PopNode removes the highest-priority internal node.
-func (queue *priorityQueue) PopNode() NodeIndex {
+func (queue *priorityQueue) PopNode() nodeIndex {
item := heap.Pop(queue)
- idx, ok := item.(NodeIndex)
+ idx, ok := item.(nodeIndex)
if !ok {
- panic("commitquery: heap pop item is not a NodeIndex")
+ panic("commitquery: heap pop item is not a nodeIndex")
}
return idx
diff --git a/commitquery/reduce.go b/commitquery/reduce.go
new file mode 100644
index 00000000..ac85d619
--- /dev/null
+++ b/commitquery/reduce.go
@@ -0,0 +1,166 @@
+package commitquery
+
+import (
+ "slices"
+)
+
+// removeRedundant removes redundant merge-base candidates.
+func removeRedundant(query *Query, candidates []nodeIndex) ([]nodeIndex, error) {
+ for _, idx := range candidates {
+ if query.effectiveGeneration(idx) != generationInfinity {
+ return removeRedundantWithGen(query, candidates), nil
+ }
+ }
+
+ return removeRedundantNoGen(query, candidates)
+}
+
+func removeRedundantNoGen(query *Query, candidates []nodeIndex) ([]nodeIndex, error) {
+ redundant := make([]bool, len(candidates))
+ work := make([]nodeIndex, 0, len(candidates)-1)
+ filledIndex := make([]int, 0, len(candidates)-1)
+
+ for i, candidate := range candidates {
+ if redundant[i] {
+ continue
+ }
+
+ work = work[:0]
+ filledIndex = filledIndex[:0]
+
+ minGeneration := query.effectiveGeneration(candidate)
+
+ for j, other := range candidates {
+ if i == j || redundant[j] {
+ continue
+ }
+
+ work = append(work, other)
+ filledIndex = append(filledIndex, j)
+
+ otherGeneration := query.effectiveGeneration(other)
+ if otherGeneration < minGeneration {
+ minGeneration = otherGeneration
+ }
+ }
+
+ err := query.paintDownToCommon(candidate, work, minGeneration)
+ if err != nil {
+ return nil, err
+ }
+
+ if query.hasAnyMarks(candidate, markRight) {
+ redundant[i] = true
+ }
+
+ for j, other := range work {
+ if query.hasAnyMarks(other, markLeft) {
+ redundant[filledIndex[j]] = true
+ }
+ }
+
+ query.clearTouchedMarks(allMarks)
+ }
+
+ out := make([]nodeIndex, 0, len(candidates))
+ for i, idx := range candidates {
+ if !redundant[i] {
+ out = append(out, idx)
+ }
+ }
+
+ return out, nil
+}
+
+func removeRedundantWithGen(query *Query, candidates []nodeIndex) []nodeIndex {
+ sorted := append([]nodeIndex(nil), candidates...)
+ slices.SortFunc(sorted, compareByGeneration(query))
+
+ minGeneration := query.effectiveGeneration(sorted[0])
+ minGenPos := 0
+ countStillIndependent := len(candidates)
+
+ query.beginMarkPhase()
+
+ walkStart := make([]nodeIndex, 0, len(candidates)*2)
+
+ for _, idx := range candidates {
+ query.setMarks(idx, markResult)
+
+ for _, parent := range query.parents(idx) {
+ if query.hasAnyMarks(parent, markStale) {
+ continue
+ }
+
+ query.setMarks(parent, markStale)
+ walkStart = append(walkStart, parent)
+ }
+ }
+
+ slices.SortFunc(walkStart, compareByGeneration(query))
+
+ for _, idx := range walkStart {
+ query.clearMarks(idx, markStale)
+ }
+
+ for i := len(walkStart) - 1; i >= 0 && countStillIndependent > 1; i-- {
+ stack := []nodeIndex{walkStart[i]}
+ query.setMarks(walkStart[i], markStale)
+
+ for len(stack) > 0 {
+ top := stack[len(stack)-1]
+
+ if query.hasAnyMarks(top, markResult) {
+ query.clearMarks(top, markResult)
+
+ countStillIndependent--
+ if countStillIndependent <= 1 {
+ break
+ }
+
+ if top == sorted[minGenPos] {
+ for minGenPos < len(sorted)-1 && query.hasAnyMarks(sorted[minGenPos], markStale) {
+ minGenPos++
+ }
+
+ minGeneration = query.effectiveGeneration(sorted[minGenPos])
+ }
+ }
+
+ if query.effectiveGeneration(top) < minGeneration {
+ stack = stack[:len(stack)-1]
+
+ continue
+ }
+
+ pushed := false
+
+ for _, parent := range query.parents(top) {
+ if query.hasAnyMarks(parent, markStale) {
+ continue
+ }
+
+ query.setMarks(parent, markStale)
+ stack = append(stack, parent)
+ pushed = true
+
+ break
+ }
+
+ if !pushed {
+ stack = stack[:len(stack)-1]
+ }
+ }
+ }
+
+ out := make([]nodeIndex, 0, len(candidates))
+ for _, idx := range candidates {
+ if !query.hasAnyMarks(idx, markStale) {
+ out = append(out, idx)
+ }
+ }
+
+ query.clearTouchedMarks(markStale | markResult)
+
+ return out
+}
diff --git a/internal/commitquery/ancestor.go b/internal/commitquery/ancestor.go
deleted file mode 100644
index d050ce08..00000000
--- a/internal/commitquery/ancestor.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package commitquery
-
-// IsAncestor reports whether ancestor is reachable from descendant through
-// commit parent edges.
-func IsAncestor(ctx *Context, ancestor, descendant NodeIndex) (bool, error) {
- if ancestor == descendant {
- return true, nil
- }
-
- ancestorGeneration := ctx.EffectiveGeneration(ancestor)
- descendantGeneration := ctx.EffectiveGeneration(descendant)
-
- if ancestorGeneration != generationInfinity &&
- descendantGeneration != generationInfinity &&
- ancestorGeneration > descendantGeneration {
- return false, nil
- }
-
- minGeneration := uint64(0)
- if ancestorGeneration != generationInfinity {
- minGeneration = ancestorGeneration
- }
-
- err := paintDownToCommon(ctx, ancestor, []NodeIndex{descendant}, minGeneration)
- if err != nil {
- return false, err
- }
-
- return ctx.HasAnyMarks(ancestor, markRight), nil
-}
diff --git a/internal/commitquery/compare.go b/internal/commitquery/compare.go
deleted file mode 100644
index 748ef712..00000000
--- a/internal/commitquery/compare.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package commitquery
-
-import "codeberg.org/lindenii/furgit/objectid"
-
-// Compare compares two internal nodes using merge-base queue ordering.
-func (ctx *Context) Compare(left, right NodeIndex) int {
- leftGeneration := ctx.EffectiveGeneration(left)
- rightGeneration := ctx.EffectiveGeneration(right)
-
- switch {
- case leftGeneration < rightGeneration:
- return -1
- case leftGeneration > rightGeneration:
- return 1
- }
-
- switch {
- case ctx.nodes[left].commitTime < ctx.nodes[right].commitTime:
- return -1
- case ctx.nodes[left].commitTime > ctx.nodes[right].commitTime:
- return 1
- }
-
- return objectid.Compare(ctx.nodes[left].id, ctx.nodes[right].id)
-}
diff --git a/internal/commitquery/context.go b/internal/commitquery/context.go
deleted file mode 100644
index bafbb5c4..00000000
--- a/internal/commitquery/context.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// Package commitquery provides private commit-domain query routines.
-package commitquery
-
-import (
- commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore"
-)
-
-// Context owns the mutable node arena for one commit query.
-type Context struct {
- store objectstore.Store
- graph *commitgraphread.Reader
-
- nodes []node
-
- byOID map[objectid.ObjectID]NodeIndex
- byGraphPos map[commitgraphread.Position]NodeIndex
-
- markPhase uint32
- touched []NodeIndex
-}
-
-// NewContext builds one empty query context over one object store and optional commit-graph reader.
-func NewContext(store objectstore.Store, graph *commitgraphread.Reader) *Context {
- return &Context{
- store: store,
- graph: graph,
- byOID: make(map[objectid.ObjectID]NodeIndex),
- byGraphPos: make(map[commitgraphread.Position]NodeIndex),
- }
-}
diff --git a/internal/commitquery/generation.go b/internal/commitquery/generation.go
deleted file mode 100644
index c5edcd9f..00000000
--- a/internal/commitquery/generation.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package commitquery
-
-import (
- "math"
-
- "codeberg.org/lindenii/furgit/objectid"
-)
-
-// EffectiveGeneration returns one node's generation value.
-func (ctx *Context) EffectiveGeneration(idx NodeIndex) uint64 {
- if !ctx.nodes[idx].hasGeneration {
- return generationInfinity
- }
-
- return ctx.nodes[idx].generation
-}
-
-const (
- generationInfinity = uint64(math.MaxUint64)
-)
-
-func compareByGeneration(ctx *Context) func(NodeIndex, NodeIndex) int {
- return func(left, right NodeIndex) int {
- leftGeneration := ctx.EffectiveGeneration(left)
- rightGeneration := ctx.EffectiveGeneration(right)
-
- switch {
- case leftGeneration < rightGeneration:
- return -1
- case leftGeneration > rightGeneration:
- return 1
- }
-
- switch {
- case ctx.nodes[left].commitTime < ctx.nodes[right].commitTime:
- return -1
- case ctx.nodes[left].commitTime > ctx.nodes[right].commitTime:
- return 1
- }
-
- return objectid.Compare(ctx.nodes[left].id, ctx.nodes[right].id)
- }
-}
diff --git a/internal/commitquery/graph_pos.go b/internal/commitquery/graph_pos.go
deleted file mode 100644
index 437f86e6..00000000
--- a/internal/commitquery/graph_pos.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package commitquery
-
-import commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
-
-// ResolveGraphPos resolves one commit-graph position to one internal query node.
-func (ctx *Context) ResolveGraphPos(pos commitgraphread.Position) (NodeIndex, error) {
- idx, ok := ctx.byGraphPos[pos]
- if ok {
- err := ctx.ensureLoaded(idx)
- if err != nil {
- return 0, err
- }
-
- return idx, nil
- }
-
- commit, err := ctx.graph.CommitAt(pos)
- if err != nil {
- return 0, err
- }
-
- idx, ok = ctx.byOID[commit.OID]
- if !ok {
- idx = ctx.newNode(commit.OID)
- ctx.byOID[commit.OID] = idx
- }
-
- ctx.byGraphPos[pos] = idx
- ctx.nodes[idx].graphPos = pos
- ctx.nodes[idx].hasGraphPos = true
-
- err = ctx.loadCommitAtGraphPos(idx, pos)
- if err != nil {
- delete(ctx.byGraphPos, pos)
-
- return 0, err
- }
-
- return idx, nil
-}
-
-// loadByGraphPos populates one node from a commit-graph position.
-func (ctx *Context) loadByGraphPos(idx NodeIndex) error {
- pos := ctx.nodes[idx].graphPos
-
- return ctx.loadCommitAtGraphPos(idx, pos)
-}
-
-func (ctx *Context) loadCommitAtGraphPos(idx NodeIndex, pos commitgraphread.Position) error {
- commit, err := ctx.graph.CommitAt(pos)
- if err != nil {
- return err
- }
-
- parents := make([]Parent, 0, 2+len(commit.ExtraParents))
-
- if commit.Parent1.Valid {
- parentOID, err := ctx.graph.OIDAt(commit.Parent1.Pos)
- if err != nil {
- return err
- }
-
- parents = append(parents, Parent{
- ID: parentOID,
- GraphPos: commit.Parent1.Pos,
- HasGraphPos: true,
- })
- }
-
- if commit.Parent2.Valid {
- parentOID, err := ctx.graph.OIDAt(commit.Parent2.Pos)
- if err != nil {
- return err
- }
-
- parents = append(parents, Parent{
- ID: parentOID,
- GraphPos: commit.Parent2.Pos,
- HasGraphPos: true,
- })
- }
-
- for _, parentPos := range commit.ExtraParents {
- parentOID, err := ctx.graph.OIDAt(parentPos)
- if err != nil {
- return err
- }
-
- parents = append(parents, Parent{
- ID: parentOID,
- GraphPos: parentPos,
- HasGraphPos: true,
- })
- }
-
- data := Commit{
- ID: commit.OID,
- Parents: parents,
- CommitTime: commit.CommitTimeUnix,
- Generation: commit.GenerationV2,
- HasGeneration: commit.GenerationV2 != 0,
- GraphPos: pos,
- HasGraphPos: true,
- }
-
- return ctx.populateNode(idx, data)
-}
diff --git a/internal/commitquery/load.go b/internal/commitquery/load.go
deleted file mode 100644
index b795f7d9..00000000
--- a/internal/commitquery/load.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package commitquery
-
-// ensureLoaded completes one node's metadata load if it has not been loaded yet.
-func (ctx *Context) ensureLoaded(idx NodeIndex) error {
- if ctx.nodes[idx].loaded {
- return nil
- }
-
- if ctx.nodes[idx].hasGraphPos {
- return ctx.loadByGraphPos(idx)
- }
-
- return ctx.loadByOID(idx)
-}
diff --git a/internal/commitquery/marks.go b/internal/commitquery/marks.go
deleted file mode 100644
index f88fdf25..00000000
--- a/internal/commitquery/marks.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package commitquery
-
-// Marks returns the mark bits of one internal node.
-func (ctx *Context) Marks(idx NodeIndex) markBits {
- return ctx.nodes[idx].marks
-}
-
-// HasAnyMarks reports whether one internal node has any requested bit.
-func (ctx *Context) HasAnyMarks(idx NodeIndex, bits markBits) bool {
- return ctx.nodes[idx].marks&bits != 0
-}
-
-// HasAllMarks reports whether one internal node already has all requested bits.
-func (ctx *Context) HasAllMarks(idx NodeIndex, bits markBits) bool {
- return ctx.nodes[idx].marks&bits == bits
-}
-
-// SetMarks ORs one set of mark bits into one internal node.
-func (ctx *Context) SetMarks(idx NodeIndex, bits markBits) {
- newBits := bits &^ ctx.nodes[idx].marks
- if newBits == 0 {
- return
- }
-
- ctx.trackTouched(idx)
- ctx.nodes[idx].marks |= bits
-}
-
-// ClearMarks removes one set of mark bits from one internal node.
-func (ctx *Context) ClearMarks(idx NodeIndex, bits markBits) {
- if ctx.nodes[idx].marks&bits == 0 {
- return
- }
-
- ctx.trackTouched(idx)
- ctx.nodes[idx].marks &^= bits
-}
-
-// BeginMarkPhase starts one tracked mark-mutation phase.
-func (ctx *Context) BeginMarkPhase() {
- ctx.markPhase++
- if ctx.markPhase == 0 {
- ctx.markPhase++
- for i := range ctx.nodes {
- ctx.nodes[i].touchedPhase = 0
- }
- }
-
- ctx.touched = ctx.touched[:0]
-}
-
-// ClearTouchedMarks clears the provided bits from all nodes touched in the
-// current mark phase.
-func (ctx *Context) ClearTouchedMarks(bits markBits) {
- for _, idx := range ctx.touched {
- ctx.nodes[idx].marks &^= bits
- }
-}
-
-func (ctx *Context) trackTouched(idx NodeIndex) {
- if ctx.nodes[idx].touchedPhase == ctx.markPhase {
- return
- }
-
- ctx.nodes[idx].touchedPhase = ctx.markPhase
- ctx.touched = append(ctx.touched, idx)
-}
diff --git a/internal/commitquery/merge_bases.go b/internal/commitquery/merge_bases.go
deleted file mode 100644
index 62ab9bd6..00000000
--- a/internal/commitquery/merge_bases.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package commitquery
-
-import "slices"
-
-// MergeBases computes fully reduced merge bases using one query context.
-func MergeBases(ctx *Context, left, right NodeIndex) ([]NodeIndex, error) {
- if left == right {
- return []NodeIndex{left}, nil
- }
-
- err := paintDownToCommon(ctx, left, []NodeIndex{right}, 0)
- if err != nil {
- return nil, err
- }
-
- candidates := collectMarkedResults(ctx)
-
- if len(candidates) <= 1 {
- slices.SortFunc(candidates, ctx.Compare)
-
- return candidates, nil
- }
-
- ctx.ClearTouchedMarks(allMarks)
-
- reduced, err := removeRedundant(ctx, candidates)
- if err != nil {
- return nil, err
- }
-
- slices.SortFunc(reduced, ctx.Compare)
-
- return reduced, nil
-}
-
-func paintDownToCommon(ctx *Context, left NodeIndex, rights []NodeIndex, minGeneration uint64) error {
- ctx.BeginMarkPhase()
-
- ctx.SetMarks(left, markLeft)
-
- if len(rights) == 0 {
- ctx.SetMarks(left, markResult)
-
- return nil
- }
-
- queue := newPriorityQueue(ctx)
- queue.PushNode(left)
-
- for _, right := range rights {
- ctx.SetMarks(right, markRight)
- queue.PushNode(right)
- }
-
- lastGeneration := generationInfinity
-
- for queueHasNonStale(ctx, queue) {
- idx := queue.PopNode()
-
- generation := ctx.EffectiveGeneration(idx)
- if generation > lastGeneration {
- return errBadGenerationOrder
- }
-
- lastGeneration = generation
- if generation < minGeneration {
- break
- }
-
- flags := ctx.Marks(idx) & (markLeft | markRight | markStale)
- if flags == (markLeft | markRight) {
- ctx.SetMarks(idx, markResult)
-
- flags |= markStale
- }
-
- for _, parent := range ctx.Parents(idx) {
- if ctx.HasAllMarks(parent, flags) {
- continue
- }
-
- ctx.SetMarks(parent, flags)
- queue.PushNode(parent)
- }
- }
-
- return nil
-}
-
-func queueHasNonStale(ctx *Context, queue *priorityQueue) bool {
- for _, idx := range queue.items {
- if !ctx.HasAnyMarks(idx, markStale) {
- return true
- }
- }
-
- return false
-}
-
-func collectMarkedResults(ctx *Context) []NodeIndex {
- out := make([]NodeIndex, 0, 4)
-
- for _, idx := range ctx.touched {
- if !ctx.HasAnyMarks(idx, markResult) {
- continue
- }
-
- if ctx.HasAnyMarks(idx, markStale) {
- continue
- }
-
- out = append(out, idx)
- }
-
- return out
-}
diff --git a/internal/commitquery/populate.go b/internal/commitquery/populate.go
deleted file mode 100644
index 87d65bf8..00000000
--- a/internal/commitquery/populate.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package commitquery
-
-import "fmt"
-
-// populateNode fills one node's metadata and resolves its parents.
-func (ctx *Context) populateNode(idx NodeIndex, commit Commit) error {
- if ctx.nodes[idx].loaded {
- if ctx.nodes[idx].id != commit.ID {
- return fmt.Errorf("commitquery: node identity mismatch: have %s, got %s", ctx.nodes[idx].id, commit.ID)
- }
-
- return nil
- }
-
- ctx.nodes[idx].id = commit.ID
- ctx.nodes[idx].commitTime = commit.CommitTime
- ctx.nodes[idx].generation = commit.Generation
- ctx.nodes[idx].hasGeneration = commit.HasGeneration
-
- if commit.HasGraphPos {
- ctx.nodes[idx].graphPos = commit.GraphPos
- ctx.nodes[idx].hasGraphPos = true
- ctx.byGraphPos[commit.GraphPos] = idx
- }
-
- ctx.nodes[idx].loaded = true
- ctx.nodes[idx].parents = ctx.nodes[idx].parents[:0]
-
- for _, parent := range commit.Parents {
- parentIdx, err := ctx.resolveParent(parent)
- if err != nil {
- ctx.nodes[idx].loaded = false
- ctx.nodes[idx].parents = nil
-
- return err
- }
-
- ctx.nodes[idx].parents = append(ctx.nodes[idx].parents, parentIdx)
- }
-
- return nil
-}
diff --git a/internal/commitquery/reduce.go b/internal/commitquery/reduce.go
deleted file mode 100644
index 917f2fa2..00000000
--- a/internal/commitquery/reduce.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package commitquery
-
-import (
- "slices"
-)
-
-// removeRedundant removes redundant merge-base candidates.
-func removeRedundant(ctx *Context, candidates []NodeIndex) ([]NodeIndex, error) {
- for _, idx := range candidates {
- if ctx.EffectiveGeneration(idx) != generationInfinity {
- return removeRedundantWithGen(ctx, candidates), nil
- }
- }
-
- return removeRedundantNoGen(ctx, candidates)
-}
-
-func removeRedundantNoGen(ctx *Context, candidates []NodeIndex) ([]NodeIndex, error) {
- redundant := make([]bool, len(candidates))
- work := make([]NodeIndex, 0, len(candidates)-1)
- filledIndex := make([]int, 0, len(candidates)-1)
-
- for i, candidate := range candidates {
- if redundant[i] {
- continue
- }
-
- work = work[:0]
- filledIndex = filledIndex[:0]
-
- minGeneration := ctx.EffectiveGeneration(candidate)
-
- for j, other := range candidates {
- if i == j || redundant[j] {
- continue
- }
-
- work = append(work, other)
- filledIndex = append(filledIndex, j)
-
- otherGeneration := ctx.EffectiveGeneration(other)
- if otherGeneration < minGeneration {
- minGeneration = otherGeneration
- }
- }
-
- err := paintDownToCommon(ctx, candidate, work, minGeneration)
- if err != nil {
- return nil, err
- }
-
- if ctx.HasAnyMarks(candidate, markRight) {
- redundant[i] = true
- }
-
- for j, other := range work {
- if ctx.HasAnyMarks(other, markLeft) {
- redundant[filledIndex[j]] = true
- }
- }
-
- ctx.ClearTouchedMarks(allMarks)
- }
-
- out := make([]NodeIndex, 0, len(candidates))
- for i, idx := range candidates {
- if !redundant[i] {
- out = append(out, idx)
- }
- }
-
- return out, nil
-}
-
-func removeRedundantWithGen(ctx *Context, candidates []NodeIndex) []NodeIndex {
- sorted := append([]NodeIndex(nil), candidates...)
- slices.SortFunc(sorted, compareByGeneration(ctx))
-
- minGeneration := ctx.EffectiveGeneration(sorted[0])
- minGenPos := 0
- countStillIndependent := len(candidates)
-
- ctx.BeginMarkPhase()
-
- walkStart := make([]NodeIndex, 0, len(candidates)*2)
-
- for _, idx := range candidates {
- ctx.SetMarks(idx, markResult)
-
- for _, parent := range ctx.Parents(idx) {
- if ctx.HasAnyMarks(parent, markStale) {
- continue
- }
-
- ctx.SetMarks(parent, markStale)
- walkStart = append(walkStart, parent)
- }
- }
-
- slices.SortFunc(walkStart, compareByGeneration(ctx))
-
- for _, idx := range walkStart {
- ctx.ClearMarks(idx, markStale)
- }
-
- for i := len(walkStart) - 1; i >= 0 && countStillIndependent > 1; i-- {
- stack := []NodeIndex{walkStart[i]}
- ctx.SetMarks(walkStart[i], markStale)
-
- for len(stack) > 0 {
- top := stack[len(stack)-1]
-
- if ctx.HasAnyMarks(top, markResult) {
- ctx.ClearMarks(top, markResult)
-
- countStillIndependent--
- if countStillIndependent <= 1 {
- break
- }
-
- if top == sorted[minGenPos] {
- for minGenPos < len(sorted)-1 && ctx.HasAnyMarks(sorted[minGenPos], markStale) {
- minGenPos++
- }
-
- minGeneration = ctx.EffectiveGeneration(sorted[minGenPos])
- }
- }
-
- if ctx.EffectiveGeneration(top) < minGeneration {
- stack = stack[:len(stack)-1]
-
- continue
- }
-
- pushed := false
-
- for _, parent := range ctx.Parents(top) {
- if ctx.HasAnyMarks(parent, markStale) {
- continue
- }
-
- ctx.SetMarks(parent, markStale)
- stack = append(stack, parent)
- pushed = true
-
- break
- }
-
- if !pushed {
- stack = stack[:len(stack)-1]
- }
- }
- }
-
- out := make([]NodeIndex, 0, len(candidates))
- for _, idx := range candidates {
- if !ctx.HasAnyMarks(idx, markStale) {
- out = append(out, idx)
- }
- }
-
- ctx.ClearTouchedMarks(markStale | markResult)
-
- return out
-}
diff --git a/mergebase/base.go b/mergebase/base.go
deleted file mode 100644
index 278fbed2..00000000
--- a/mergebase/base.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package mergebase
-
-import (
- commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore"
-)
-
-// Base reports one merge base between left and right, if any.
-//
-// Both inputs are peeled through annotated tags before commit traversal.
-func Base(
- store objectstore.Store,
- graph *commitgraphread.Reader,
- left objectid.ObjectID,
- right objectid.ObjectID,
-) (objectid.ObjectID, bool, error) {
- query := Query(store, graph, left, right)
-
- bases, err := query.All()
- if err != nil {
- return objectid.ObjectID{}, false, err
- }
-
- if len(bases) == 0 {
- return objectid.ObjectID{}, false, nil
- }
-
- return bases[0], true, nil
-}
diff --git a/mergebase/compute.go b/mergebase/compute.go
deleted file mode 100644
index 76d2294c..00000000
--- a/mergebase/compute.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package mergebase
-
-import (
- "slices"
-
- "codeberg.org/lindenii/furgit/internal/commitquery"
- "codeberg.org/lindenii/furgit/internal/peel"
- "codeberg.org/lindenii/furgit/objectid"
-)
-
-// All returns all merge bases in Git's merge-base --all order.
-func (query *Bases) All() ([]objectid.ObjectID, error) {
- if query.computed {
- return slices.Clone(query.bases), query.err
- }
-
- query.computed = true
-
- leftCommit, err := peel.ToCommit(query.store, query.left)
- if err != nil {
- query.err = err
-
- return nil, err
- }
-
- rightCommit, err := peel.ToCommit(query.store, query.right)
- if err != nil {
- query.err = err
-
- return nil, err
- }
-
- ctx := commitquery.NewContext(query.store, query.graph)
-
- leftIdx, err := ctx.ResolveOID(leftCommit)
- if err != nil {
- query.err = err
-
- return nil, err
- }
-
- rightIdx, err := ctx.ResolveOID(rightCommit)
- if err != nil {
- query.err = err
-
- return nil, err
- }
-
- candidates, err := commitquery.MergeBases(ctx, leftIdx, rightIdx)
- if err != nil {
- query.err = err
-
- return nil, err
- }
-
- slices.SortFunc(candidates, func(left, right commitquery.NodeIndex) int {
- switch {
- case ctx.CommitTime(left) > ctx.CommitTime(right):
- return -1
- case ctx.CommitTime(left) < ctx.CommitTime(right):
- return 1
- default:
- return objectid.Compare(ctx.ID(left), ctx.ID(right))
- }
- })
-
- query.bases = make([]objectid.ObjectID, 0, len(candidates))
- for _, idx := range candidates {
- query.bases = append(query.bases, ctx.ID(idx))
- }
-
- return slices.Clone(query.bases), nil
-}
diff --git a/mergebase/mergebase.go b/mergebase/mergebase.go
deleted file mode 100644
index 772dc355..00000000
--- a/mergebase/mergebase.go
+++ /dev/null
@@ -1,20 +0,0 @@
-// Package mergebase computes best common ancestors between commits.
-package mergebase
-
-import (
- commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore"
-)
-
-// Bases is one merge-base query over two commit roots.
-type Bases struct {
- store objectstore.Store
- graph *commitgraphread.Reader
- left objectid.ObjectID
- right objectid.ObjectID
-
- computed bool
- bases []objectid.ObjectID
- err error
-}
diff --git a/mergebase/query.go b/mergebase/query.go
deleted file mode 100644
index 9a934377..00000000
--- a/mergebase/query.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package mergebase
-
-import (
- commitgraphread "codeberg.org/lindenii/furgit/commitgraph/read"
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore"
-)
-
-// Query builds one merge-base query over two commit roots.
-//
-// Both inputs are peeled through annotated tags before commit traversal.
-func Query(
- store objectstore.Store,
- graph *commitgraphread.Reader,
- left objectid.ObjectID,
- right objectid.ObjectID,
-) *Bases {
- return &Bases{
- store: store,
- graph: graph,
- left: left,
- right: right,
- }
-}
diff --git a/receivepack/hooks/reject_force_push.go b/receivepack/hooks/reject_force_push.go
index cf5ddaea..4bc749a5 100644
--- a/receivepack/hooks/reject_force_push.go
+++ b/receivepack/hooks/reject_force_push.go
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
- "codeberg.org/lindenii/furgit/ancestor"
+ "codeberg.org/lindenii/furgit/commitquery"
"codeberg.org/lindenii/furgit/objectid"
objectmix "codeberg.org/lindenii/furgit/objectstore/mix"
receivepack "codeberg.org/lindenii/furgit/receivepack"
@@ -46,7 +46,7 @@ func RejectForcePush() receivepack.Hook {
continue
}
- ok, err := ancestor.Is(objects, nil, current.ID, update.NewID)
+ ok, err := commitquery.New(objects, nil).IsAncestor(current.ID, update.NewID)
if err != nil {
return nil, fmt.Errorf("check fast-forward %s: %w", update.Name, err)
}