github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dprocedures/dolt_rebase.go (about) 1 // Copyright 2023 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 dprocedures 16 17 import ( 18 "errors" 19 "fmt" 20 21 "github.com/dolthub/go-mysql-server/sql" 22 "github.com/dolthub/go-mysql-server/sql/types" 23 goerrors "gopkg.in/src-d/go-errors.v1" 24 25 "github.com/dolthub/dolt/go/cmd/dolt/cli" 26 "github.com/dolthub/dolt/go/libraries/doltcore/cherry_pick" 27 "github.com/dolthub/dolt/go/libraries/doltcore/diff" 28 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 29 "github.com/dolthub/dolt/go/libraries/doltcore/env/actions" 30 "github.com/dolthub/dolt/go/libraries/doltcore/merge" 31 "github.com/dolthub/dolt/go/libraries/doltcore/rebase" 32 "github.com/dolthub/dolt/go/libraries/doltcore/ref" 33 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" 34 ) 35 36 var doltRebaseProcedureSchema = []*sql.Column{ 37 { 38 Name: "status", 39 Type: types.Int64, 40 Nullable: false, 41 }, 42 { 43 Name: "message", 44 Type: types.LongText, 45 Nullable: true, 46 }, 47 } 48 49 var RebaseActionEnumType = types.MustCreateEnumType([]string{ 50 rebase.RebaseActionDrop, 51 rebase.RebaseActionPick, 52 rebase.RebaseActionReword, 53 rebase.RebaseActionSquash, 54 rebase.RebaseActionFixup}, sql.Collation_Default) 55 56 var DoltRebaseSystemTableSchema = []*sql.Column{ 57 { 58 Name: "rebase_order", 59 Type: types.MustCreateDecimalType(6, 2), 60 Nullable: false, 61 PrimaryKey: true, 62 }, 63 { 64 Name: "action", 65 Type: RebaseActionEnumType, 66 Nullable: false, 67 }, 68 { 69 Name: "commit_hash", 70 Type: types.Text, 71 Nullable: false, 72 }, 73 { 74 Name: "commit_message", 75 Type: types.Text, 76 Nullable: false, 77 }, 78 } 79 80 // ErrRebaseUncommittedChanges is used when a rebase is started, but there are uncommitted (and not 81 // ignored) changes in the working set. 82 var ErrRebaseUncommittedChanges = fmt.Errorf("cannot start a rebase with uncommitted changes") 83 84 // ErrRebaseConflict is used when a merge conflict is detected while rebasing a commit. 85 var ErrRebaseConflict = goerrors.NewKind( 86 "merge conflict detected while rebasing commit %s. " + 87 "the rebase has been automatically aborted") 88 89 // ErrRebaseConflictWithAbortError is used when a merge conflict is detected while rebasing a commit, 90 // and we are unable to cleanly abort the rebase. 91 var ErrRebaseConflictWithAbortError = goerrors.NewKind( 92 "merge conflict detected while rebasing commit %s. " + 93 "attempted to abort rebase operation, but encountered error: %w") 94 95 // SuccessfulRebaseMessage is used when a rebase finishes successfully. The branch that was rebased should be appended 96 // to the end of the message. 97 var SuccessfulRebaseMessage = "Successfully rebased and updated refs/heads/" 98 99 var RebaseAbortedMessage = "Interactive rebase aborted" 100 101 func doltRebase(ctx *sql.Context, args ...string) (sql.RowIter, error) { 102 res, message, err := doDoltRebase(ctx, args) 103 if err != nil { 104 return nil, err 105 } 106 return rowToIter(int64(res), message), nil 107 } 108 109 func doDoltRebase(ctx *sql.Context, args []string) (int, string, error) { 110 if ctx.GetCurrentDatabase() == "" { 111 return 1, "", sql.ErrNoDatabaseSelected.New() 112 } 113 114 apr, err := cli.CreateRebaseArgParser().Parse(args) 115 if err != nil { 116 return 1, "", err 117 } 118 119 switch { 120 case apr.Contains(cli.AbortParam): 121 err := abortRebase(ctx) 122 if err != nil { 123 return 1, "", err 124 } else { 125 return 0, RebaseAbortedMessage, nil 126 } 127 128 case apr.Contains(cli.ContinueFlag): 129 rebaseBranch, err := continueRebase(ctx) 130 if err != nil { 131 return 1, "", err 132 } else { 133 return 0, SuccessfulRebaseMessage + rebaseBranch, nil 134 } 135 136 default: 137 if apr.NArg() == 0 { 138 return 1, "", fmt.Errorf("not enough args") 139 } else if apr.NArg() > 1 { 140 return 1, "", fmt.Errorf("too many args") 141 } 142 if !apr.Contains(cli.InteractiveFlag) { 143 return 1, "", fmt.Errorf("non-interactive rebases not currently supported") 144 } 145 err = startRebase(ctx, apr.Arg(0)) 146 if err != nil { 147 return 1, "", err 148 } 149 150 currentBranch, err := currentBranch(ctx) 151 if err != nil { 152 return 1, "", err 153 } 154 155 return 0, fmt.Sprintf("interactive rebase started on branch %s; "+ 156 "adjust the rebase plan in the dolt_rebase table, then continue rebasing by "+ 157 "calling dolt_rebase('--continue')", currentBranch), nil 158 } 159 } 160 161 func startRebase(ctx *sql.Context, upstreamPoint string) error { 162 if upstreamPoint == "" { 163 return fmt.Errorf("no upstream branch specified") 164 } 165 166 err := validateWorkingSetCanStartRebase(ctx) 167 if err != nil { 168 return err 169 } 170 171 doltSession := dsess.DSessFromSess(ctx.Session) 172 headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) 173 if err != nil { 174 return err 175 } 176 dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) 177 if !ok { 178 return fmt.Errorf("unable to find database %s", ctx.GetCurrentDatabase()) 179 } 180 181 rebaseBranch, err := currentBranch(ctx) 182 if err != nil { 183 return err 184 } 185 186 startCommit, err := dbData.Ddb.ResolveCommitRef(ctx, ref.NewBranchRef(rebaseBranch)) 187 if err != nil { 188 return err 189 } 190 191 commitSpec, err := doltdb.NewCommitSpec(upstreamPoint) 192 if err != nil { 193 return err 194 } 195 196 optCmt, err := dbData.Ddb.Resolve(ctx, commitSpec, headRef) 197 if err != nil { 198 return err 199 } 200 upstreamCommit, ok := optCmt.ToCommit() 201 if !ok { 202 return doltdb.ErrGhostCommitEncountered 203 } 204 205 // rebaseWorkingBranch is the name of the temporary branch used when performing a rebase. In Git, a rebase 206 // happens with a detatched HEAD, but Dolt doesn't support that, we use a temporary branch. 207 rebaseWorkingBranch := "dolt_rebase_" + rebaseBranch 208 var rsc doltdb.ReplicationStatusController 209 err = actions.CreateBranchWithStartPt(ctx, dbData, rebaseWorkingBranch, upstreamPoint, false, &rsc) 210 if err != nil { 211 return err 212 } 213 err = commitTransaction(ctx, doltSession, &rsc) 214 if err != nil { 215 return err 216 } 217 218 // Checkout our new branch 219 wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseWorkingBranch)) 220 if err != nil { 221 return err 222 } 223 err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef) 224 if err != nil { 225 return err 226 } 227 228 dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) 229 if !ok { 230 return fmt.Errorf("unable to get db datata for database %s", ctx.GetCurrentDatabase()) 231 } 232 233 db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase()) 234 if err != nil { 235 return err 236 } 237 238 workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) 239 if err != nil { 240 return err 241 } 242 243 branchRoots, err := dbData.Ddb.ResolveBranchRoots(ctx, ref.NewBranchRef(rebaseBranch)) 244 if err != nil { 245 return err 246 } 247 248 newWorkingSet, err := workingSet.StartRebase(ctx, upstreamCommit, rebaseBranch, branchRoots.Working) 249 if err != nil { 250 return err 251 } 252 253 err = doltSession.SetWorkingSet(ctx, ctx.GetCurrentDatabase(), newWorkingSet) 254 if err != nil { 255 return err 256 } 257 258 // Create the rebase plan and save it in the database 259 rebasePlan, err := rebase.CreateDefaultRebasePlan(ctx, startCommit, upstreamCommit) 260 if err != nil { 261 abortErr := abortRebase(ctx) 262 if abortErr != nil { 263 return fmt.Errorf("%s: unable to cleanly abort rebase: %s", err.Error(), abortErr.Error()) 264 } 265 return err 266 } 267 rdb, ok := db.(rebase.RebasePlanDatabase) 268 if !ok { 269 return fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db) 270 } 271 return rdb.SaveRebasePlan(ctx, rebasePlan) 272 } 273 274 // validateRebaseBranchHasntChanged checks that the branch being rebased hasn't been updated since the rebase started, 275 // and returns an error if any changes are detected. 276 func validateRebaseBranchHasntChanged(ctx *sql.Context, branch string, rebaseState *doltdb.RebaseState) error { 277 doltSession := dsess.DSessFromSess(ctx.Session) 278 doltDb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase()) 279 if !ok { 280 return fmt.Errorf("unable to access DoltDB for database %s", ctx.GetCurrentDatabase()) 281 } 282 283 wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(branch)) 284 if err != nil { 285 return err 286 } 287 288 resolvedWorkingSet, err := doltDb.ResolveWorkingSet(ctx, wsRef) 289 if err != nil { 290 return err 291 } 292 hash2, err := resolvedWorkingSet.StagedRoot().HashOf() 293 if err != nil { 294 return err 295 } 296 hash1, err := rebaseState.PreRebaseWorkingRoot().HashOf() 297 if err != nil { 298 return err 299 } 300 if hash1 != hash2 { 301 return fmt.Errorf("rebase aborted due to changes in branch %s", branch) 302 } 303 304 return nil 305 } 306 307 func validateWorkingSetCanStartRebase(ctx *sql.Context) error { 308 doltSession := dsess.DSessFromSess(ctx.Session) 309 310 // Make sure there isn't an active rebase or merge in progress already 311 ws, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) 312 if err != nil { 313 return err 314 } 315 if ws.MergeActive() { 316 return fmt.Errorf("unable to start rebase while a merge is in progress – abort the current merge before proceeding") 317 } 318 if ws.RebaseActive() { 319 return fmt.Errorf("unable to start rebase while another rebase is in progress – abort the current rebase before proceeding") 320 } 321 322 // Make sure the working set doesn't contain any uncommitted changes 323 roots, ok := doltSession.GetRoots(ctx, ctx.GetCurrentDatabase()) 324 if !ok { 325 return fmt.Errorf("unable to get roots for database %s", ctx.GetCurrentDatabase()) 326 } 327 wsOnlyHasIgnoredTables, err := diff.WorkingSetContainsOnlyIgnoredTables(ctx, roots) 328 if err != nil { 329 return err 330 } 331 if !wsOnlyHasIgnoredTables { 332 return ErrRebaseUncommittedChanges 333 } 334 335 return nil 336 } 337 338 func abortRebase(ctx *sql.Context) error { 339 doltSession := dsess.DSessFromSess(ctx.Session) 340 341 workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) 342 if err != nil { 343 return err 344 } 345 if !workingSet.RebaseActive() { 346 return fmt.Errorf("no rebase in progress") 347 } 348 349 // Clear the rebase state (even though we're going to delete this branch next) 350 rebaseState := workingSet.RebaseState() 351 workingSet = workingSet.AbortRebase() 352 err = doltSession.SetWorkingSet(ctx, ctx.GetCurrentDatabase(), workingSet) 353 if err != nil { 354 return err 355 } 356 357 // Delete the working branch 358 var rsc doltdb.ReplicationStatusController 359 dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) 360 if !ok { 361 return fmt.Errorf("unable to get DbData for database %s", ctx.GetCurrentDatabase()) 362 } 363 headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) 364 if err != nil { 365 return err 366 } 367 err = actions.DeleteBranch(ctx, dbData, headRef.GetPath(), actions.DeleteOptions{ 368 Force: true, 369 AllowDeletingCurrentBranch: true, 370 }, doltSession.Provider(), &rsc) 371 if err != nil { 372 return err 373 } 374 375 // Switch back to the original branch head 376 wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseState.Branch())) 377 if err != nil { 378 return err 379 } 380 381 return doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef) 382 } 383 384 func continueRebase(ctx *sql.Context) (string, error) { 385 // TODO: Eventually, when we allow interactive-rebases to be stopped and started (e.g. with the break action, 386 // or for conflict resolution), we'll need to track what step we're at in the rebase plan. 387 388 // Validate that we are in an interactive rebase 389 doltSession := dsess.DSessFromSess(ctx.Session) 390 workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) 391 if err != nil { 392 return "", err 393 } 394 if !workingSet.RebaseActive() { 395 return "", fmt.Errorf("no rebase in progress") 396 } 397 398 db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase()) 399 if err != nil { 400 return "", err 401 } 402 403 rdb, ok := db.(rebase.RebasePlanDatabase) 404 if !ok { 405 return "", fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db) 406 } 407 rebasePlan, err := rdb.LoadRebasePlan(ctx) 408 if err != nil { 409 return "", err 410 } 411 412 err = rebase.ValidateRebasePlan(ctx, rebasePlan) 413 if err != nil { 414 return "", err 415 } 416 417 for _, step := range rebasePlan.Steps { 418 err = processRebasePlanStep(ctx, &step) 419 if err != nil { 420 return "", err 421 } 422 } 423 424 // Update the branch being rebased to point to the same commit as our temporary working branch 425 rebaseBranchWorkingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) 426 if err != nil { 427 return "", err 428 } 429 dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) 430 if !ok { 431 return "", fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase()) 432 } 433 434 rebaseBranch := rebaseBranchWorkingSet.RebaseState().Branch() 435 rebaseWorkingBranch := "dolt_rebase_" + rebaseBranch 436 437 // Check that the branch being rebased hasn't been updated since the rebase started 438 err = validateRebaseBranchHasntChanged(ctx, rebaseBranch, rebaseBranchWorkingSet.RebaseState()) 439 if err != nil { 440 return "", err 441 } 442 443 // TODO: copyABranch (and the underlying call to doltdb.NewBranchAtCommit) has a race condition 444 // where another session can set the branch head AFTER doltdb.NewBranchAtCommit updates 445 // the branch head, but BEFORE doltdb.NewBranchAtCommit retrieves the working set for the 446 // branch and updates the working root and staged root for the working set. We may be able 447 // to fix this race condition by changing doltdb.NewBranchAtCommit to use 448 // database.CommitWithWorkingSet, since it updates a branch head and working set atomically. 449 err = copyABranch(ctx, dbData, rebaseWorkingBranch, rebaseBranch, true, nil) 450 if err != nil { 451 return "", err 452 } 453 454 // Checkout the branch being rebased 455 previousBranchWorkingSetRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseBranchWorkingSet.RebaseState().Branch())) 456 if err != nil { 457 return "", err 458 } 459 err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), previousBranchWorkingSetRef) 460 if err != nil { 461 return "", err 462 } 463 464 // delete the temporary working branch 465 dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) 466 if !ok { 467 return "", fmt.Errorf("unable to lookup dbdata") 468 } 469 return rebaseBranch, actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{ 470 Force: true, 471 }, doltSession.Provider(), nil) 472 } 473 474 func processRebasePlanStep(ctx *sql.Context, planStep *rebase.RebasePlanStep) error { 475 // Make sure we have a transaction opened for the session 476 // NOTE: After our first call to cherry-pick, the tx is committed, so a new tx needs to be started 477 // as we process additional rebase actions. 478 doltSession := dsess.DSessFromSess(ctx.Session) 479 if doltSession.GetTransaction() == nil { 480 _, err := doltSession.StartTransaction(ctx, sql.ReadWrite) 481 if err != nil { 482 return err 483 } 484 } 485 486 switch planStep.Action { 487 case rebase.RebaseActionDrop: 488 return nil 489 490 case rebase.RebaseActionPick, rebase.RebaseActionReword: 491 options := cherry_pick.CherryPickOptions{} 492 if planStep.Action == rebase.RebaseActionReword { 493 options.CommitMessage = planStep.CommitMsg 494 } 495 return handleRebaseCherryPick(ctx, planStep.CommitHash, options) 496 497 case rebase.RebaseActionSquash, rebase.RebaseActionFixup: 498 options := cherry_pick.CherryPickOptions{Amend: true} 499 if planStep.Action == rebase.RebaseActionSquash { 500 commitMessage, err := squashCommitMessage(ctx, planStep.CommitHash) 501 if err != nil { 502 return err 503 } 504 options.CommitMessage = commitMessage 505 } 506 return handleRebaseCherryPick(ctx, planStep.CommitHash, options) 507 508 default: 509 return fmt.Errorf("rebase action '%s' is not supported", planStep.Action) 510 } 511 } 512 513 // handleRebaseCherryPick runs a cherry-pick for the specified |commitHash|, using the specified 514 // cherry-pick |options| and checks the results for any errors or merge conflicts. The initial 515 // version of rebase doesn't support conflict resolution, so if any conflicts are detected, the 516 // rebase is aborted and an error is returned. 517 func handleRebaseCherryPick(ctx *sql.Context, commitHash string, options cherry_pick.CherryPickOptions) error { 518 _, mergeResult, err := cherry_pick.CherryPick(ctx, commitHash, options) 519 520 var schemaConflict merge.SchemaConflict 521 isSchemaConflict := errors.As(err, &schemaConflict) 522 523 if (mergeResult != nil && mergeResult.HasMergeArtifacts()) || isSchemaConflict { 524 // TODO: rebase doesn't currently support conflict resolution, but ideally, when a conflict 525 // is detected, the rebase would be paused and the user would resolve the conflict just 526 // like any other conflict, and then call dolt_rebase --continue to keep going. 527 abortErr := abortRebase(ctx) 528 if abortErr != nil { 529 return ErrRebaseConflictWithAbortError.New(commitHash, abortErr) 530 } 531 return ErrRebaseConflict.New(commitHash) 532 } 533 return err 534 } 535 536 // squashCommitMessage looks up the commit at HEAD and the commit identified by |nextCommitHash| and squashes their two 537 // commit messages together. 538 func squashCommitMessage(ctx *sql.Context, nextCommitHash string) (string, error) { 539 doltSession := dsess.DSessFromSess(ctx.Session) 540 headCommit, err := doltSession.GetHeadCommit(ctx, ctx.GetCurrentDatabase()) 541 if err != nil { 542 return "", err 543 } 544 headCommitMeta, err := headCommit.GetCommitMeta(ctx) 545 if err != nil { 546 return "", err 547 } 548 549 ddb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase()) 550 if !ok { 551 return "", fmt.Errorf("unable to get doltdb!") 552 } 553 spec, err := doltdb.NewCommitSpec(nextCommitHash) 554 if err != nil { 555 return "", err 556 } 557 headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) 558 if err != nil { 559 return "", err 560 } 561 562 optCmt, err := ddb.Resolve(ctx, spec, headRef) 563 if err != nil { 564 return "", err 565 } 566 nextCommit, ok := optCmt.ToCommit() 567 if !ok { 568 return "", doltdb.ErrGhostCommitEncountered 569 } 570 571 nextCommitMeta, err := nextCommit.GetCommitMeta(ctx) 572 if err != nil { 573 return "", err 574 } 575 commitMessage := headCommitMeta.Description + "\n\n" + nextCommitMeta.Description 576 577 return commitMessage, nil 578 } 579 580 // currentBranch returns the name of the currently checked out branch, or any error if one was encountered. 581 func currentBranch(ctx *sql.Context) (string, error) { 582 doltSession := dsess.DSessFromSess(ctx.Session) 583 headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) 584 if err != nil { 585 return "", err 586 } 587 return headRef.GetPath(), nil 588 }