github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/env/actions/commitwalk/commitwalk.go (about)

     1  // Copyright 2019 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package commitwalk
    16  
    17  import (
    18  	"container/heap"
    19  	"context"
    20  	"io"
    21  
    22  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    23  	"github.com/dolthub/dolt/go/store/datas"
    24  	"github.com/dolthub/dolt/go/store/hash"
    25  )
    26  
    27  type c struct {
    28  	ddb       *doltdb.DoltDB
    29  	commit    *doltdb.OptionalCommit
    30  	meta      *datas.CommitMeta
    31  	hash      hash.Hash
    32  	height    uint64
    33  	invisible bool
    34  	queued    bool
    35  }
    36  
    37  type q struct {
    38  	pending           []*c
    39  	numVisiblePending int
    40  	loaded            map[hash.Hash]*c
    41  }
    42  
    43  func (q *q) NumVisiblePending() int {
    44  	return q.numVisiblePending
    45  }
    46  
    47  func (q *q) Push(x interface{}) {
    48  	q.pending = append(q.pending, x.(*c))
    49  }
    50  
    51  func (q *q) Pop() interface{} {
    52  	old := q.pending
    53  	ret := old[len(old)-1]
    54  	q.pending = old[:len(old)-1]
    55  	return ret
    56  }
    57  
    58  func (q *q) Len() int {
    59  	return len(q.pending)
    60  }
    61  
    62  func (q *q) Swap(i, j int) {
    63  	q.pending[i], q.pending[j] = q.pending[j], q.pending[i]
    64  }
    65  
    66  // Less returns true if the commit at index i is "less" than the commit at index j. It may be the case that you are comparing
    67  // two resolved commits, two ghost commits, or a resolved commit and a ghost commit. Ghost commits will always be "less" than
    68  // resolved commits. If both commits are resolved, then the commit with the higher height is "less". If the heights are equal, then
    69  // the commit with the newer timestamp is "less". Finally if both commits are ghost commits, we don't really have enough
    70  // information to compare on, so we just compare the hashes to ensure that the results are stable.
    71  func (q *q) Less(i, j int) bool {
    72  	_, okI := q.pending[i].commit.ToCommit()
    73  	_, okJ := q.pending[i].commit.ToCommit()
    74  
    75  	if !okI && okJ {
    76  		return true
    77  	} else if okI && !okJ {
    78  		return false
    79  	} else if !okI && !okJ {
    80  		return q.pending[i].hash.String() < q.pending[j].hash.String()
    81  	}
    82  
    83  	if q.pending[i].height > q.pending[j].height {
    84  		return true
    85  	}
    86  
    87  	if q.pending[i].height == q.pending[j].height {
    88  		return q.pending[i].meta.UserTimestamp > q.pending[j].meta.UserTimestamp
    89  	}
    90  	return false
    91  }
    92  
    93  func (q *q) PopPending() *c {
    94  	c := heap.Pop(q).(*c)
    95  	if !c.invisible {
    96  		q.numVisiblePending--
    97  	}
    98  	return c
    99  }
   100  
   101  func (q *q) AddPendingIfUnseen(ctx context.Context, ddb *doltdb.DoltDB, id hash.Hash) error {
   102  	c, err := q.Get(ctx, ddb, id)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	if !c.queued {
   107  		c.queued = true
   108  		heap.Push(q, c)
   109  		if !c.invisible {
   110  			q.numVisiblePending++
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  func (q *q) SetInvisible(ctx context.Context, ddb *doltdb.DoltDB, id hash.Hash) error {
   117  	c, err := q.Get(ctx, ddb, id)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	if !c.invisible {
   122  		c.invisible = true
   123  		if c.queued {
   124  			q.numVisiblePending--
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  func load(ctx context.Context, ddb *doltdb.DoltDB, h hash.Hash) (*doltdb.OptionalCommit, error) {
   131  	cs, err := doltdb.NewCommitSpec(h.String())
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	c, err := ddb.Resolve(ctx, cs, nil)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	return c, nil
   140  }
   141  
   142  func (q *q) Get(ctx context.Context, ddb *doltdb.DoltDB, id hash.Hash) (*c, error) {
   143  	if l, ok := q.loaded[id]; ok {
   144  		return l, nil
   145  	}
   146  
   147  	optCmt, err := load(ctx, ddb, id)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	commit, ok := optCmt.ToCommit()
   153  	if !ok {
   154  		return &c{ddb: ddb, commit: optCmt, hash: id}, nil
   155  	}
   156  
   157  	h, err := commit.Height()
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	meta, err := commit.GetCommitMeta(ctx)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	c := &c{ddb: ddb, commit: &doltdb.OptionalCommit{Commit: commit, Addr: id}, meta: meta, height: h, hash: id}
   167  	q.loaded[id] = c
   168  	return c, nil
   169  }
   170  
   171  func newQueue() *q {
   172  	return &q{loaded: make(map[hash.Hash]*c)}
   173  }
   174  
   175  // GetDotDotRevisions returns the commits reachable from commit at hashes
   176  // `includedHeads` that are not reachable from hashes `excludedHeads`.
   177  // `includedHeads` and `excludedHeads` must be commits in `ddb`. Returns up
   178  // to `num` commits, in reverse topological order starting at `includedHeads`,
   179  // with tie breaking based on the height of commit graph between
   180  // concurrent commits --- higher commits appear first. Remaining
   181  // ties are broken by timestamp; newer commits appear first.
   182  //
   183  // Roughly mimics `git log main..feature` or `git log main...feature` (if
   184  // more than one `includedHead` is provided).
   185  func GetDotDotRevisions(ctx context.Context, includedDB *doltdb.DoltDB, includedHeads []hash.Hash, excludedDB *doltdb.DoltDB, excludedHeads []hash.Hash, num int) ([]*doltdb.OptionalCommit, error) {
   186  	itr, err := GetDotDotRevisionsIterator(ctx, includedDB, includedHeads, excludedDB, excludedHeads, nil)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	var commitList []*doltdb.OptionalCommit
   192  	for num < 0 || len(commitList) < num {
   193  		_, commit, err := itr.Next(ctx)
   194  		if err == io.EOF {
   195  			break
   196  		} else if err != nil {
   197  			return nil, err
   198  		}
   199  
   200  		commitList = append(commitList, commit)
   201  	}
   202  
   203  	return commitList, nil
   204  }
   205  
   206  // GetTopologicalOrderCommitIterator returns an iterator for commits generated with the same semantics as
   207  // GetTopologicalOrderCommits
   208  func GetTopologicalOrderIterator(ctx context.Context, ddb *doltdb.DoltDB, startCommitHashes []hash.Hash, matchFn func(*doltdb.OptionalCommit) (bool, error)) (doltdb.CommitItr, error) {
   209  	return newCommiterator(ctx, ddb, startCommitHashes, matchFn)
   210  }
   211  
   212  type commiterator struct {
   213  	ddb               *doltdb.DoltDB
   214  	startCommitHashes []hash.Hash
   215  	matchFn           func(*doltdb.OptionalCommit) (bool, error)
   216  	q                 *q
   217  }
   218  
   219  var _ doltdb.CommitItr = (*commiterator)(nil)
   220  
   221  func newCommiterator(ctx context.Context, ddb *doltdb.DoltDB, startCommitHashes []hash.Hash, matchFn func(*doltdb.OptionalCommit) (bool, error)) (*commiterator, error) {
   222  	itr := &commiterator{
   223  		ddb:               ddb,
   224  		startCommitHashes: startCommitHashes,
   225  		matchFn:           matchFn,
   226  	}
   227  
   228  	err := itr.Reset(ctx)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	return itr, nil
   234  }
   235  
   236  // Next implements doltdb.CommitItr
   237  func (iter *commiterator) Next(ctx context.Context) (hash.Hash, *doltdb.OptionalCommit, error) {
   238  	if iter.q.NumVisiblePending() > 0 {
   239  		nextC := iter.q.PopPending()
   240  
   241  		var err error
   242  		parents := []hash.Hash{}
   243  		commit, ok := nextC.commit.ToCommit()
   244  		if ok {
   245  			parents, err = commit.ParentHashes(ctx)
   246  			if err != nil {
   247  				return hash.Hash{}, nil, err
   248  			}
   249  		}
   250  
   251  		for _, parentID := range parents {
   252  			if err := iter.q.AddPendingIfUnseen(ctx, nextC.ddb, parentID); err != nil {
   253  				return hash.Hash{}, nil, err
   254  			}
   255  		}
   256  
   257  		matches := true
   258  		if iter.matchFn != nil {
   259  			matches, err = iter.matchFn(nextC.commit)
   260  
   261  			if err != nil {
   262  				return hash.Hash{}, nil, err
   263  			}
   264  		}
   265  
   266  		if matches {
   267  			return nextC.hash, &doltdb.OptionalCommit{Commit: commit, Addr: nextC.hash}, nil
   268  		}
   269  
   270  		return iter.Next(ctx)
   271  	}
   272  
   273  	return hash.Hash{}, nil, io.EOF
   274  }
   275  
   276  // Reset implements doltdb.CommitItr
   277  func (i *commiterator) Reset(ctx context.Context) error {
   278  	i.q = newQueue()
   279  	for _, startCommitHash := range i.startCommitHashes {
   280  		if err := i.q.AddPendingIfUnseen(ctx, i.ddb, startCommitHash); err != nil {
   281  			return err
   282  		}
   283  	}
   284  	return nil
   285  }
   286  
   287  // GetDotDotRevisionsIterator returns an iterator for commits generated with the same semantics as
   288  // GetDotDotRevisions
   289  func GetDotDotRevisionsIterator(ctx context.Context, includedDdb *doltdb.DoltDB, startCommitHashes []hash.Hash, excludedDdb *doltdb.DoltDB, excludingCommitHashes []hash.Hash, matchFn func(*doltdb.OptionalCommit) (bool, error)) (doltdb.CommitItr, error) {
   290  	return newDotDotCommiterator(ctx, includedDdb, startCommitHashes, excludedDdb, excludingCommitHashes, matchFn)
   291  }
   292  
   293  // GetTopNTopoOrderedCommitsMatching returns the first N commits (If N <= 0 then all commits) reachable from the commits in
   294  // `startCommitHashes` in reverse topological order, with tiebreaking done by the height of the commit graph -- higher
   295  // commits appear first. Remaining ties are broken by timestamp; newer commits appear first. DO NOT DELETE, USED IN DOLTHUB
   296  func GetTopNTopoOrderedCommitsMatching(ctx context.Context, ddb *doltdb.DoltDB, startCommitHashes []hash.Hash, n int, matchFn func(commit *doltdb.OptionalCommit) (bool, error)) ([]*doltdb.Commit, error) {
   297  	itr, err := GetTopologicalOrderIterator(ctx, ddb, startCommitHashes, matchFn)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	var commitList []*doltdb.Commit
   303  	for n < 0 || len(commitList) < n {
   304  		_, optCmt, err := itr.Next(ctx)
   305  		if err == io.EOF {
   306  			break
   307  		} else if err != nil {
   308  			return nil, err
   309  		}
   310  		commit, ok := optCmt.ToCommit()
   311  		if !ok {
   312  			return nil, doltdb.ErrGhostCommitEncountered
   313  		}
   314  		commitList = append(commitList, commit)
   315  	}
   316  	return commitList, nil
   317  }
   318  
   319  type dotDotCommiterator struct {
   320  	includedDdb           *doltdb.DoltDB
   321  	excludedDdb           *doltdb.DoltDB
   322  	startCommitHashes     []hash.Hash
   323  	excludingCommitHashes []hash.Hash
   324  	matchFn               func(*doltdb.OptionalCommit) (bool, error)
   325  	q                     *q
   326  }
   327  
   328  var _ doltdb.CommitItr = (*dotDotCommiterator)(nil)
   329  
   330  func newDotDotCommiterator(ctx context.Context, includedDdb *doltdb.DoltDB, startCommitHashes []hash.Hash, excludedDdb *doltdb.DoltDB, excludingCommitHashes []hash.Hash, matchFn func(*doltdb.OptionalCommit) (bool, error)) (*dotDotCommiterator, error) {
   331  	itr := &dotDotCommiterator{
   332  		includedDdb:           includedDdb,
   333  		excludedDdb:           excludedDdb,
   334  		startCommitHashes:     startCommitHashes,
   335  		excludingCommitHashes: excludingCommitHashes,
   336  		matchFn:               matchFn,
   337  	}
   338  
   339  	err := itr.Reset(ctx)
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  
   344  	return itr, nil
   345  }
   346  
   347  // Next implements doltdb.CommitItr
   348  func (i *dotDotCommiterator) Next(ctx context.Context) (hash.Hash, *doltdb.OptionalCommit, error) {
   349  	if i.q.NumVisiblePending() > 0 {
   350  		nextC := i.q.PopPending()
   351  
   352  		commit, ok := nextC.commit.ToCommit()
   353  		if !ok {
   354  			return nextC.hash, nextC.commit, nil
   355  		}
   356  
   357  		parents, err := commit.ParentHashes(ctx)
   358  		if err != nil {
   359  			return hash.Hash{}, nil, err
   360  		}
   361  
   362  		for _, parentID := range parents {
   363  			if nextC.invisible {
   364  				if err := i.q.SetInvisible(ctx, nextC.ddb, parentID); err != nil {
   365  					return hash.Hash{}, nil, err
   366  				}
   367  			}
   368  			if err := i.q.AddPendingIfUnseen(ctx, nextC.ddb, parentID); err != nil {
   369  				return hash.Hash{}, nil, err
   370  			}
   371  		}
   372  
   373  		matches := true
   374  		if i.matchFn != nil {
   375  			matches, err = i.matchFn(nextC.commit)
   376  			if err != nil {
   377  				return hash.Hash{}, nil, err
   378  			}
   379  		}
   380  
   381  		// If not invisible, return commit. Otherwise get next commit
   382  		if !nextC.invisible && matches {
   383  			return nextC.hash, nextC.commit, nil
   384  		}
   385  		return i.Next(ctx)
   386  	}
   387  
   388  	return hash.Hash{}, nil, io.EOF
   389  }
   390  
   391  // Reset implements doltdb.CommitItr
   392  func (i *dotDotCommiterator) Reset(ctx context.Context) error {
   393  	i.q = newQueue()
   394  	for _, excludingCommitHash := range i.excludingCommitHashes {
   395  		if err := i.q.SetInvisible(ctx, i.excludedDdb, excludingCommitHash); err != nil {
   396  			return err
   397  		}
   398  		if err := i.q.AddPendingIfUnseen(ctx, i.excludedDdb, excludingCommitHash); err != nil {
   399  			return err
   400  		}
   401  	}
   402  	for _, startCommitHash := range i.startCommitHashes {
   403  		if err := i.q.AddPendingIfUnseen(ctx, i.includedDdb, startCommitHash); err != nil {
   404  			return err
   405  		}
   406  	}
   407  	return nil
   408  }