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 }