github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/env/actions/checkout.go (about) 1 // Copyright 2021 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 actions 16 17 import ( 18 "context" 19 "time" 20 21 "github.com/dolthub/dolt/go/store/datas" 22 23 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 24 "github.com/dolthub/dolt/go/libraries/doltcore/env" 25 "github.com/dolthub/dolt/go/libraries/doltcore/ref" 26 "github.com/dolthub/dolt/go/libraries/utils/set" 27 "github.com/dolthub/dolt/go/store/hash" 28 ) 29 30 func CheckoutAllTables(ctx context.Context, roots doltdb.Roots) (doltdb.Roots, error) { 31 tbls, err := doltdb.UnionTableNames(ctx, roots.Working, roots.Staged, roots.Head) 32 if err != nil { 33 return doltdb.Roots{}, err 34 } 35 36 return MoveTablesFromHeadToWorking(ctx, roots, tbls) 37 } 38 39 // CheckoutTables takes in a set of tables and docs and checks them out to another branch. 40 func CheckoutTables(ctx context.Context, roots doltdb.Roots, tables []string) (doltdb.Roots, error) { 41 return MoveTablesFromHeadToWorking(ctx, roots, tables) 42 } 43 44 // MoveTablesFromHeadToWorking replaces the tables named from the given head to the given working root, overwriting any 45 // working changes, and returns the new resulting roots 46 func MoveTablesFromHeadToWorking(ctx context.Context, roots doltdb.Roots, tbls []string) (doltdb.Roots, error) { 47 var unknownTbls []string 48 for _, tblName := range tbls { 49 tbl, ok, err := roots.Staged.GetTable(ctx, doltdb.TableName{Name: tblName}) 50 if err != nil { 51 return doltdb.Roots{}, err 52 } 53 fkc, err := roots.Staged.GetForeignKeyCollection(ctx) 54 if err != nil { 55 return doltdb.Roots{}, err 56 } 57 58 if !ok { 59 tbl, ok, err = roots.Head.GetTable(ctx, doltdb.TableName{Name: tblName}) 60 if err != nil { 61 return doltdb.Roots{}, err 62 } 63 64 fkc, err = roots.Head.GetForeignKeyCollection(ctx) 65 if err != nil { 66 return doltdb.Roots{}, err 67 } 68 69 if !ok { 70 unknownTbls = append(unknownTbls, tblName) 71 continue 72 } 73 } 74 75 roots.Working, err = roots.Working.PutTable(ctx, doltdb.TableName{Name: tblName}, tbl) 76 if err != nil { 77 return doltdb.Roots{}, err 78 } 79 80 roots.Working, err = roots.Working.PutForeignKeyCollection(ctx, fkc) 81 if err != nil { 82 return doltdb.Roots{}, err 83 } 84 } 85 86 if len(unknownTbls) > 0 { 87 // Return table not exist error before RemoveTables, which fails silently if the table is not on the root. 88 err := validateTablesExist(ctx, roots.Working, unknownTbls) 89 if err != nil { 90 return doltdb.Roots{}, err 91 } 92 93 roots.Working, err = roots.Working.RemoveTables(ctx, false, false, unknownTbls...) 94 95 if err != nil { 96 return doltdb.Roots{}, err 97 } 98 } 99 100 return roots, nil 101 } 102 103 // RootsForBranch returns the roots needed for a branch checkout. |roots.Head| should be the pre-checkout head. The 104 // returned roots struct has |Head| set to |branchRoot|. 105 func RootsForBranch(ctx context.Context, roots doltdb.Roots, branchRoot doltdb.RootValue, force bool) (doltdb.Roots, error) { 106 conflicts := set.NewStrSet([]string{}) 107 if roots.Head == nil { 108 roots.Working = branchRoot 109 roots.Staged = branchRoot 110 roots.Head = branchRoot 111 return roots, nil 112 } 113 114 wrkTblHashes, err := moveModifiedTables(ctx, roots.Head, branchRoot, roots.Working, conflicts, force) 115 if err != nil { 116 return doltdb.Roots{}, err 117 } 118 119 stgTblHashes, err := moveModifiedTables(ctx, roots.Head, branchRoot, roots.Staged, conflicts, force) 120 if err != nil { 121 return doltdb.Roots{}, err 122 } 123 124 if conflicts.Size() > 0 { 125 return doltdb.Roots{}, ErrCheckoutWouldOverwrite{conflicts.AsSlice()} 126 } 127 128 workingForeignKeys, err := moveForeignKeys(ctx, roots.Head, branchRoot, roots.Working, force) 129 if err != nil { 130 return doltdb.Roots{}, err 131 } 132 133 stagedForeignKeys, err := moveForeignKeys(ctx, roots.Head, branchRoot, roots.Staged, force) 134 if err != nil { 135 return doltdb.Roots{}, err 136 } 137 138 roots.Working, err = writeTableHashes(ctx, branchRoot, wrkTblHashes) 139 if err != nil { 140 return doltdb.Roots{}, err 141 } 142 143 roots.Staged, err = writeTableHashes(ctx, branchRoot, stgTblHashes) 144 if err != nil { 145 return doltdb.Roots{}, err 146 } 147 148 roots.Working, err = roots.Working.PutForeignKeyCollection(ctx, workingForeignKeys) 149 if err != nil { 150 return doltdb.Roots{}, err 151 } 152 153 roots.Staged, err = roots.Staged.PutForeignKeyCollection(ctx, stagedForeignKeys) 154 if err != nil { 155 return doltdb.Roots{}, err 156 } 157 158 roots.Head = branchRoot 159 return roots, nil 160 } 161 162 // CleanOldWorkingSet resets the source branch's working set to the branch head, leaving the source branch unchanged 163 func CleanOldWorkingSet( 164 ctx context.Context, 165 dbData env.DbData, 166 doltDb *doltdb.DoltDB, 167 username, email string, 168 initialRoots doltdb.Roots, 169 initialHeadRef ref.DoltRef, 170 initialWs *doltdb.WorkingSet, 171 ) error { 172 // reset the source branch's working set to the branch head, leaving the source branch unchanged 173 err := ResetHard(ctx, dbData, doltDb, username, email, "", initialRoots, initialHeadRef, initialWs) 174 if err != nil { 175 return err 176 } 177 178 // Annoyingly, after the ResetHard above we need to get all the roots again, because the working set has changed 179 cm, err := doltDb.ResolveCommitRef(ctx, initialHeadRef) 180 if err != nil { 181 return err 182 } 183 184 headRoot, err := cm.ResolveRootValue(ctx) 185 if err != nil { 186 return err 187 } 188 189 workingSet, err := doltDb.ResolveWorkingSet(ctx, initialWs.Ref()) 190 if err != nil { 191 return err 192 } 193 194 resetRoots := doltdb.Roots{ 195 Head: headRoot, 196 Working: workingSet.WorkingRoot(), 197 Staged: workingSet.StagedRoot(), 198 } 199 200 // we also have to do a clean, because we the ResetHard won't touch any new tables (tables only in the working set) 201 newRoots, err := CleanUntracked(ctx, resetRoots, []string{}, false, true) 202 if err != nil { 203 return err 204 } 205 206 h, err := workingSet.HashOf() 207 if err != nil { 208 return err 209 } 210 211 err = doltDb.UpdateWorkingSet( 212 ctx, 213 initialWs.Ref(), 214 initialWs.WithWorkingRoot(newRoots.Working).WithStagedRoot(newRoots.Staged).ClearMerge().ClearRebase(), 215 h, 216 217 &datas.WorkingSetMeta{ 218 Name: username, 219 Email: email, 220 Timestamp: uint64(time.Now().Unix()), 221 Description: "reset hard", 222 }, 223 nil, 224 ) 225 if err != nil { 226 return err 227 } 228 return nil 229 } 230 231 // BranchHeadRoot returns the root value at the branch head with the name given 232 func BranchHeadRoot(ctx context.Context, db *doltdb.DoltDB, brName string) (doltdb.RootValue, error) { 233 cs, err := doltdb.NewCommitSpec(brName) 234 if err != nil { 235 return nil, doltdb.RootValueUnreadable{RootType: doltdb.HeadRoot, Cause: err} 236 } 237 238 optCmt, err := db.Resolve(ctx, cs, nil) 239 if err != nil { 240 return nil, doltdb.RootValueUnreadable{RootType: doltdb.HeadRoot, Cause: err} 241 } 242 243 cm, ok := optCmt.ToCommit() 244 if !ok { 245 return nil, doltdb.ErrGhostCommitEncountered 246 } 247 248 branchRoot, err := cm.GetRootValue(ctx) 249 if err != nil { 250 return nil, err 251 } 252 return branchRoot, nil 253 } 254 255 // moveModifiedTables handles working set changes during a branch change. 256 // When moving between branches, changes in the working set should travel with you. 257 // Working set changes cannot be moved if the table differs between the old and new head, 258 // in this case, we throw a conflict and error (as per Git). 259 func moveModifiedTables(ctx context.Context, oldRoot, newRoot, changedRoot doltdb.RootValue, conflicts *set.StrSet, force bool) (map[string]hash.Hash, error) { 260 resultMap := make(map[string]hash.Hash) 261 tblNames, err := newRoot.GetTableNames(ctx, doltdb.DefaultSchemaName) 262 if err != nil { 263 return nil, err 264 } 265 266 for _, tblName := range tblNames { 267 oldHash, _, err := oldRoot.GetTableHash(ctx, tblName) 268 if err != nil { 269 return nil, err 270 } 271 272 newHash, _, err := newRoot.GetTableHash(ctx, tblName) 273 if err != nil { 274 return nil, err 275 } 276 277 changedHash, _, err := changedRoot.GetTableHash(ctx, tblName) 278 if err != nil { 279 return nil, err 280 } 281 282 if oldHash == changedHash { 283 resultMap[tblName] = newHash 284 } else if oldHash == newHash { 285 resultMap[tblName] = changedHash 286 } else if force { 287 resultMap[tblName] = newHash 288 } else { 289 conflicts.Add(tblName) 290 } 291 } 292 293 tblNames, err = changedRoot.GetTableNames(ctx, doltdb.DefaultSchemaName) 294 if err != nil { 295 return nil, err 296 } 297 298 for _, tblName := range tblNames { 299 if _, exists := resultMap[tblName]; !exists { 300 oldHash, _, err := oldRoot.GetTableHash(ctx, tblName) 301 if err != nil { 302 return nil, err 303 } 304 305 changedHash, _, err := changedRoot.GetTableHash(ctx, tblName) 306 if err != nil { 307 return nil, err 308 } 309 310 if oldHash == emptyHash { 311 resultMap[tblName] = changedHash 312 } else if force { 313 resultMap[tblName] = oldHash 314 } else if oldHash != changedHash { 315 conflicts.Add(tblName) 316 } 317 } 318 } 319 320 return resultMap, nil 321 } 322 323 // moveForeignKeys returns the foreign key collection that should be used for the new working set. 324 func moveForeignKeys(ctx context.Context, oldRoot, newRoot, changedRoot doltdb.RootValue, force bool) (*doltdb.ForeignKeyCollection, error) { 325 oldFks, err := oldRoot.GetForeignKeyCollection(ctx) 326 if err != nil { 327 return nil, err 328 } 329 330 newFks, err := newRoot.GetForeignKeyCollection(ctx) 331 if err != nil { 332 return nil, err 333 } 334 335 changedFks, err := changedRoot.GetForeignKeyCollection(ctx) 336 if err != nil { 337 return nil, err 338 } 339 340 oldHash, err := oldFks.HashOf(ctx, oldRoot.VRW()) 341 if err != nil { 342 return nil, err 343 } 344 345 newHash, err := newFks.HashOf(ctx, newRoot.VRW()) 346 if err != nil { 347 return nil, err 348 } 349 350 changedHash, err := changedFks.HashOf(ctx, changedRoot.VRW()) 351 if err != nil { 352 return nil, err 353 } 354 355 if oldHash == changedHash { 356 return newFks, nil 357 } else if oldHash == newHash { 358 return changedFks, nil 359 } else { 360 // Both roots have modified the foreign keys. We need to do more work to merge them together into a new foreign 361 // key collection. 362 return mergeForeignKeyChanges(ctx, oldFks, newRoot, newFks, changedRoot, changedFks, force) 363 } 364 } 365 366 // mergeForeignKeyChanges merges the foreign key changes from the old and changed roots into a new foreign key 367 // collection, or returns an error if the changes are incompatible. Changes are incompatible if the changed root 368 // and new root both altered foreign keys on the same table. 369 func mergeForeignKeyChanges( 370 ctx context.Context, 371 oldFks *doltdb.ForeignKeyCollection, 372 newRoot doltdb.RootValue, 373 newFks *doltdb.ForeignKeyCollection, 374 changedRoot doltdb.RootValue, 375 changedFks *doltdb.ForeignKeyCollection, 376 force bool, 377 ) (*doltdb.ForeignKeyCollection, error) { 378 fksByTable := make(map[string][]doltdb.ForeignKey) 379 380 conflicts := set.NewEmptyStrSet() 381 tblNames, err := newRoot.GetTableNames(ctx, doltdb.DefaultSchemaName) 382 if err != nil { 383 return nil, err 384 } 385 386 for _, tblName := range tblNames { 387 oldFksForTable, _ := oldFks.KeysForTable(tblName) 388 newFksForTable, _ := newFks.KeysForTable(tblName) 389 changedFksForTable, _ := changedFks.KeysForTable(tblName) 390 391 oldHash, err := doltdb.CombinedHash(oldFksForTable) 392 if err != nil { 393 return nil, err 394 } 395 newHash, err := doltdb.CombinedHash(newFksForTable) 396 if err != nil { 397 return nil, err 398 } 399 changedHash, err := doltdb.CombinedHash(changedFksForTable) 400 if err != nil { 401 return nil, err 402 } 403 404 if oldHash == changedHash { 405 fksByTable[tblName] = append(fksByTable[tblName], newFksForTable...) 406 } else if oldHash == newHash { 407 fksByTable[tblName] = append(fksByTable[tblName], changedFksForTable...) 408 } else if force { 409 fksByTable[tblName] = append(fksByTable[tblName], newFksForTable...) 410 } else { 411 conflicts.Add(tblName) 412 } 413 } 414 415 tblNames, err = changedRoot.GetTableNames(ctx, doltdb.DefaultSchemaName) 416 if err != nil { 417 return nil, err 418 } 419 420 for _, tblName := range tblNames { 421 if _, exists := fksByTable[tblName]; !exists { 422 oldKeys, _ := oldFks.KeysForTable(tblName) 423 oldHash, err := doltdb.CombinedHash(oldKeys) 424 if err != nil { 425 return nil, err 426 } 427 428 changedKeys, _ := changedFks.KeysForTable(tblName) 429 changedHash, err := doltdb.CombinedHash(changedKeys) 430 if err != nil { 431 return nil, err 432 } 433 434 if oldHash == emptyHash { 435 fksByTable[tblName] = append(fksByTable[tblName], changedKeys...) 436 } else if force { 437 fksByTable[tblName] = append(fksByTable[tblName], oldKeys...) 438 } else if oldHash != changedHash { 439 conflicts.Add(tblName) 440 } 441 } 442 } 443 444 if conflicts.Size() > 0 { 445 return nil, ErrCheckoutWouldOverwrite{conflicts.AsSlice()} 446 } 447 448 fks := make([]doltdb.ForeignKey, 0) 449 for _, v := range fksByTable { 450 fks = append(fks, v...) 451 } 452 453 return doltdb.NewForeignKeyCollection(fks...) 454 } 455 456 // writeTableHashes writes new table hash values for the root given and returns it. 457 // This is an inexpensive and convenient way of replacing all the tables at once. 458 func writeTableHashes(ctx context.Context, head doltdb.RootValue, tblHashes map[string]hash.Hash) (doltdb.RootValue, error) { 459 names, err := head.GetTableNames(ctx, doltdb.DefaultSchemaName) 460 if err != nil { 461 return nil, err 462 } 463 464 var toDrop []string 465 for _, name := range names { 466 if _, ok := tblHashes[name]; !ok { 467 toDrop = append(toDrop, name) 468 } 469 } 470 471 head, err = head.RemoveTables(ctx, false, false, toDrop...) 472 if err != nil { 473 return nil, err 474 } 475 476 for k, v := range tblHashes { 477 if v == emptyHash { 478 continue 479 } 480 481 head, err = head.SetTableHash(ctx, k, v) 482 if err != nil { 483 return nil, err 484 } 485 } 486 487 return head, nil 488 } 489 490 // CheckoutWouldStompWorkingSetChanges checks that the current working set is "compatible" with the dest working set. 491 // This means that if both working sets are present (ie there are changes on both source and dest branches), 492 // we check if the changes are identical before allowing a clobbering checkout. 493 // Working set errors are ignored by this function, because they are properly handled elsewhere. 494 func CheckoutWouldStompWorkingSetChanges(ctx context.Context, sourceRoots, destRoots doltdb.Roots) (bool, error) { 495 496 wouldStomp := doRootsHaveIncompatibleChanges(sourceRoots, destRoots) 497 498 if !wouldStomp { 499 return false, nil 500 } 501 502 // In some cases, a working set differs from its head only by the feature version. 503 // If this is the case, moving the working set is safe. 504 modifiedSourceRoots, err := clearFeatureVersion(ctx, sourceRoots) 505 if err != nil { 506 return true, err 507 } 508 509 modifiedDestRoots, err := clearFeatureVersion(ctx, destRoots) 510 if err != nil { 511 return true, err 512 } 513 514 return doRootsHaveIncompatibleChanges(modifiedSourceRoots, modifiedDestRoots), nil 515 } 516 517 func doRootsHaveIncompatibleChanges(sourceRoots, destRoots doltdb.Roots) bool { 518 sourceHasChanges, sourceWorkingHash, sourceStagedHash, err := RootHasUncommittedChanges(sourceRoots) 519 if err != nil { 520 return false 521 } 522 523 destHasChanges, destWorkingHash, destStagedHash, err := RootHasUncommittedChanges(destRoots) 524 if err != nil { 525 return false 526 } 527 528 // This is a stomping checkout operation if both the source and dest have uncommitted changes, and they're not the 529 // same uncommitted changes 530 return sourceHasChanges && destHasChanges && (sourceWorkingHash != destWorkingHash || sourceStagedHash != destStagedHash) 531 } 532 533 // clearFeatureVersion creates a new version of the provided roots where all three roots have the same 534 // feature version. By hashing these new roots, we can easily determine whether the roots differ only by 535 // their feature version. 536 func clearFeatureVersion(ctx context.Context, roots doltdb.Roots) (doltdb.Roots, error) { 537 currentBranchFeatureVersion, _, err := roots.Head.GetFeatureVersion(ctx) 538 if err != nil { 539 return doltdb.Roots{}, err 540 } 541 542 modifiedWorking, err := roots.Working.SetFeatureVersion(currentBranchFeatureVersion) 543 if err != nil { 544 return doltdb.Roots{}, err 545 } 546 547 modifiedStaged, err := roots.Staged.SetFeatureVersion(currentBranchFeatureVersion) 548 if err != nil { 549 return doltdb.Roots{}, err 550 } 551 552 return doltdb.Roots{ 553 Head: roots.Head, 554 Working: modifiedWorking, 555 Staged: modifiedStaged, 556 }, nil 557 } 558 559 // RootHasUncommittedChanges returns whether the roots given have uncommitted changes, and the hashes of 560 // the working and staged roots are identical. This function will ignore any difference in feature 561 // versions between the root values. 562 func RootHasUncommittedChanges(roots doltdb.Roots) (hasChanges bool, workingHash hash.Hash, stagedHash hash.Hash, err error) { 563 roots, err = clearFeatureVersion(context.Background(), roots) 564 if err != nil { 565 return false, hash.Hash{}, hash.Hash{}, err 566 } 567 568 headHash, err := roots.Head.HashOf() 569 if err != nil { 570 return false, hash.Hash{}, hash.Hash{}, err 571 } 572 573 workingHash, err = roots.Working.HashOf() 574 if err != nil { 575 return false, hash.Hash{}, hash.Hash{}, err 576 } 577 578 stagedHash, err = roots.Staged.HashOf() 579 if err != nil { 580 return false, hash.Hash{}, hash.Hash{}, err 581 } 582 583 hasChanges = workingHash != stagedHash || stagedHash != headHash 584 return hasChanges, workingHash, stagedHash, nil 585 }