chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user