diff --git a/go/cmd/dolt/commands/rebase.go b/go/cmd/dolt/commands/rebase.go index 34be0f73699..30a1dc14e12 100644 --- a/go/cmd/dolt/commands/rebase.go +++ b/go/cmd/dolt/commands/rebase.go @@ -122,6 +122,11 @@ func (cmd RebaseCmd) Exec(ctx context.Context, commandStr string, args []string, return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("error: "+rows[0][1].(string))), usage) } + if status != 0 { + cli.Println(fmt.Sprintf("runtime error: unexpected non-zero, non-one status from DOLT_REBASE: %d", status)) + return 1 + } + // If the rebase was successful, or if it was aborted, print out the message and // ensure the branch being rebased is checked out in the CLI message := rows[0][1].(string) @@ -134,6 +139,14 @@ func (cmd RebaseCmd) Exec(ctx context.Context, commandStr string, args []string, return 0 } + if strings.HasPrefix(message, dprocedures.EditPausePrefix) { + // We need to pause to edit a commit. This is similar to date conflicts, but that makes it's way to us as an error + // This is not an error scenario, Just print the message, and return 0. + cli.Println(message) + return 0 + } + + // At this point, we know the rebase has just been initiated, and we are in interactive mode. rebasePlan, err := getRebasePlan(cliCtx, queryist.Context, queryist.Queryist, apr.Arg(0), branchName) if err != nil { // attempt to abort the rebase @@ -168,8 +181,7 @@ func (cmd RebaseCmd) Exec(ctx context.Context, commandStr string, args []string, rows, err = cli.GetRowsForSql(queryist.Queryist, queryist.Context, "CALL DOLT_REBASE('--continue');") if err != nil { - // If the error is a data conflict, don't abort the rebase, but let the caller resolve the conflicts - if dprocedures.ErrRebaseDataConflict.Is(err) || strings.Contains(err.Error(), dprocedures.ErrRebaseDataConflict.Message[:40]) { + if isRebaseConflictError(err) { if checkoutErr := syncCliBranchToSqlSessionBranch(queryist.Context, dEnv); checkoutErr != nil { return HandleVErrAndExitCode(errhand.VerboseErrorFromError(checkoutErr), usage) } @@ -198,7 +210,17 @@ func (cmd RebaseCmd) Exec(ctx context.Context, commandStr string, args []string, return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("error: "+rows[0][1].(string))), usage) } - cli.Println(rows[0][1].(string)) + message = rows[0][1].(string) + + // Check if this is an edit pause. Such messages have a status value of 0, so all we have to go on is the message. + if strings.Contains(message, dprocedures.EditPausePrefix) { + // Make sure the CLI is on the same branch so the user can make their edits. + if checkoutErr := syncCliBranchToSqlSessionBranch(queryist.Context, dEnv); checkoutErr != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(checkoutErr), usage) + } + } + + cli.Println(message) return 0 } @@ -289,6 +311,7 @@ func buildInitialRebaseMsg(sqlCtx *sql.Context, queryist cli.Queryist, rebaseBra buffer.WriteString("# Commands:\n") buffer.WriteString("# p, pick = use commit\n") buffer.WriteString("# d, drop = remove commit\n") + buffer.WriteString("# e, edit = use commit, but stop for amending\n") buffer.WriteString("# r, reword = use commit, but edit the commit message\n") buffer.WriteString("# s, squash = use commit, but meld into previous commit\n") buffer.WriteString("# f, fixup = like \"squash\", but discard this commit's message\n") @@ -314,6 +337,31 @@ func getRebaseAction(col interface{}) (string, bool) { } } +// rebaseActionMap maps short action forms to their full names +var rebaseActionMap = map[string]string{ + "p": rebase.RebaseActionPick, + "r": rebase.RebaseActionReword, + "e": rebase.RebaseActionEdit, + "s": rebase.RebaseActionSquash, + "f": rebase.RebaseActionFixup, + "d": rebase.RebaseActionDrop, + // Also accept full names + rebase.RebaseActionPick: rebase.RebaseActionPick, + rebase.RebaseActionReword: rebase.RebaseActionReword, + rebase.RebaseActionEdit: rebase.RebaseActionEdit, + rebase.RebaseActionSquash: rebase.RebaseActionSquash, + rebase.RebaseActionFixup: rebase.RebaseActionFixup, + rebase.RebaseActionDrop: rebase.RebaseActionDrop, +} + +func expandRebaseAction(action string) (string, error) { + if fullAction, ok := rebaseActionMap[action]; ok { + return fullAction, nil + } + + return "", fmt.Errorf("unknown action in rebase plan: %s", action) +} + // parseRebaseMessage parses the rebase message from the editor and adds all uncommented out lines as steps in the rebase plan. func parseRebaseMessage(rebaseMsg string) (*rebase.RebasePlan, error) { plan := &rebase.RebasePlan{} @@ -324,8 +372,14 @@ func parseRebaseMessage(rebaseMsg string) (*rebase.RebasePlan, error) { if len(rebaseStepParts) != 3 { return nil, fmt.Errorf("invalid line %d: %s", i, line) } + + expandedAction, err := expandRebaseAction(rebaseStepParts[0]) + if err != nil { + return nil, fmt.Errorf("line %d: %s", i+1, err.Error()) + } + plan.Steps = append(plan.Steps, rebase.RebasePlanStep{ - Action: rebaseStepParts[0], + Action: expandedAction, CommitHash: rebaseStepParts[1], CommitMsg: rebaseStepParts[2], }) @@ -354,9 +408,8 @@ func insertRebasePlanIntoDoltRebaseTable(plan *rebase.RebasePlan, sqlCtx *sql.Co } // syncCliBranchToSqlSessionBranch sets the current branch for the CLI (in repo_state.json) to the active branch -// for the current session. This is needed during rebasing, since any conflicts need to be resolved while the -// session is on the rebase working branch (e.g. dolt_rebase_t1) and after the rebase finishes, the session needs -// to be back on the branch being rebased (e.g. t1). +// for the current session. This is needed during rebasing, as the user may need to stop in the middle of the +// process to handle a conflict or an edit operation. func syncCliBranchToSqlSessionBranch(ctx *sql.Context, dEnv *env.DoltEnv) error { doltSession := dsess.DSessFromSess(ctx.Session) currentBranch, err := doltSession.GetBranch(ctx) @@ -366,3 +419,20 @@ func syncCliBranchToSqlSessionBranch(ctx *sql.Context, dEnv *env.DoltEnv) error return saveHeadBranch(dEnv.FS, currentBranch) } + +// isRebaseConflictError checks if the given error represents a rebase pause condition +// (data conflicts) that should not abort the rebase but instead allow the user to resolve/continue. +func isRebaseConflictError(err error) bool { + if err == nil { + return false + } + + // Check typed errors first (for local execution) + if dprocedures.ErrRebaseDataConflict.Is(err) { + return true + } + + // For over-the-wire errors that lose their type, match against error message patterns + errMsg := err.Error() + return strings.HasPrefix(errMsg, dprocedures.RebaseDataConflictPrefix) +} diff --git a/go/libraries/doltcore/rebase/rebase.go b/go/libraries/doltcore/rebase/rebase.go index 9254c1bf958..c016881f3a8 100644 --- a/go/libraries/doltcore/rebase/rebase.go +++ b/go/libraries/doltcore/rebase/rebase.go @@ -32,6 +32,7 @@ const ( RebaseActionSquash = "squash" RebaseActionFixup = "fixup" RebaseActionDrop = "drop" + RebaseActionEdit = "edit" RebaseActionReword = "reword" ) diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go index 060443885ed..7f4374df456 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go @@ -54,7 +54,8 @@ var RebaseActionEnumType = types.MustCreateEnumType([]string{ rebase.RebaseActionPick, rebase.RebaseActionReword, rebase.RebaseActionSquash, - rebase.RebaseActionFixup}, sql.Collation_Default) + rebase.RebaseActionFixup, + rebase.RebaseActionEdit}, sql.Collation_Default) // GetDoltRebaseSystemTableSchema returns the schema for the dolt_rebase system table. // This is used by Doltgres to update the dolt_rebase schema using Doltgres types. @@ -108,11 +109,50 @@ var ErrRebaseDataConflictsCantBeResolved = goerrors.NewKind( "The rebase has been aborted. Set @@autocommit to 0 or set @@dolt_allow_commit_conflicts to 1 and " + "try the rebase again to resolve the conflicts.") +// rebaseResult represents the result of a rebase operation, containing the status code, +// message, halt flag, and any error that occurred. This allows clean separation between success +// states (including pauses) and actual error conditions. +type rebaseResult struct { + status int + message string + halt bool // true if rebase should pause (for conflicts or edit actions) + err error +} + +// newRebaseSuccess creates a successful rebase result +func newRebaseSuccess(message string) rebaseResult { + return rebaseResult{status: 0, message: message, halt: false, err: nil} +} + +// newRebasePause creates a rebase pause result (success with pause message) +func newRebasePause(message string) rebaseResult { + return rebaseResult{status: 0, message: message, halt: true, err: nil} +} + +// newRebaseError creates a rebase error result +func newRebaseError(err error) rebaseResult { + return rebaseResult{status: 1, message: "", halt: false, err: err} +} + +// Error message prefixes for pattern matching when errors lose their type over-the-wire +const ( + RebaseDataConflictPrefix = "data conflict detected while rebasing commit" +) + // ErrRebaseDataConflict is used when a data conflict is detected while rebasing a commit. -var ErrRebaseDataConflict = goerrors.NewKind("data conflict detected while rebasing commit %s (%s). \n\n" + +var ErrRebaseDataConflict = goerrors.NewKind(RebaseDataConflictPrefix + " %s (%s). \n\n" + "Resolve the conflicts and remove them from the dolt_conflicts_ tables, " + "then continue the rebase by calling dolt_rebase('--continue')") +var EditPausePrefix = "edit action paused at commit" + +// createEditPauseMessage creates a pause message for edit actions +func createEditPauseMessage(commitHash, commitMsg string) string { + return fmt.Sprintf(EditPausePrefix+" %s (%s). \n\n"+ + "You can now modify the working directory and stage changes. "+ + "When ready, continue the rebase by calling dolt_rebase('--continue')", commitHash, commitMsg) +} + // ErrRebaseSchemaConflict is used when a schema conflict is detected while rebasing a commit. var ErrRebaseSchemaConflict = goerrors.NewKind( "schema conflict detected while rebasing commit %s. " + @@ -158,12 +198,8 @@ func doDoltRebase(ctx *sql.Context, args []string) (int, string, error) { } case apr.Contains(cli.ContinueFlag): - rebaseBranch, err := continueRebase(ctx) - if err != nil { - return 1, "", err - } else { - return 0, SuccessfulRebaseMessage + rebaseBranch, nil - } + result := continueRebase(ctx) + return result.status, result.message, result.err default: commitBecomesEmptyHandling, err := processCommitBecomesEmptyParams(apr) @@ -191,11 +227,8 @@ func doDoltRebase(ctx *sql.Context, args []string) (int, string, error) { } if !apr.Contains(cli.InteractiveFlag) { - rebaseBranch, err := continueRebase(ctx) - if err != nil { - return 1, "", err - } - return 0, SuccessfulRebaseMessage + rebaseBranch, nil + result := continueRebase(ctx) + return result.status, result.message, result.err } return 0, fmt.Sprintf("interactive rebase started on branch %s; "+ @@ -598,15 +631,15 @@ func recordCurrentStep(ctx *sql.Context, step rebase.RebasePlanStep) error { return nil } -func continueRebase(ctx *sql.Context) (string, error) { +func continueRebase(ctx *sql.Context) rebaseResult { // Validate that we are in an interactive rebase if err := validateActiveRebase(ctx); err != nil { - return "", err + return newRebaseError(err) } // If there are conflicts, stop the rebase with an error message about resolving the conflicts before continuing if err := validateNoConflicts(ctx); err != nil { - return "", err + return newRebaseError(err) } // After we've checked for conflicts in the working set, commit the SQL transaction to ensure @@ -615,22 +648,22 @@ func continueRebase(ctx *sql.Context) (string, error) { if doltSession.GetTransaction() == nil { _, err := doltSession.StartTransaction(ctx, sql.ReadWrite) if err != nil { - return "", err + return newRebaseError(err) } } if err := doltSession.CommitTransaction(ctx, doltSession.GetTransaction()); err != nil { - return "", err + return newRebaseError(err) } rebasePlan, err := loadRebasePlan(ctx) if err != nil { - return "", err + return newRebaseError(err) } for _, step := range rebasePlan.Steps { workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) if err != nil { - return "", err + return newRebaseError(err) } rebaseStepOrder := step.RebaseOrderAsFloat() @@ -639,13 +672,13 @@ func continueRebase(ctx *sql.Context) (string, error) { hasStagedChanges, hasUnstagedChanges, err := workingSetStatus(ctx) if err != nil { - return "", err + return newRebaseError(err) } // If the rebase is just starting but there are any uncommitted changes in the working set, // tell the user they need to commit them before we can start executing the rebase plan. if !rebasingStarted && (hasStagedChanges || hasUnstagedChanges) { - return "", ErrRebaseUncommittedChanges.New() + return newRebaseError(ErrRebaseUncommittedChanges.New()) } // If we've already executed this step, move to the next plan step @@ -656,14 +689,14 @@ func continueRebase(ctx *sql.Context) (string, error) { // If the rebase is continued, but not all working set changes are staged, then tell the user // they need to explicitly stage the tables before the rebase can be continued. if hasUnstagedChanges { - return "", ErrRebaseUnstagedChanges.New() + return newRebaseError(ErrRebaseUnstagedChanges.New()) } // If we've already executed this step, but the working set has staged changes, // then we need to make the commit for the manual changes made for this step. if rebasingStarted && rebaseStepOrder == lastAttemptedStep && hasStagedChanges { if err = commitManuallyStagedChangesForStep(ctx, step); err != nil { - return "", err + return newRebaseError(err) } continue } @@ -672,20 +705,20 @@ func continueRebase(ctx *sql.Context) (string, error) { // go ahead and execute this step. if !rebasingStarted || rebaseStepOrder > lastAttemptedStep { if err = recordCurrentStep(ctx, step); err != nil { - return "", err + return newRebaseError(err) } doltSession := dsess.DSessFromSess(ctx.Session) workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) if err != nil { - return "", err + return newRebaseError(err) } - err = processRebasePlanStep(ctx, &step, + result := processRebasePlanStep(ctx, &step, workingSet.RebaseState().CommitBecomesEmptyHandling(), workingSet.RebaseState().EmptyCommitHandling()) - if err != nil { - return "", err + if result.err != nil || result.status != 0 || result.halt { + return result } } @@ -693,7 +726,7 @@ func continueRebase(ctx *sql.Context) (string, error) { if doltSession.GetTransaction() == nil { _, err = doltSession.StartTransaction(ctx, sql.ReadWrite) if err != nil { - return "", err + return newRebaseError(err) } } } @@ -701,11 +734,11 @@ func continueRebase(ctx *sql.Context) (string, error) { // Update the branch being rebased to point to the same commit as our temporary working branch rebaseBranchWorkingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) if err != nil { - return "", err + return newRebaseError(err) } dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) if !ok { - return "", fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase()) + return newRebaseError(fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase())) } rebaseBranch := rebaseBranchWorkingSet.RebaseState().Branch() @@ -714,7 +747,7 @@ func continueRebase(ctx *sql.Context) (string, error) { // Check that the branch being rebased hasn't been updated since the rebase started err = validateRebaseBranchHasntChanged(ctx, rebaseBranch, rebaseBranchWorkingSet.RebaseState()) if err != nil { - return "", err + return newRebaseError(err) } // TODO: copyABranch (and the underlying call to doltdb.NewBranchAtCommit) has a race condition @@ -725,32 +758,38 @@ func continueRebase(ctx *sql.Context) (string, error) { // database.CommitWithWorkingSet, since it updates a branch head and working set atomically. err = copyABranch(ctx, dbData, rebaseWorkingBranch, rebaseBranch, true, false, nil) if err != nil { - return "", err + return newRebaseError(err) } // Checkout the branch being rebased previousBranchWorkingSetRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseBranchWorkingSet.RebaseState().Branch())) if err != nil { - return "", err + return newRebaseError(err) } err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), previousBranchWorkingSetRef) if err != nil { - return "", err + return newRebaseError(err) } // Start a new transaction so the session will see the changes to the branch pointer if _, err = doltSession.StartTransaction(ctx, sql.ReadWrite); err != nil { - return "", err + return newRebaseError(err) } // delete the temporary working branch dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) if !ok { - return "", fmt.Errorf("unable to lookup dbdata") + return newRebaseError(fmt.Errorf("unable to lookup dbdata")) } - return rebaseBranch, actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{ + + err = actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{ Force: true, }, doltSession.Provider(), nil) + if err != nil { + return newRebaseError(err) + } + + return newRebaseSuccess(SuccessfulRebaseMessage + rebaseBranch) } // commitManuallyStagedChangesForStep handles committing staged changes after a conflict has been manually @@ -817,8 +856,12 @@ func commitManuallyStagedChangesForStep(ctx *sql.Context, step rebase.RebasePlan return err } -func processRebasePlanStep(ctx *sql.Context, planStep *rebase.RebasePlanStep, - commitBecomesEmptyHandling doltdb.EmptyCommitHandling, emptyCommitHandling doltdb.EmptyCommitHandling) error { +func processRebasePlanStep( + ctx *sql.Context, + planStep *rebase.RebasePlanStep, + commitBecomesEmptyHandling doltdb.EmptyCommitHandling, + emptyCommitHandling doltdb.EmptyCommitHandling, +) rebaseResult { // Make sure we have a transaction opened for the session // NOTE: After our first call to cherry-pick, the tx is committed, so a new tx needs to be started // as we process additional rebase actions. @@ -826,18 +869,18 @@ func processRebasePlanStep(ctx *sql.Context, planStep *rebase.RebasePlanStep, if doltSession.GetTransaction() == nil { _, err := doltSession.StartTransaction(ctx, sql.ReadWrite) if err != nil { - return err + return newRebaseError(err) } } // If the action is "drop", then we don't need to do anything if planStep.Action == rebase.RebaseActionDrop { - return nil + return newRebaseSuccess("") } options, err := createCherryPickOptionsForRebaseStep(ctx, planStep, commitBecomesEmptyHandling, emptyCommitHandling) if err != nil { - return err + return newRebaseError(err) } return handleRebaseCherryPick(ctx, planStep, *options) @@ -851,9 +894,11 @@ func createCherryPickOptionsForRebaseStep(ctx *sql.Context, planStep *rebase.Reb options.EmptyCommitHandling = emptyCommitHandling switch planStep.Action { - case rebase.RebaseActionDrop, rebase.RebaseActionPick: - // Nothing to do – the drop action doesn't result in a cherry pick and the pick action - // doesn't require any special options (i.e. no amend, no custom commit message). + case rebase.RebaseActionDrop, rebase.RebaseActionPick, rebase.RebaseActionEdit: + // No special cherry-pick options required. + // Drop action doesn't result in a cherry pick. + // Pick action doesn't require any special options (i.e. no amend, no custom commit message). + // Edit action is a straightforward cherry-pick, with a pause after. case rebase.RebaseActionReword: options.CommitMessage = planStep.CommitMsg @@ -877,10 +922,13 @@ func createCherryPickOptionsForRebaseStep(ctx *sql.Context, planStep *rebase.Reb } // handleRebaseCherryPick runs a cherry-pick for the specified |commitHash|, using the specified -// cherry-pick |options| and checks the results for any errors or merge conflicts. If a data conflict -// is detected, then the ErrRebaseDataConflict error is returned. If a schema conflict is detected, -// then the ErrRebaseSchemaConflict error is returned. -func handleRebaseCherryPick(ctx *sql.Context, planStep *rebase.RebasePlanStep, options cherry_pick.CherryPickOptions) error { +// cherry-pick |options| and checks the results for any errors or merge conflicts. Returns a +// rebaseResult indicating success, pause (for conflicts or edit actions), or error. +func handleRebaseCherryPick( + ctx *sql.Context, + planStep *rebase.RebasePlanStep, + options cherry_pick.CherryPickOptions, +) rebaseResult { _, mergeResult, err := cherry_pick.CherryPick(ctx, planStep.CommitHash, options) // TODO: rebase doesn't support schema conflict resolution yet. Ideally, when a schema conflict @@ -889,15 +937,15 @@ func handleRebaseCherryPick(ctx *sql.Context, planStep *rebase.RebasePlanStep, o var schemaConflict merge.SchemaConflict if errors.As(err, &schemaConflict) { if abortErr := abortRebase(ctx); abortErr != nil { - return ErrRebaseConflictWithAbortError.New(planStep.CommitHash, abortErr) + return newRebaseError(ErrRebaseConflictWithAbortError.New(planStep.CommitHash, abortErr)) } - return ErrRebaseSchemaConflict.New(planStep.CommitHash) + return newRebaseError(ErrRebaseSchemaConflict.New(planStep.CommitHash)) } doltSession := dsess.DSessFromSess(ctx.Session) if mergeResult != nil && mergeResult.HasMergeArtifacts() { if err := validateConflictsCanBeResolved(ctx, planStep); err != nil { - return err + return newRebaseError(err) } // If @@dolt_allow_commit_conflicts is enabled, then we need to make a SQL commit here, which @@ -906,27 +954,35 @@ func handleRebaseCherryPick(ctx *sql.Context, planStep *rebase.RebasePlanStep, o // conflicts within the same session, so in that case, we do NOT make a SQL commit. allowCommitConflictsEnabled, err := isAllowCommitConflictsEnabled(ctx) if err != nil { - return err + return newRebaseError(err) } if allowCommitConflictsEnabled { if doltSession.GetTransaction() == nil { _, err := doltSession.StartTransaction(ctx, sql.ReadWrite) if err != nil { - return err + return newRebaseError(err) } } err = doltSession.CommitTransaction(ctx, doltSession.GetTransaction()) if err != nil { - return err + return newRebaseError(err) } } - // Otherwise, let the caller know about the conflict and how to resolve - return ErrRebaseDataConflict.New(planStep.CommitHash, planStep.CommitMsg) + // Return error for data conflict - this is a failure state that requires resolution + return newRebaseError(ErrRebaseDataConflict.New(planStep.CommitHash, planStep.CommitMsg)) } - return err + // If this is an edit action and no conflicts occurred, pause the rebase to allow user modifications + if planStep.Action == rebase.RebaseActionEdit && err == nil { + return newRebasePause(createEditPauseMessage(planStep.CommitHash, planStep.CommitMsg)) + } + + if err != nil { + return newRebaseError(err) + } + return newRebaseSuccess("") } // squashCommitMessage looks up the commit at HEAD and the commit identified by |nextCommitHash| and squashes their two diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go index 401236f28c7..3f6dec90d4d 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go @@ -15,6 +15,9 @@ package enginetest import ( + "regexp" + + "github.com/dolthub/go-mysql-server/enginetest" "github.com/dolthub/go-mysql-server/enginetest/queries" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/plan" @@ -24,6 +27,22 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dprocedures" ) +// editPauseMessageValidator validates edit pause message format +type editPauseMessageValidator struct{} + +var _ enginetest.CustomValueValidator = &editPauseMessageValidator{} +var editPauseRegex = regexp.MustCompile(`^edit action paused at commit [0-9a-v]{32} \(.+\)\.\s+You can now modify the working directory and stage changes\. When ready, continue the rebase by calling dolt_rebase\('--continue'\)$`) + +func (epmv *editPauseMessageValidator) Validate(val interface{}) (bool, error) { + message, ok := val.(string) + if !ok { + return false, nil + } + return editPauseRegex.MatchString(message), nil +} + +var editPauseMessage = &editPauseMessageValidator{} + var DoltRebaseScriptTests = []queries.ScriptTest{ { Name: "dolt_rebase errors: basic errors", @@ -149,6 +168,174 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ }, }, }, + { + Name: "dolt_rebase: edit action functionality test", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t on main');", + "call dolt_branch('feature');", + "call dolt_checkout('feature');", + "insert into t values (1);", + "call dolt_commit('-am', 'feature commit 1');", + "call dolt_rebase('--interactive', 'main');", + "update dolt_rebase set action = 'edit' where rebase_order = 1;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + // Verify edit action was set + Query: "select action from dolt_rebase where rebase_order = 1;", + Expected: []sql.Row{{"edit"}}, + }, + { + // Continue rebase - should pause at the edit action + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, editPauseMessage}}, + }, + { + // Continue again - should complete since no changes were made + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/feature"}}, + }, + }, + }, + { + Name: "dolt_rebase: multiple edit actions", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t on main');", + "call dolt_branch('feature2');", + "call dolt_checkout('feature2');", + "insert into t values (10);", + "call dolt_commit('-am', 'commit 1');", + "insert into t values (20);", + "call dolt_commit('-am', 'commit 2');", + "call dolt_rebase('--interactive', 'main');", + "update dolt_rebase set action = 'edit';", + }, + Assertions: []queries.ScriptTestAssertion{ + { + // First edit pause + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, editPauseMessage}}, + }, + { + // Continue first edit without changes - should pause at second edit + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, editPauseMessage}}, + }, + { + // Continue second edit to complete rebase + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/feature2"}}, + }, + }, + }, + { + Name: "dolt_rebase: edit action followed by conflict", + SetUpScript: []string{ + "set @@dolt_allow_commit_conflicts = 1;", + "create table t (pk int primary key, val varchar(100));", + "insert into t values (1, 'original1'), (2, 'original2'), (3, 'original3');", + "call dolt_commit('-Am', 'initial table with data');", + "call dolt_branch('feature');", + // Update a row on main. Will conflict below. + "update t set val = 'main_updated_row2' where pk = 2;", + "call dolt_commit('-am', 'main updates row 2');", + // update row on feature branch (no conflict) + "call dolt_checkout('feature');", + "update t set val = 'feature_updated_row1' where pk = 1;", + "call dolt_commit('-am', 'feature updates row 1');", + + // update row on feature branch that will conflict with main + "update t set val = 'feature_updated_row2' where pk = 2;", + "call dolt_commit('-am', 'feature updates row 2');", + + "call dolt_rebase('--interactive', 'main');", + "update dolt_rebase set action = 'edit' where rebase_order = 1;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + // Verify the rebase plan is set up correctly + Query: "select rebase_order, action, commit_message from dolt_rebase order by rebase_order;", + Expected: []sql.Row{ + {"1", "edit", "feature updates row 1"}, + {"2", "pick", "feature updates row 2"}, + }, + }, + { + // Continue rebase - should pause at the edit action + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, editPauseMessage}}, + }, + { + // Verify we can see the current state during edit, main's change should be visible. + Query: "select * from t order by pk;", + Expected: []sql.Row{ + {1, "feature_updated_row1"}, + {2, "main_updated_row2"}, + {3, "original3"}}, + }, + { + Query: "update t set val = 'edited_during_rebase_row3' where pk = 3;", + SkipResultsCheck: true, + }, + { + Query: "call dolt_add('t');", + SkipResultsCheck: true, + }, + { + Query: "call dolt_commit('--amend', '-m', 'feature updates row 1 and 3 (edited)');", + SkipResultsCheck: true, + }, + { + // Continue - the following pick should have a conflict error + Query: "call dolt_rebase('--continue');", + ExpectedErr: dprocedures.ErrRebaseDataConflict, + }, + { + // Verify conflict is detected + Query: "select count(*) from dolt_conflicts;", + Expected: []sql.Row{{1}}, + }, + { + // Verify the conflict is on the expected table + Query: "select * from dolt_conflicts;", + Expected: []sql.Row{{"t", uint64(1)}}, + }, + { + // Resolve the conflict by choosing the feature branch version + Query: "delete from dolt_conflicts_t;", + SkipResultsCheck: true, + }, + { + Query: "update t set val = 'resolved_conflict_row2' where pk = 2;", + SkipResultsCheck: true, + }, + { + Query: "call dolt_add('t');", + Expected: []sql.Row{{0}}, + }, + { + // Continue rebase after resolving conflict. Since everything is staged, + // this will grab the commit details from the 'pick'ed commit. + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/feature"}}, + }, + { + // Verify final state shows our edit and conflict resolution + Query: "select * from t order by pk;", + Expected: []sql.Row{ + {1, "feature_updated_row1"}, + {2, "resolved_conflict_row2"}, + {3, "edited_during_rebase_row3"}}, + }, + { + // Verify commit history shows our picked commit with original message + Query: "select message from dolt_log limit 1;", + Expected: []sql.Row{{"feature updates row 2"}}, + }, + }, + }, { Name: "dolt_rebase errors: rebase working branch already exists", SetUpScript: []string{ @@ -832,8 +1019,8 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ { Query: "select * from dolt_rebase order by rebase_order ASC;", Expected: []sql.Row{ - {"1", "pick", doltCommit, "inserting row 1 on branch1"}, - {"2", "pick", doltCommit, "updating row 1 on branch1"}, + {"1", "pick", doltCommit, "inserting row 1 on branch1"}, // This will be moved to the end. + {"2", "pick", doltCommit, "updating row 1 on branch1"}, // so this update is the first picked commit, and it conflicts. {"3", "pick", doltCommit, "updating row 1, again, on branch1"}, }, }, @@ -905,6 +1092,8 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ Expected: []sql.Row{{0}}, }, { + // This will commit the change, and proceed to the next pick, which also modifies row 1 + // Another conflict. Query: "call dolt_rebase('--continue');", ExpectedErr: dprocedures.ErrRebaseDataConflict, }, @@ -1225,6 +1414,7 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ Query: "call dolt_rebase('--continue');", ExpectedErr: dprocedures.ErrRebaseDataConflict, }, + { Query: "select * from t;", Expected: []sql.Row{{0, "zero"}, {999, "nines"}}, @@ -1679,6 +1869,167 @@ commit }, }, }, + { + Name: "dolt_rebase: comprehensive edit and conflict workflow", + SetUpScript: []string{ + "set @@dolt_allow_commit_conflicts = 1;", + "create table t1 (pk int primary key, val int);", + "insert into t1 values (1, 1), (5, 5);", + "call dolt_commit('-Am', 'main commit 1');", + "call dolt_branch('b1');", + + // Create commits on b1 branch that will be rebased + "call dolt_checkout('b1');", + "update t1 set val = 11 where pk = 1;", // Modification of data which should apply without conflict + "call dolt_commit('-am', 'b1 commit 1 - to edit');", + "insert into t1 values (10, 100);", + "call dolt_commit('-am', 'b1 commit 2 - will conflict');", + "insert into t1 values (20, 200);", + "call dolt_commit('-am', 'b1 commit 3 - clean apply');", + "insert into t1 values (30, 300);", + "call dolt_commit('-am', 'b1 commit 4 - edit at end');", + + // Add conflicting data to main + "call dolt_checkout('main');", + "insert into t1 values (10, 999);", // This will conflict with b1 commit 2 + "call dolt_commit('-am', 'main conflicting data');", + + // Start interactive rebase + "call dolt_checkout('b1');", + "call dolt_rebase('-i', 'main');", + + // Set up rebase plan: edit, pick (conflict), edit + "update dolt_rebase set action = 'edit' where rebase_order = 1;", + "update dolt_rebase set action = 'edit' where rebase_order = 4;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + // Verify rebase plan is set up correctly + Query: "select rebase_order, action, commit_message from dolt_rebase order by rebase_order;", + Expected: []sql.Row{ + {"1", "edit", "b1 commit 1 - to edit"}, + {"2", "pick", "b1 commit 2 - will conflict"}, + {"3", "pick", "b1 commit 3 - clean apply"}, + {"4", "edit", "b1 commit 4 - edit at end"}, + }, + }, + { + // Start rebase - should pause at first edit + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, editPauseMessage}}, + }, + { + // Verify we're on the rebase working branch + Query: "select active_branch();", + Expected: []sql.Row{{"dolt_rebase_b1"}}, + }, + { + // Verify initial state during first edit - main's data (5,5),(10,999) should be visible, + // as well as b1s first edit (1,11). + Query: "select pk, val from t1 order by pk;", + Expected: []sql.Row{ + {1, 11}, {5, 5}, {10, 999}, + }, + }, + { + // Make an edit during the pause + Query: "update t1 set val = 55 where pk = 5;", + SkipResultsCheck: true, + }, + { + Query: "call dolt_add('t1');", + SkipResultsCheck: true, + }, + { + Query: "call dolt_commit('--amend', '-m', 'b1 commit 1 - to edit (modified)');", + SkipResultsCheck: true, + }, + { + // Continue from first edit - should hit conflict on step 2 + Query: "call dolt_rebase('--continue');", + ExpectedErr: dprocedures.ErrRebaseDataConflict, + // The error message returned is long, and contains a commit id. We need a better regex + // match in the testing harness to validate this error. + // ExpectedErrStr: "data conflict detected while rebasing commit sn5pdhug6aaccvoue7ejp759aafbb1jn.....", + }, + { + // Verify we have conflicts + Query: "select count(*) from dolt_conflicts;", + Expected: []sql.Row{{1}}, + }, + { + Query: "select our_pk, our_val, their_pk, their_val from dolt_conflicts_t1;", + Expected: []sql.Row{{10, 999, 10, 100}}, + }, + { + // Resolve conflict deleting conflict table row and updating pk=10 to 200. + Query: "delete from dolt_conflicts_t1;", + SkipResultsCheck: true, + }, + { + Query: "update t1 set val = 200 where pk = 10;", + SkipResultsCheck: true, + }, + { + Query: "call dolt_add('t1');", + SkipResultsCheck: true, + }, + { + // Continue at this stage will finish the commit, there is another edit after this. + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, editPauseMessage}}, + }, + { + // Verify we're still on rebase working branch + Query: "select active_branch();", + Expected: []sql.Row{{"dolt_rebase_b1"}}, + }, + { + // Verify state includes all changes: edit from step 1, conflict resolution from step 2, and step 3 + Query: "select pk, val from t1 order by pk;", + Expected: []sql.Row{ + {1, 11}, {5, 55}, {10, 200}, {20, 200}, {30, 300}, + }, + }, + { + // We are in the edit state. Create another commit on top, without actually editing the current one. + Query: "insert into t1 values (42, 24);", + SkipResultsCheck: true, + }, + { + Query: "call dolt_add('t1');", + SkipResultsCheck: true, + }, + { + Query: "call dolt_commit('-am', 'b1 commit 5 - additional commit during edit');", + SkipResultsCheck: true, + }, + { + // Continue from final edit - should complete successfully + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/b1"}}, + }, + { + // Verify we're back on the original branch + Query: "select active_branch();", + Expected: []sql.Row{{"b1"}}, + }, + { + // Verify all the log messages we expect are present. + Query: "select message from dolt_log;", + Expected: []sql.Row{ + {"b1 commit 5 - additional commit during edit"}, + {"b1 commit 4 - edit at end"}, + {"b1 commit 3 - clean apply"}, + {"b1 commit 2 - will conflict"}, + {"b1 commit 1 - to edit (modified)"}, + {"main conflicting data"}, + {"main commit 1"}, + {"Initialize data repository"}, + }, + }, + }, + }, } var DoltRebaseMultiSessionScriptTests = []queries.ScriptTest{ diff --git a/integration-tests/bats/rebase.bats b/integration-tests/bats/rebase.bats index a3ab3329b47..71122b3c0d6 100755 --- a/integration-tests/bats/rebase.bats +++ b/integration-tests/bats/rebase.bats @@ -38,6 +38,12 @@ setupCustomEditorScript() { export DOLT_TEST_FORCE_OPEN_EDITOR="1" } +getHeadHash() { + run dolt sql -r csv -q "select commit_hash from dolt_log limit 1 offset 0;" + [ "$status" -eq 0 ] || return 1 + echo "${lines[1]}" +} + @test "rebase: no rebase in progress errors" { run dolt rebase --abort [ "$status" -eq 1 ] @@ -116,9 +122,7 @@ setupCustomEditorScript() { setupCustomEditorScript "rebasePlan.txt" dolt checkout b1 - run dolt show head - [ "$status" -eq 0 ] - COMMIT1=${lines[0]:12:32} + COMMIT1="$(getHeadHash)" touch rebasePlan.txt echo "pick $COMMIT1 b1 commit 1" >> rebasePlan.txt @@ -238,45 +242,31 @@ message" setupCustomEditorScript "multiStepPlan.txt" dolt checkout b1 - run dolt show head - [ "$status" -eq 0 ] - COMMIT1=${lines[0]:12:32} + COMMIT1="$(getHeadHash)" dolt sql -q "insert into t2 values (1);" dolt commit -am "b1 commit 2" - run dolt show head - [ "$status" -eq 0 ] - COMMIT2=${lines[0]:12:32} + COMMIT2="$(getHeadHash)" dolt sql -q "insert into t2 values (2);" dolt commit -am "b1 commit 3" - run dolt show head - [ "$status" -eq 0 ] - COMMIT3=${lines[0]:12:32} + COMMIT3="$(getHeadHash)" dolt sql -q "insert into t2 values (3);" dolt commit -am "b1 commit 4" - run dolt show head - [ "$status" -eq 0 ] - COMMIT4=${lines[0]:12:32} + COMMIT4="$(getHeadHash)" dolt sql -q "insert into t2 values (4);" dolt commit -am "b1 commit 5" - run dolt show head - [ "$status" -eq 0 ] - COMMIT5=${lines[0]:12:32} + COMMIT5="$(getHeadHash)" dolt sql -q "insert into t2 values (5);" dolt commit -am "b1 commit 6" - run dolt show head - [ "$status" -eq 0 ] - COMMIT6=${lines[0]:12:32} + COMMIT6="$(getHeadHash)" dolt sql -q "insert into t2 values (6);" dolt commit -am "b1 commit 7" - run dolt show head - [ "$status" -eq 0 ] - COMMIT7=${lines[0]:12:32} + COMMIT7="$(getHeadHash)" touch multiStepPlan.txt echo "pick $COMMIT1 b1 commit 1" >> multiStepPlan.txt @@ -317,9 +307,7 @@ message" dolt sql -q "CREATE table t3 (pk int primary key);" dolt add t3 dolt commit -m "b2 commit 1" - run dolt show head - [ "$status" -eq 0 ] - COMMIT1=${lines[0]:12:32} + COMMIT1="$(getHeadHash)" dolt sql -q "insert into t3 values (1);" dolt commit -am "b2 commit 2" @@ -327,9 +315,7 @@ message" dolt commit -am "b2 commit 3" dolt checkout b1 - run dolt show head - [ "$status" -eq 0 ] - COMMIT2=${lines[0]:12:32} + COMMIT2="$(getHeadHash)" touch nonStandardPlan.txt echo "pick $COMMIT1 b2 commit 1" >> nonStandardPlan.txt @@ -390,6 +376,7 @@ message" [[ "$output" =~ "data conflict detected while rebasing commit" ]] || false [[ "$output" =~ "b1 commit 2" ]] || false + # Assert that we are on the rebase working branch (not the branch being rebased) run dolt sql -q "select active_branch();" [ "$status" -eq 0 ] @@ -540,3 +527,192 @@ message" run dolt log [[ $output =~ "repeating change from main on b1" ]] || false } + +@test "rebase: edit action basic functionality" { + # Get the commit hash for b1 commit 1 using the established pattern + dolt checkout b1 + COMMIT1="$(getHeadHash)" + + # Create a rebase plan file with an edit action + touch rebase_plan.txt + echo "edit $COMMIT1 b1 commit 1" >> rebase_plan.txt + + setupCustomEditorScript rebase_plan.txt + + # Start interactive rebase with edit action - should pause for editing + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "edit action paused at commit" ]] || false + [[ "$output" =~ "You can now modify the working directory" ]] || false + + # Assert that we are on the rebase working branch (not the branch being rebased) + run dolt sql -q "select active_branch();" + [ "$status" -eq 0 ] + [[ "$output" =~ " dolt_rebase_b1 " ]] || false + + # Continue the rebase after the edit pause + run dolt rebase --continue + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + # Assert that we are on the original working branch + run dolt sql -q "select active_branch();" + [ "$status" -eq 0 ] + [[ "$output" =~ "b1" ]] || false + ! [[ "$output" =~ "dolt_rebase_b1" ]] || false +} + +@test "rebase: complex edit and conflict scenario" { + # Setup: Create conflicting data on main (for commit 2) + dolt sql -q "INSERT INTO t1 VALUES (10, 100);" + dolt commit -am "main conflicting data" + + # Setup: Create 4 commits on b1 to rebase + dolt checkout b1 + + # Commit 1: Will be edited (no conflict with main) + dolt sql -q "INSERT INTO t1 VALUES (5, 50);" + dolt commit -am "b1 commit 1 - to edit" + COMMIT1="$(getHeadHash)" + + # Commit 2: Will conflict with main + dolt sql -q "INSERT INTO t1 VALUES (10, 200);" # Conflicts with main's (10, 100) + dolt commit -am "b1 commit 2 - will conflict" + COMMIT2="$(getHeadHash)" + + # Commit 3: Clean apply after conflict + dolt sql -q "INSERT INTO t1 VALUES (20, 300);" + dolt commit -am "b1 commit 3 - clean apply" + COMMIT3="$(getHeadHash)" + + # Commit 4: Will be edited at the end + dolt sql -q "INSERT INTO t1 VALUES (30, 400);" + dolt commit -am "b1 commit 4 - edit at end" + COMMIT4="$(getHeadHash)" + + # Create rebase plan: edit, pick (conflict), pick, edit + setupCustomEditorScript "complex_rebase_plan.txt" + touch complex_rebase_plan.txt + echo "edit $COMMIT1 b1 commit 1 - to edit" >> complex_rebase_plan.txt + echo "pick $COMMIT2 b1 commit 2 - will conflict" >> complex_rebase_plan.txt + echo "pick $COMMIT3 b1 commit 3 - clean apply" >> complex_rebase_plan.txt + echo "edit $COMMIT4 b1 commit 4 - edit at end" >> complex_rebase_plan.txt + + # Start the rebase - should pause at first edit + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "edit action paused at commit" ]] || false + + # Assert we're on the rebase working branch + run dolt sql -q "select active_branch();" + [ "$status" -eq 0 ] + [[ "$output" =~ " dolt_rebase_b1 " ]] || false + + # Make changes during edit pause and add another commit + dolt sql -q "UPDATE t1 SET c = 55 WHERE pk = 5;" + dolt commit -a --amend -m "edit modification to commit 1" + + dolt sql -q "INSERT INTO t1 VALUES (15, 150);" + dolt add t1 + dolt commit -m "edit modification to commit 1" + + # Continue rebase - should hit conflict on commit 2 + run dolt rebase --continue + [ "$status" -eq 1 ] + [[ "$output" =~ "data conflict detected while rebasing commit" ]] || false + [[ "$output" =~ "b1 commit 2 - will conflict" ]] || false + + # Resolve the conflict + run dolt conflicts cat . + [ "$status" -eq 0 ] + [[ "$output" =~ "+ | ours | 10 | 100 | NULL | NULL | NULL | NULL |" ]] || false + [[ "$output" =~ "+ | theirs | 10 | 200 | NULL | NULL | NULL | NULL |" ]] || false + dolt conflicts resolve --theirs t1 + dolt add t1 + + # Continue rebase - should proceed to commit 3 then pause at commit 4 edit + run dolt rebase --continue + [ "$status" -eq 0 ] + [[ "$output" =~ "edit action paused at commit" ]] || false + [[ "$output" =~ "b1 commit 4 - edit at end" ]] || false + + # We're still on the rebase working branch + run dolt sql -q "select active_branch();" + [ "$status" -eq 0 ] + [[ "$output" =~ " dolt_rebase_b1 " ]] || false + + # Continue from edit pause - should complete successfully + run dolt rebase --continue + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + # Verify we're back on the original branch + run dolt sql -q "select active_branch();" + [ "$status" -eq 0 ] + [[ "$output" =~ "b1" ]] || false + ! [[ "$output" =~ "dolt_rebase_b1" ]] || false + + # Count commits between main and HEAD - should be 5 + # (original 4 commits + 1 edit modification commit) + run dolt log --oneline main..HEAD + [ "$status" -eq 0 ] + # Count the lines (each commit is one line) + commit_count=$(echo "$output" | wc -l) + [ "$commit_count" -eq 5 ] + + # Verify the data is correct after all operations + run dolt sql -q "SELECT * FROM t1 ORDER BY pk;" + [ "$status" -eq 0 ] + [[ "$output" =~ "| 1 | 1 |" ]] || false # Original main data + [[ "$output" =~ "| 5 | 55 |" ]] || false # Modified during edit + [[ "$output" =~ "| 10 | 200 |" ]] || false # Conflict resolved to theirs + [[ "$output" =~ "| 20 | 300 |" ]] || false # From commit 3 + [[ "$output" =~ "| 30 | 400 |" ]] || false # From commit 4 + + # Verify no rebase state remains + run dolt status + [ "$status" -eq 0 ] + ! [[ "$output" =~ "rebase in progress" ]] || false +} + +@test "rebase: short action forms should be recognized" { + setupCustomEditorScript "shortActionsPlan.txt" + + dolt checkout b1 + COMMIT1="$(getHeadHash)" + + dolt sql -q "insert into t2 values (1);" + dolt commit -am "b1 commit 2" + COMMIT2="$(getHeadHash)" + + dolt sql -q "insert into t2 values (2);" + dolt commit -am "b1 commit 3" + COMMIT3="$(getHeadHash)" + + touch shortActionsPlan.txt + echo "p $COMMIT1 b1 commit 1" >> shortActionsPlan.txt + echo "r $COMMIT2 reworded commit" >> shortActionsPlan.txt + echo "f $COMMIT3 b1 commit 3" >> shortActionsPlan.txt + + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt log --oneline + [ "$status" -eq 0 ] + [[ "$output" =~ "reworded commit" ]] || false +} + +@test "rebase: invalid action forms should be rejected with clear error" { + setupCustomEditorScript "invalidActionsPlan.txt" + + dolt checkout b1 + COMMIT1="$(getHeadHash)" + + touch invalidActionsPlan.txt + echo "invalid $COMMIT1 b1 commit 1" >> invalidActionsPlan.txt + + run dolt rebase -i main + [ "$status" -ne 0 ] + [[ "$output" =~ "unknown action in rebase plan: invalid" ]] || false +}