Files
dolthub--dolt/go/libraries/doltcore/commitgraph/commitgraph.go
T
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

306 lines
8.0 KiB
Go

// Copyright 2026 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package commitgraph provides a lightweight topological commit graph iterator.
//
// Unlike env/actions/commitwalk, this package does NOT depend on the doltdb
// package and therefore does not transitively pull in go-mysql-server or
// go-icu-regex. It depends only on store/datas and store/hash.
//
// Consumers inside Dolt that already import doltdb should continue using
// commitwalk. This package is for lightweight consumers (such as DumboDB) that
// only need commit graph traversal without the full SQL engine dependency.
package commitgraph
import (
"container/heap"
"context"
"io"
"github.com/dolthub/dolt/go/store/datas"
"github.com/dolthub/dolt/go/store/hash"
)
// HashResolver resolves a commit hash to its CommitInfo. Implementations
// should return a CommitInfo with IsGhost=true for ghost (placeholder) commits.
type HashResolver interface {
ResolveCommitHash(ctx context.Context, h hash.Hash) (*CommitInfo, error)
}
// CommitInfo holds the commit data needed for graph traversal.
type CommitInfo struct {
Hash hash.Hash
Height uint64
Meta *datas.CommitMeta
Parents []hash.Hash // parent1, parent2, ...
IsGhost bool
}
// Iterator walks commits in reverse topological order.
type Iterator interface {
// Next returns the next commit. Returns io.EOF when exhausted.
Next(ctx context.Context) (*CommitInfo, error)
// Reset restarts the walk from the original start hashes.
Reset(ctx context.Context) error
}
// GetTopologicalOrderIterator returns an Iterator that walks every commit
// reachable from startHashes in reverse topological order (highest/newest
// first). Ties at the same height are broken by timestamp (newer first).
// Ghost commits sort before resolved commits.
//
// The optional matchFn filters commits: return true to include, false to skip.
func GetTopologicalOrderIterator(
ctx context.Context,
resolver HashResolver,
startHashes []hash.Hash,
matchFn func(*CommitInfo) (bool, error),
) (Iterator, error) {
return newTopoIter(ctx, resolver, startHashes, matchFn)
}
// GetDotDotIterator returns an Iterator that yields commits reachable from
// includedHashes but NOT reachable from excludedHashes, analogous to
// `git log excluded..included`.
func GetDotDotIterator(
ctx context.Context,
includedResolver HashResolver,
includedHashes []hash.Hash,
excludedResolver HashResolver,
excludedHashes []hash.Hash,
matchFn func(*CommitInfo) (bool, error),
) (Iterator, error) {
return newDotDotIter(ctx, includedResolver, includedHashes, excludedResolver, excludedHashes, matchFn)
}
// --- priority queue --------------------------------------------------------
type entry struct {
resolver HashResolver
info *CommitInfo
invisible bool
queued bool
}
type pq struct {
pending []*entry
numVisiblePending int
loaded map[hash.Hash]*entry
}
func newPQ() *pq { return &pq{loaded: make(map[hash.Hash]*entry)} }
func (q *pq) Len() int { return len(q.pending) }
func (q *pq) Swap(i, j int) { q.pending[i], q.pending[j] = q.pending[j], q.pending[i] }
func (q *pq) Push(x interface{}) { q.pending = append(q.pending, x.(*entry)) }
func (q *pq) Pop() interface{} {
old := q.pending
ret := old[len(old)-1]
q.pending = old[:len(old)-1]
return ret
}
func (q *pq) Less(i, j int) bool {
ei, ej := q.pending[i], q.pending[j]
if ei.info.IsGhost && !ej.info.IsGhost {
return true
}
if !ei.info.IsGhost && ej.info.IsGhost {
return false
}
if ei.info.IsGhost && ej.info.IsGhost {
return ei.info.Hash.String() < ej.info.Hash.String()
}
if ei.info.Height > ej.info.Height {
return true
}
if ei.info.Height == ej.info.Height && ei.info.Meta != nil && ej.info.Meta != nil {
return ei.info.Meta.UserTimestampMillis() > ej.info.Meta.UserTimestampMillis()
}
return false
}
func (q *pq) get(ctx context.Context, r HashResolver, h hash.Hash) (*entry, error) {
if e, ok := q.loaded[h]; ok {
return e, nil
}
info, err := r.ResolveCommitHash(ctx, h)
if err != nil {
return nil, err
}
e := &entry{resolver: r, info: info}
q.loaded[h] = e
return e, nil
}
func (q *pq) addIfUnseen(ctx context.Context, r HashResolver, h hash.Hash) error {
e, err := q.get(ctx, r, h)
if err != nil {
return err
}
if !e.queued {
e.queued = true
heap.Push(q, e)
if !e.invisible {
q.numVisiblePending++
}
}
return nil
}
func (q *pq) setInvisible(ctx context.Context, r HashResolver, h hash.Hash) error {
e, err := q.get(ctx, r, h)
if err != nil {
return err
}
if !e.invisible {
e.invisible = true
if e.queued {
q.numVisiblePending--
}
}
return nil
}
func (q *pq) popPending() *entry {
e := heap.Pop(q).(*entry)
if !e.invisible {
q.numVisiblePending--
}
return e
}
// --- topological iterator --------------------------------------------------
type topoIter struct {
resolver HashResolver
startHashes []hash.Hash
matchFn func(*CommitInfo) (bool, error)
q *pq
}
func newTopoIter(ctx context.Context, resolver HashResolver, startHashes []hash.Hash, matchFn func(*CommitInfo) (bool, error)) (*topoIter, error) {
it := &topoIter{resolver: resolver, startHashes: startHashes, matchFn: matchFn}
if err := it.Reset(ctx); err != nil {
return nil, err
}
return it, nil
}
func (it *topoIter) Next(ctx context.Context) (*CommitInfo, error) {
for it.q.numVisiblePending > 0 {
e := it.q.popPending()
for _, ph := range e.info.Parents {
if err := it.q.addIfUnseen(ctx, e.resolver, ph); err != nil {
return nil, err
}
}
if it.matchFn != nil {
ok, err := it.matchFn(e.info)
if err != nil {
return nil, err
}
if !ok {
continue
}
}
return e.info, nil
}
return nil, io.EOF
}
func (it *topoIter) Reset(ctx context.Context) error {
it.q = newPQ()
for _, h := range it.startHashes {
if err := it.q.addIfUnseen(ctx, it.resolver, h); err != nil {
return err
}
}
return nil
}
// --- dot-dot iterator ------------------------------------------------------
type dotDotIter struct {
includedResolver HashResolver
excludedResolver HashResolver
startHashes []hash.Hash
excludedHashes []hash.Hash
matchFn func(*CommitInfo) (bool, error)
q *pq
}
func newDotDotIter(ctx context.Context, inclR HashResolver, inclH []hash.Hash, exclR HashResolver, exclH []hash.Hash, matchFn func(*CommitInfo) (bool, error)) (*dotDotIter, error) {
it := &dotDotIter{
includedResolver: inclR,
excludedResolver: exclR,
startHashes: inclH,
excludedHashes: exclH,
matchFn: matchFn,
}
if err := it.Reset(ctx); err != nil {
return nil, err
}
return it, nil
}
func (it *dotDotIter) Next(ctx context.Context) (*CommitInfo, error) {
for it.q.numVisiblePending > 0 {
e := it.q.popPending()
for _, ph := range e.info.Parents {
if e.invisible {
if err := it.q.setInvisible(ctx, e.resolver, ph); err != nil {
return nil, err
}
}
if err := it.q.addIfUnseen(ctx, e.resolver, ph); err != nil {
return nil, err
}
}
if e.invisible {
continue
}
if it.matchFn != nil {
ok, err := it.matchFn(e.info)
if err != nil {
return nil, err
}
if !ok {
continue
}
}
return e.info, nil
}
return nil, io.EOF
}
func (it *dotDotIter) Reset(ctx context.Context) error {
it.q = newPQ()
for _, h := range it.excludedHashes {
if err := it.q.setInvisible(ctx, it.excludedResolver, h); err != nil {
return err
}
if err := it.q.addIfUnseen(ctx, it.excludedResolver, h); err != nil {
return err
}
}
for _, h := range it.startHashes {
if err := it.q.addIfUnseen(ctx, it.includedResolver, h); err != nil {
return err
}
}
return nil
}