diff --git a/go/cmd/dolt/commands/commit.go b/go/cmd/dolt/commands/commit.go index 8edd2f72cf..7fb7caf6ef 100644 --- a/go/cmd/dolt/commands/commit.go +++ b/go/cmd/dolt/commands/commit.go @@ -359,7 +359,7 @@ func getCommitMessageFromEditor(sqlCtx *sql.Context, queryist cli.Queryist, sugg editorStr := cliCtx.Config().GetStringOrDefault(config.DoltEditor, backupEd) cli.ExecuteWithStdioRestored(func() { - commitMsg, cErr := editor.OpenCommitEditor(editorStr, initialMsg) + commitMsg, cErr := editor.OpenTempEditor(editorStr, initialMsg) if cErr != nil { err = cErr } diff --git a/go/cmd/dolt/commands/rebase.go b/go/cmd/dolt/commands/rebase.go new file mode 100644 index 0000000000..c03a15e38e --- /dev/null +++ b/go/cmd/dolt/commands/rebase.go @@ -0,0 +1,341 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/gocraft/dbr/v2" + "github.com/gocraft/dbr/v2/dialect" + + "github.com/dolthub/dolt/go/cmd/dolt/cli" + "github.com/dolthub/dolt/go/cmd/dolt/errhand" + eventsapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi/v1alpha1" + "github.com/dolthub/dolt/go/libraries/doltcore/dconfig" + "github.com/dolthub/dolt/go/libraries/doltcore/env" + "github.com/dolthub/dolt/go/libraries/doltcore/rebase" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dprocedures" + "github.com/dolthub/dolt/go/libraries/utils/argparser" + "github.com/dolthub/dolt/go/libraries/utils/config" + "github.com/dolthub/dolt/go/libraries/utils/editor" +) + +var rebaseDocs = cli.CommandDocumentationContent{ + ShortDesc: "Reapplies commits on top of another base tip", + LongDesc: `Rewrites commit history for the current branch by replaying commits, allowing the commits to be reordered, +squashed, or dropped. The commits included in the rebase plan are the commits reachable by the current branch, but NOT +reachable from the branch specified as the argument when starting a rebase (also known as the upstream branch). This is +the same as Git and Dolt's "two dot log" syntax, or |upstreamBranch|..|currentBranch|. + +Rebasing is useful to clean and organize your commit history, especially before merging a feature branch back to a shared +branch. For example, you can drop commits that contain debugging or test changes, or squash or fixup small commits into a +single commit, or reorder commits so that related changes are adjacent in the new commit history. +`, + Synopsis: []string{ + `(-i | --interactive) {{.LessThan}}upstream{{.GreaterThan}}`, + `(--continue | --abort)`, + }, +} + +type RebaseCmd struct{} + +var _ cli.Command = RebaseCmd{} + +// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command +func (cmd RebaseCmd) Name() string { + return "rebase" +} + +// Description returns a description of the command +func (cmd RebaseCmd) Description() string { + return rebaseDocs.ShortDesc +} + +// EventType returns the type of the event to log +func (cmd RebaseCmd) EventType() eventsapi.ClientEventType { + return eventsapi.ClientEventType_REBASE +} + +func (cmd RebaseCmd) Docs() *cli.CommandDocumentation { + ap := cmd.ArgParser() + return cli.NewCommandDocumentation(rebaseDocs, ap) +} + +func (cmd RebaseCmd) ArgParser() *argparser.ArgParser { + return cli.CreateRebaseArgParser() +} + +// Exec executes the command +func (cmd RebaseCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int { + ap := cmd.ArgParser() + help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, rebaseDocs, ap)) + apr := cli.ParseArgsOrDie(ap, args, help) + + queryist, sqlCtx, closeFunc, err := cliCtx.QueryEngine(ctx) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if closeFunc != nil { + defer closeFunc() + } + + branchName, err := getActiveBranchName(sqlCtx, queryist) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + + query, err := constructInterpolatedDoltRebaseQuery(apr) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + + rows, err := GetRowsForSql(queryist, sqlCtx, query) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + + status, err := getInt64ColAsInt64(rows[0][0]) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if status == 1 { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("error: "+rows[0][1].(string))), usage) + } + + message := rows[0][1].(string) + if strings.Contains(message, dprocedures.SuccessfulRebaseMessage) { + cli.Println(dprocedures.SuccessfulRebaseMessage + branchName) + } else if strings.Contains(message, dprocedures.RebaseAbortedMessage) { + cli.Println(dprocedures.RebaseAbortedMessage) + } else { + rebasePlan, err := getRebasePlan(cliCtx, sqlCtx, queryist, apr.Arg(0), branchName) + if err != nil { + // attempt to abort the rebase + _, _, _ = queryist.Query(sqlCtx, "CALL DOLT_REBASE('--abort');") + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + + // if all uncommented lines are deleted in the editor, abort the rebase + if rebasePlan == nil || rebasePlan.Steps == nil || len(rebasePlan.Steps) == 0 { + rows, err := GetRowsForSql(queryist, sqlCtx, "CALL DOLT_REBASE('--abort');") + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + status, err := getInt64ColAsInt64(rows[0][0]) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if status == 1 { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("error: "+rows[0][1].(string))), usage) + } + + cli.Println(dprocedures.RebaseAbortedMessage) + } else { + err = insertRebasePlanIntoDoltRebaseTable(rebasePlan, sqlCtx, queryist) + if err != nil { + // attempt to abort the rebase + _, _, _ = queryist.Query(sqlCtx, "CALL DOLT_REBASE('--abort');") + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + + rows, err := GetRowsForSql(queryist, sqlCtx, "CALL DOLT_REBASE('--continue');") + if err != nil { + // attempt to abort the rebase + _, _, _ = queryist.Query(sqlCtx, "CALL DOLT_REBASE('--abort');") + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + status, err := getInt64ColAsInt64(rows[0][0]) + if err != nil { + // attempt to abort the rebase + _, _, _ = queryist.Query(sqlCtx, "CALL DOLT_REBASE('--abort');") + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if status == 1 { + // attempt to abort the rebase + _, _, _ = queryist.Query(sqlCtx, "CALL DOLT_REBASE('--abort');") + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("error: "+rows[0][1].(string))), usage) + } + + cli.Println(dprocedures.SuccessfulRebaseMessage + branchName) + } + } + + return HandleVErrAndExitCode(nil, usage) +} + +// constructInterpolatedDoltRebaseQuery generates the sql query necessary to call the DOLT_REBASE() function. +// Also interpolates this query to prevent sql injection. +func constructInterpolatedDoltRebaseQuery(apr *argparser.ArgParseResults) (string, error) { + var params []interface{} + var args []string + + if apr.NArg() == 1 { + params = append(params, apr.Arg(0)) + args = append(args, "?") + } + if apr.Contains(cli.InteractiveFlag) { + args = append(args, "'--interactive'") + } + if apr.Contains(cli.ContinueFlag) { + args = append(args, "'--continue'") + } + if apr.Contains(cli.AbortParam) { + args = append(args, "'--abort'") + } + + query := fmt.Sprintf("CALL DOLT_REBASE(%s);", strings.Join(args, ", ")) + return dbr.InterpolateForDialect(query, params, dialect.MySQL) +} + +// getRebasePlan opens an editor for users to edit the rebase plan and returns the parsed rebase plan from the editor. +func getRebasePlan(cliCtx cli.CliContext, sqlCtx *sql.Context, queryist cli.Queryist, rebaseBranch, currentBranch string) (*rebase.RebasePlan, error) { + if cli.ExecuteWithStdioRestored == nil { + return nil, nil + } + + if !checkIsTerminal() { + return nil, nil + } + + initialRebaseMsg, err := buildInitialRebaseMsg(sqlCtx, queryist, rebaseBranch, currentBranch) + if err != nil { + return nil, err + } + + backupEd := "vim" + // try getting default editor on the user system + if ed, edSet := os.LookupEnv(dconfig.EnvEditor); edSet { + backupEd = ed + } + // try getting Dolt config core.editor + editorStr := cliCtx.Config().GetStringOrDefault(config.DoltEditor, backupEd) + + var rebaseMsg string + cli.ExecuteWithStdioRestored(func() { + rebaseMsg, err = editor.OpenTempEditor(editorStr, initialRebaseMsg) + }) + if err != nil { + return nil, err + } + + return parseRebaseMessage(rebaseMsg) +} + +// buildInitialRebaseMsg builds the initial message to display to the user when they open the rebase plan editor, +// including the formatted rebase plan. +func buildInitialRebaseMsg(sqlCtx *sql.Context, queryist cli.Queryist, rebaseBranch, currentBranch string) (string, error) { + var buffer bytes.Buffer + + rows, err := GetRowsForSql(queryist, sqlCtx, "SELECT action, commit_hash, commit_message FROM dolt_rebase ORDER BY rebase_order") + if err != nil { + return "", err + } + + // rebase plan + for _, row := range rows { + action, found := getRebaseAction(row[0]) + if !found { + return "", errors.New("invalid rebase action") + } + commitHash := row[1].(string) + commitMessage := row[2].(string) + buffer.WriteString(fmt.Sprintf("%s %s %s\n", action, commitHash, commitMessage)) + } + buffer.WriteString("\n") + + // help text + rebaseBranchHash, err := getHashOf(queryist, sqlCtx, rebaseBranch) + if err != nil { + return "", err + } + currentBranchHash, err := getHashOf(queryist, sqlCtx, currentBranch) + if err != nil { + return "", err + } + numSteps := len(rows) + buffer.WriteString(fmt.Sprintf("# Rebase %s..%s onto %s (%d commands)\n#\n", rebaseBranchHash, currentBranchHash, rebaseBranchHash, numSteps)) + + buffer.WriteString("# Commands:\n") + buffer.WriteString("# p, pick = use commit\n") + buffer.WriteString("# d, drop = remove commit\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") + buffer.WriteString("# These lines can be re-ordered; they are executed from top to bottom.\n") + buffer.WriteString("#\n") + buffer.WriteString("# If you remove a line here THAT COMMIT WILL BE LOST.\n") + buffer.WriteString("#\n") + buffer.WriteString("# However, if you remove everything, the rebase will be aborted.\n") + buffer.WriteString("#\n") + + return buffer.String(), nil +} + +// getRebaseAction returns the rebase action for the given row. This conversion is necessary because a local client +// returns an int representing the enum whereas a remote client properly returns the label. +// TODO: Remove this once the local client returns the label. +func getRebaseAction(col interface{}) (string, bool) { + action, ok := col.(string) + if ok { + return action, true + } else { + return dprocedures.RebaseActionEnumType.At(int(col.(uint16))) + } +} + +// 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{} + splitMsg := strings.Split(rebaseMsg, "\n") + for i, line := range splitMsg { + if !strings.HasPrefix(line, "#") && strings.TrimSpace(line) != "" { + rebaseStepParts := strings.SplitN(line, " ", 3) + if len(rebaseStepParts) != 3 { + return nil, fmt.Errorf("invalid line %d: %s", i, line) + } + plan.Steps = append(plan.Steps, rebase.RebasePlanStep{ + Action: rebaseStepParts[0], + CommitHash: rebaseStepParts[1], + CommitMsg: rebaseStepParts[2], + }) + } + } + + return plan, nil +} + +// insertRebasePlanIntoDoltRebaseTable inserts the rebase plan into the dolt_rebase table by re-building the dolt_rebase +// table from scratch. +func insertRebasePlanIntoDoltRebaseTable(plan *rebase.RebasePlan, sqlCtx *sql.Context, queryist cli.Queryist) error { + _, err := GetRowsForSql(queryist, sqlCtx, "TRUNCATE TABLE dolt_rebase") + if err != nil { + return err + } + + for i, step := range plan.Steps { + _, err := GetRowsForSql(queryist, sqlCtx, fmt.Sprintf("INSERT INTO dolt_rebase VALUES (%d, '%s', '%s', '%s')", i+1, step.Action, step.CommitHash, step.CommitMsg)) + if err != nil { + return err + } + } + + return nil +} diff --git a/go/cmd/dolt/dolt.go b/go/cmd/dolt/dolt.go index 7f4a727c59..48f94778b6 100644 --- a/go/cmd/dolt/dolt.go +++ b/go/cmd/dolt/dolt.go @@ -125,6 +125,7 @@ var doltSubCommands = []cli.Command{ commands.ProfileCmd{}, commands.QueryDiff{}, commands.ReflogCmd{}, + commands.RebaseCmd{}, } var commandsWithoutCliCtx = []cli.Command{ diff --git a/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event.pb.go b/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event.pb.go index 6e09d77b48..757af77960 100644 --- a/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event.pb.go +++ b/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event.pb.go @@ -17,7 +17,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v4.22.0 +// protoc v4.26.0 // source: dolt/services/eventsapi/v1alpha1/client_event.proto package eventsapi diff --git a/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event_grpc.pb.go b/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event_grpc.pb.go index 8a09ad1cf6..f82b5eed9d 100644 --- a/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event_grpc.pb.go +++ b/go/gen/proto/dolt/services/eventsapi/v1alpha1/client_event_grpc.pb.go @@ -17,7 +17,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v4.22.0 +// - protoc v4.26.0 // source: dolt/services/eventsapi/v1alpha1/client_event.proto package eventsapi diff --git a/go/gen/proto/dolt/services/eventsapi/v1alpha1/event_constants.pb.go b/go/gen/proto/dolt/services/eventsapi/v1alpha1/event_constants.pb.go index a180d1b5a3..730def3931 100644 --- a/go/gen/proto/dolt/services/eventsapi/v1alpha1/event_constants.pb.go +++ b/go/gen/proto/dolt/services/eventsapi/v1alpha1/event_constants.pb.go @@ -17,7 +17,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v4.22.0 +// protoc v4.26.0 // source: dolt/services/eventsapi/v1alpha1/event_constants.proto package eventsapi @@ -157,6 +157,7 @@ const ( ClientEventType_PROFILE ClientEventType = 62 ClientEventType_REFLOG ClientEventType = 63 ClientEventType_SQL_SERVER_HEARTBEAT ClientEventType = 64 + ClientEventType_REBASE ClientEventType = 65 ) // Enum value maps for ClientEventType. @@ -227,6 +228,7 @@ var ( 62: "PROFILE", 63: "REFLOG", 64: "SQL_SERVER_HEARTBEAT", + 65: "REBASE", } ClientEventType_value = map[string]int32{ "TYPE_UNSPECIFIED": 0, @@ -294,6 +296,7 @@ var ( "PROFILE": 62, "REFLOG": 63, "SQL_SERVER_HEARTBEAT": 64, + "REBASE": 65, } ) @@ -484,7 +487,7 @@ var file_dolt_services_eventsapi_v1alpha1_event_constants_proto_rawDesc = []byte 0x52, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x49, 0x4e, 0x55, 0x58, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x41, 0x52, 0x57, - 0x49, 0x4e, 0x10, 0x03, 0x2a, 0xa3, 0x08, 0x0a, 0x0f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, + 0x49, 0x4e, 0x10, 0x03, 0x2a, 0xaf, 0x08, 0x0a, 0x0f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x49, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x41, 0x54, @@ -550,28 +553,29 @@ var file_dolt_services_eventsapi_v1alpha1_event_constants_proto_rawDesc = []byte 0x04, 0x53, 0x48, 0x4f, 0x57, 0x10, 0x3d, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x3e, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x46, 0x4c, 0x4f, 0x47, 0x10, 0x3f, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x51, 0x4c, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x48, - 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x40, 0x2a, 0x6a, 0x0a, 0x08, 0x4d, 0x65, - 0x74, 0x72, 0x69, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x12, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, - 0x0a, 0x10, 0x42, 0x59, 0x54, 0x45, 0x53, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, - 0x45, 0x44, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, - 0x5f, 0x4d, 0x53, 0x5f, 0x45, 0x4c, 0x41, 0x50, 0x53, 0x45, 0x44, 0x10, 0x02, 0x12, 0x17, 0x0a, - 0x13, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x41, 0x50, 0x49, 0x5f, 0x52, 0x50, 0x43, 0x5f, 0x45, - 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x2a, 0x45, 0x0a, 0x0b, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, - 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x15, 0x0a, 0x11, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x55, 0x52, 0x4c, 0x5f, 0x53, - 0x43, 0x48, 0x45, 0x4d, 0x45, 0x10, 0x02, 0x22, 0x04, 0x08, 0x01, 0x10, 0x01, 0x2a, 0x3f, 0x0a, - 0x05, 0x41, 0x70, 0x70, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x50, 0x50, 0x5f, 0x49, 0x44, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, - 0x0a, 0x08, 0x41, 0x50, 0x50, 0x5f, 0x44, 0x4f, 0x4c, 0x54, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, - 0x41, 0x50, 0x50, 0x5f, 0x44, 0x4f, 0x4c, 0x54, 0x47, 0x52, 0x45, 0x53, 0x10, 0x02, 0x42, 0x51, - 0x5a, 0x4f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x6f, 0x6c, - 0x74, 0x68, 0x75, 0x62, 0x2f, 0x64, 0x6f, 0x6c, 0x74, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, 0x6f, 0x6c, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x61, 0x70, - 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x45, 0x41, 0x52, 0x54, 0x42, 0x45, 0x41, 0x54, 0x10, 0x40, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, + 0x42, 0x41, 0x53, 0x45, 0x10, 0x41, 0x2a, 0x6a, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x49, 0x44, 0x12, 0x16, 0x0a, 0x12, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x42, 0x59, + 0x54, 0x45, 0x53, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x17, 0x0a, 0x13, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x4d, 0x53, 0x5f, + 0x45, 0x4c, 0x41, 0x50, 0x53, 0x45, 0x44, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x4d, + 0x4f, 0x54, 0x45, 0x41, 0x50, 0x49, 0x5f, 0x52, 0x50, 0x43, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x03, 0x2a, 0x45, 0x0a, 0x0b, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, + 0x44, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, + 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x55, 0x52, 0x4c, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, + 0x45, 0x10, 0x02, 0x22, 0x04, 0x08, 0x01, 0x10, 0x01, 0x2a, 0x3f, 0x0a, 0x05, 0x41, 0x70, 0x70, + 0x49, 0x44, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x50, 0x50, 0x5f, 0x49, 0x44, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x50, + 0x50, 0x5f, 0x44, 0x4f, 0x4c, 0x54, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x41, 0x50, 0x50, 0x5f, + 0x44, 0x4f, 0x4c, 0x54, 0x47, 0x52, 0x45, 0x53, 0x10, 0x02, 0x42, 0x51, 0x5a, 0x4f, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x6f, 0x6c, 0x74, 0x68, 0x75, 0x62, + 0x2f, 0x64, 0x6f, 0x6c, 0x74, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2f, 0x64, 0x6f, 0x6c, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, + 0x68, 0x61, 0x31, 0x3b, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore.pb.go b/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore.pb.go index c08906e3d8..11c0193b9f 100644 --- a/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore.pb.go +++ b/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v4.22.0 +// protoc v4.26.0 // source: dolt/services/remotesapi/v1alpha1/chunkstore.proto package remotesapi diff --git a/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore_grpc.pb.go b/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore_grpc.pb.go index 28cfc20104..a329e73ab0 100644 --- a/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore_grpc.pb.go +++ b/go/gen/proto/dolt/services/remotesapi/v1alpha1/chunkstore_grpc.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v4.22.0 +// - protoc v4.26.0 // source: dolt/services/remotesapi/v1alpha1/chunkstore.proto package remotesapi diff --git a/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials.pb.go b/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials.pb.go index 7c2e689b33..470c29d0a8 100644 --- a/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials.pb.go +++ b/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v4.22.0 +// protoc v4.26.0 // source: dolt/services/remotesapi/v1alpha1/credentials.proto package remotesapi diff --git a/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials_grpc.pb.go b/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials_grpc.pb.go index 8ae0dfc1eb..aa91af6773 100644 --- a/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials_grpc.pb.go +++ b/go/gen/proto/dolt/services/remotesapi/v1alpha1/credentials_grpc.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v4.22.0 +// - protoc v4.26.0 // source: dolt/services/remotesapi/v1alpha1/credentials.proto package remotesapi diff --git a/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication.pb.go b/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication.pb.go index 6ac797942a..a77f74d938 100644 --- a/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication.pb.go +++ b/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v4.22.0 +// protoc v4.26.0 // source: dolt/services/replicationapi/v1alpha1/replication.proto package replicationapi diff --git a/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication_grpc.pb.go b/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication_grpc.pb.go index 275cc34a39..2e6dbb2b62 100644 --- a/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication_grpc.pb.go +++ b/go/gen/proto/dolt/services/replicationapi/v1alpha1/replication_grpc.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v4.22.0 +// - protoc v4.26.0 // source: dolt/services/replicationapi/v1alpha1/replication.proto package replicationapi diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go index b4bb1a4013..ebea10747f 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go @@ -92,6 +92,12 @@ var ErrRebaseConflictWithAbortError = goerrors.NewKind( "merge conflict detected while rebasing commit %s. " + "attempted to abort rebase operation, but encountered error: %w") +// SuccessfulRebaseMessage is used when a rebase finishes successfully. The branch that was rebased should be appended +// to the end of the message. +var SuccessfulRebaseMessage = "Successfully rebased and updated refs/heads/" + +var RebaseAbortedMessage = "Interactive rebase aborted" + func doltRebase(ctx *sql.Context, args ...string) (sql.RowIter, error) { res, message, err := doDoltRebase(ctx, args) if err != nil { @@ -116,15 +122,15 @@ func doDoltRebase(ctx *sql.Context, args []string) (int, string, error) { if err != nil { return 1, "", err } else { - return 0, "interactive rebase aborted", nil + return 0, RebaseAbortedMessage, nil } case apr.Contains(cli.ContinueFlag): - err := continueRebase(ctx) + rebaseBranch, err := continueRebase(ctx) if err != nil { return 1, "", err } else { - return 0, "interactive rebase completed", nil + return 0, SuccessfulRebaseMessage + rebaseBranch, nil } default: @@ -371,7 +377,7 @@ func abortRebase(ctx *sql.Context) error { return doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef) } -func continueRebase(ctx *sql.Context) error { +func continueRebase(ctx *sql.Context) (string, error) { // TODO: Eventually, when we allow interactive-rebases to be stopped and started (e.g. with the break action, // or for conflict resolution), we'll need to track what step we're at in the rebase plan. @@ -379,46 +385,46 @@ func continueRebase(ctx *sql.Context) error { doltSession := dsess.DSessFromSess(ctx.Session) workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) if err != nil { - return err + return "", err } if !workingSet.RebaseActive() { - return fmt.Errorf("no rebase in progress") + return "", fmt.Errorf("no rebase in progress") } db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase()) if err != nil { - return err + return "", err } rdb, ok := db.(rebase.RebasePlanDatabase) if !ok { - return fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db) + return "", fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db) } rebasePlan, err := rdb.LoadRebasePlan(ctx) if err != nil { - return err + return "", err } err = rebase.ValidateRebasePlan(ctx, rebasePlan) if err != nil { - return err + return "", err } for _, step := range rebasePlan.Steps { err = processRebasePlanStep(ctx, &step) if err != nil { - return err + return "", err } } // 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 "", err } dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) if !ok { - return fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase()) + return "", fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase()) } rebaseBranch := rebaseBranchWorkingSet.RebaseState().Branch() @@ -427,7 +433,7 @@ func continueRebase(ctx *sql.Context) 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 "", err } // TODO: copyABranch (and the underlying call to doltdb.NewBranchAtCommit) has a race condition @@ -438,25 +444,25 @@ func continueRebase(ctx *sql.Context) error { // database.CommitWithWorkingSet, since it updates a branch head and working set atomically. err = copyABranch(ctx, dbData, rebaseWorkingBranch, rebaseBranch, true, nil) if err != nil { - return err + return "", err } // Checkout the branch being rebased previousBranchWorkingSetRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseBranchWorkingSet.RebaseState().Branch())) if err != nil { - return err + return "", err } err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), previousBranchWorkingSetRef) if err != nil { - return err + return "", err } // delete the temporary working branch dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) if !ok { - return fmt.Errorf("unable to lookup dbdata") + return "", fmt.Errorf("unable to lookup dbdata") } - return actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{ + return rebaseBranch, actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{ Force: true, }, doltSession.Provider(), nil) } diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go index 388abb4ecb..988b552407 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go @@ -321,7 +321,7 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ }, { Query: "call dolt_rebase('--abort');", - Expected: []sql.Row{{0, "interactive rebase aborted"}}, + Expected: []sql.Row{{0, "Interactive rebase aborted"}}, }, { Query: "select active_branch();", @@ -412,7 +412,7 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ }, { Query: "call dolt_rebase('--continue');", - Expected: []sql.Row{{0, "interactive rebase completed"}}, + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/branch1"}}, }, { // When rebase completes, rebase status should be cleared @@ -627,7 +627,7 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ }, { Query: "call dolt_rebase('--continue');", - Expected: []sql.Row{{0, "interactive rebase completed"}}, + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/branch1"}}, }, { Query: "select message from dolt_log;", @@ -694,7 +694,7 @@ var DoltRebaseScriptTests = []queries.ScriptTest{ }, { Query: "call dolt_rebase('--continue');", - Expected: []sql.Row{{0, "interactive rebase completed"}}, + Expected: []sql.Row{{0, "Successfully rebased and updated refs/heads/branch1"}}, }, { Query: "select message from dolt_log;", diff --git a/go/libraries/utils/editor/edit.go b/go/libraries/utils/editor/edit.go index eb1487879b..4cc4f8add8 100644 --- a/go/libraries/utils/editor/edit.go +++ b/go/libraries/utils/editor/edit.go @@ -24,8 +24,8 @@ import ( "github.com/google/uuid" ) -// OpenCommitEditor allows user to write/edit commit message in temporary file -func OpenCommitEditor(ed string, initialContents string) (string, error) { +// OpenTempEditor allows user to write/edit message in temporary file +func OpenTempEditor(ed string, initialContents string) (string, error) { filename := filepath.Join(os.TempDir(), uuid.New().String()) err := os.WriteFile(filename, []byte(initialContents), os.ModePerm) diff --git a/go/libraries/utils/editor/edit_test.go b/go/libraries/utils/editor/edit_test.go index fb8d081541..ad145cc5af 100644 --- a/go/libraries/utils/editor/edit_test.go +++ b/go/libraries/utils/editor/edit_test.go @@ -59,7 +59,7 @@ func TestOpenCommitEditor(t *testing.T) { } for _, test := range tests { - val, err := OpenCommitEditor(test.editorStr, test.initialContents) + val, err := OpenTempEditor(test.editorStr, test.initialContents) if err != nil { t.Error(err) diff --git a/integration-tests/bats/helper/local-remote.bash b/integration-tests/bats/helper/local-remote.bash index 7b8c428ff8..86f138c0b1 100644 --- a/integration-tests/bats/helper/local-remote.bash +++ b/integration-tests/bats/helper/local-remote.bash @@ -136,6 +136,7 @@ SKIP_SERVER_TESTS=$(cat <<-EOM ~cli-hosted.bats~ ~profile.bats~ ~ls.bats~ +~rebase.bats~ EOM ) diff --git a/integration-tests/bats/rebase.bats b/integration-tests/bats/rebase.bats new file mode 100755 index 0000000000..67795e43ca --- /dev/null +++ b/integration-tests/bats/rebase.bats @@ -0,0 +1,382 @@ +#!/usr/bin/env bats +load $BATS_TEST_DIRNAME/helper/common.bash + +setup() { + setup_common + dolt sql -q "CREATE table t1 (pk int primary key, c int);" + dolt add t1 + dolt commit -m "main commit 1" + dolt branch b1 + dolt sql -q "INSERT INTO t1 VALUES (1,1);" + dolt add t1 + dolt commit -m "main commit 2" + + dolt checkout b1 + dolt sql -q "CREATE table t2 (pk int primary key);" + dolt add t2 + dolt commit -m "b1 commit 1" + + dolt checkout main +} + +teardown() { + assert_feature_version + teardown_common +} + +setupCustomEditorScript() { + touch rebaseScript.sh + echo "#!/bin/bash" >> rebaseScript.sh + if [ $# -eq 1 ]; then + echo "mv $1 \$1" >> rebaseScript.sh + fi + chmod +x rebaseScript.sh + export EDITOR=$PWD/rebaseScript.sh + export DOLT_TEST_FORCE_OPEN_EDITOR="1" +} + +@test "rebase: no rebase in progress errors" { + run dolt rebase --abort + [ "$status" -eq 1 ] + [[ "$output" =~ "no rebase in progress" ]] || false + + run dolt rebase --continue + [ "$status" -eq 1 ] + [[ "$output" =~ "no rebase in progress" ]] || false +} + +@test "rebase: -i flag required" { + run dolt rebase b1 + [ "$status" -eq 1 ] + [[ "$output" =~ "non-interactive rebases not currently supported" ]] || false +} + +@test "rebase: bad args" { + run dolt rebase -i + [ "$status" -eq 1 ] + [[ "$output" =~ "not enough args" ]] || false + + run dolt rebase -i main b1 + [ "$status" -eq 1 ] + [[ "$output" =~ "rebase takes at most one positional argument" ]] || false + + run dolt rebase --abrot + [ "$status" -eq 1 ] + [[ "$output" =~ "error: unknown option \`abrot'" ]] || false + + run dolt rebase -i foo + [ "$status" -eq 1 ] + [[ "$output" =~ "branch not found: foo" ]] || false +} + +@test "rebase: cannot rebase with dirty working set" { + dolt sql -q "INSERT INTO t1 VALUES (2,2);" + run dolt rebase -i b1 + [ "$status" -eq 1 ] + [[ "$output" =~ "cannot start a rebase with uncommitted changes" ]] || false +} + +@test "rebase: cannot rebase during active merge" { + dolt checkout b1 + dolt sql -q "INSERT INTO t1 VALUES (1,2);" + dolt add t1 + dolt commit -m "b1 commit 2" + + run dolt merge main + [ "$status" -eq 1 ] + [[ "$output" =~ "Automatic merge failed" ]] || false + + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "unable to start rebase while a merge is in progress – abort the current merge before proceeding" ]] || false +} + +@test "rebase: rebase working branch already exists" { + dolt checkout b1 + dolt branch dolt_rebase_b1 + + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "fatal: A branch named 'dolt_rebase_b1' already exists." ]] || false +} + +@test "rebase: verify custom script" { + setupCustomEditorScript "rebasePlan.txt" + + dolt checkout b1 + run dolt show head + [ "$status" -eq 0 ] + COMMIT1=${lines[0]:12:32} + + touch rebasePlan.txt + echo "pick $COMMIT1 b1 commit 1" >> rebasePlan.txt + + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "b1 commit 1" ]] || false + [[ "$output" =~ "main commit 2" ]] || false +} + +@test "rebase: basic rebase" { + setupCustomEditorScript + + dolt checkout b1 + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "main commit 2" ]] || false +} + +@test "rebase: failed rebase will abort and clean up" { + setupCustomEditorScript "invalidRebasePlan.txt" + dolt checkout b1 + + touch invalidRebasePlan.txt + echo "foo" >> invalidRebasePlan.txt + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "invalid line 0: foo" ]] || false + + run dolt branch + [ "$status" -eq 0 ] + ! [[ "$output" =~ "dolt_rebase_b1" ]] || false + + run dolt rebase --continue + [ "$status" -eq 1 ] + [[ "$output" =~ "no rebase in progress" ]] || false +} + +@test "rebase: invalid rebase plan" { + setupCustomEditorScript "invalidRebasePlan.txt" + + dolt checkout b1 + + touch invalidRebasePlan.txt + echo "foo" >> invalidRebasePlan.txt + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "invalid line 0: foo" ]] || false + + touch invalidRebasePlan.txt + echo "pick foo main commit 1" >> invalidRebasePlan.txt + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "invalid commit hash: foo" ]] || false +} + +@test "rebase: empty rebase plan aborts the rebase" { + setupCustomEditorScript "emptyRebasePlan.txt" + touch emptyRebasePlan.txt + echo "# " >> emptyRebasePlan.txt + echo "# commented out lines don't count" >> emptyRebasePlan.txt + + dolt checkout b1 + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "rebase aborted" ]] || false + + run dolt log + [ "$status" -eq 0 ] + ! [[ "$output" =~ "main commit 2" ]] || false + + run dolt rebase --continue + [ "$status" -eq 1 ] + [[ "$output" =~ "no rebase in progress" ]] || false + + run dolt branch + [ "$status" -eq 0 ] + ! [[ "$output" =~ "dolt_rebase_b1" ]] || false +} + +@test "rebase: multi step rebase" { + setupCustomEditorScript "multiStepPlan.txt" + + dolt checkout b1 + run dolt show head + [ "$status" -eq 0 ] + COMMIT1=${lines[0]:12:32} + + 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} + + 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} + + 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} + + 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} + + 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} + + 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} + + touch multiStepPlan.txt + echo "pick $COMMIT1 b1 commit 1" >> multiStepPlan.txt + echo "squash $COMMIT2 b1 commit 2" >> multiStepPlan.txt + echo "squash $COMMIT3 b1 commit 3" >> multiStepPlan.txt + echo "drop $COMMIT4 b1 commit 4" >> multiStepPlan.txt + echo "reword $COMMIT5 reworded!" >> multiStepPlan.txt + echo "fixup $COMMIT6 b1 commit 6" >> multiStepPlan.txt + echo "pick $COMMIT7 b1 commit 7" >> multiStepPlan.txt + + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt show head + [ "$status" -eq 0 ] + [[ "$output" =~ "b1 commit 7" ]] || false + + run dolt show head~1 + [ "$status" -eq 0 ] + [[ "$output" =~ "reworded!" ]] || false + + run dolt show head~2 + [ "$status" -eq 0 ] + [[ "$output" =~ "b1 commit 1" ]] || false + [[ "$output" =~ "b1 commit 2" ]] || false + [[ "$output" =~ "b1 commit 3" ]] || false + + run dolt show head~3 + [ "$status" -eq 0 ] + [[ "$output" =~ "main commit 2" ]] || false +} + +@test "rebase: non-standard plan changes" { + setupCustomEditorScript "nonStandardPlan.txt" + + dolt checkout -b b2 + 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} + + dolt sql -q "insert into t3 values (1);" + dolt commit -am "b2 commit 2" + dolt sql -q "insert into t3 values (2);" + dolt commit -am "b2 commit 3" + + dolt checkout b1 + run dolt show head + [ "$status" -eq 0 ] + COMMIT2=${lines[0]:12:32} + + touch nonStandardPlan.txt + echo "pick $COMMIT1 b2 commit 1" >> nonStandardPlan.txt + echo "pick $COMMIT2 b1 commit 1" >> nonStandardPlan.txt + + dolt checkout b2 + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b2" ]] || false + + run dolt show head + [ "$status" -eq 0 ] + [[ "$output" =~ "b1 commit 1" ]] || false + + run dolt show head~1 + [ "$status" -eq 0 ] + [[ "$output" =~ "b2 commit 1" ]] || false + + run dolt show head~2 + [ "$status" -eq 0 ] + [[ "$output" =~ "main commit 2" ]] || false +} + +@test "rebase: rebase skips merge commits" { + setupCustomEditorScript + + dolt checkout b1 + dolt merge main -m "b1 merge commit" + dolt sql -q "insert into t2 values (1);" + dolt commit -am "b1 commit 2" + + dolt checkout main + dolt sql -q "insert into t1 values (2,2);" + dolt commit -am "main commit 3" + dolt checkout b1 + + run dolt rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "b1 commit 2" ]] || false + [[ "$output" =~ "b1 commit 1" ]] || false + [[ "$output" =~ "main commit 3" ]] || false + ! [[ "$output" =~ "b1 merge commit" ]] || false +} + +@test "rebase: rebase with data conflicts aborts" { + setupCustomEditorScript + + dolt checkout b1 + dolt sql -q "INSERT INTO t1 VALUES (1,2);" + dolt commit -am "b1 commit 2" + + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "merge conflict detected while rebasing commit" ]] || false + [[ "$output" =~ "the rebase has been automatically aborted" ]] || false + + run dolt rebase --continue + [ "$status" -eq 1 ] + [[ "$output" =~ "no rebase in progress" ]] || false + + run dolt branch + [ "$status" -eq 0 ] + ! [[ "$output" =~ "dolt_rebase_b1" ]] || false +} + +@test "rebase: rebase with schema conflicts aborts" { + setupCustomEditorScript + + dolt checkout b1 + dolt sql -q "ALTER TABLE t1 MODIFY COLUMN c varchar(100);" + dolt commit -am "b1 commit 2" + + run dolt rebase -i main + [ "$status" -eq 1 ] + [[ "$output" =~ "merge conflict detected while rebasing commit" ]] || false + [[ "$output" =~ "the rebase has been automatically aborted" ]] || false + + run dolt rebase --continue + [ "$status" -eq 1 ] + [[ "$output" =~ "no rebase in progress" ]] || false + + run dolt branch + [ "$status" -eq 0 ] + ! [[ "$output" =~ "dolt_rebase_b1" ]] || false +} diff --git a/integration-tests/bats/sql-local-remote.bats b/integration-tests/bats/sql-local-remote.bats index 9c5dfd545a..b5c20e9774 100644 --- a/integration-tests/bats/sql-local-remote.bats +++ b/integration-tests/bats/sql-local-remote.bats @@ -1338,3 +1338,51 @@ SQL [ "$status" -eq "0" ] [[ "$output" =~ "5" ]] || false } + +@test "sql-local-remote: verify dolt rebase behavior" { + cd altDB + + dolt sql -q "drop table dolt_ignore;" + dolt add . + + dolt branch b1 + dolt commit -m "main commit 2" + dolt checkout b1 + dolt sql -q "create table t2 (pk int primary key)" + dolt add . + dolt commit -m "b1 commit 1" + + touch rebaseScript.sh + echo "#!/bin/bash" >> rebaseScript.sh + chmod +x rebaseScript.sh + export EDITOR=$PWD/rebaseScript.sh + export DOLT_TEST_FORCE_OPEN_EDITOR="1" + + run dolt --verbose-engine-setup rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "starting local mode" ]] || false + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "main commit 2" ]] || false + [[ "$output" =~ "b1 commit 1" ]] || false + + dolt checkout main + dolt sql -q "create table t3 (pk int primary key)" + dolt add . + dolt commit -m "main commit 3" + dolt checkout b1 + + start_sql_server altDB + run dolt --verbose-engine-setup rebase -i main + [ "$status" -eq 0 ] + [[ "$output" =~ "starting remote mode" ]] || false + [[ "$output" =~ "Successfully rebased and updated refs/heads/b1" ]] || false + + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "main commit 3" ]] || false + [[ "$output" =~ "main commit 2" ]] || false + [[ "$output" =~ "b1 commit 1" ]] || false +} diff --git a/proto/dolt/services/eventsapi/v1alpha1/event_constants.proto b/proto/dolt/services/eventsapi/v1alpha1/event_constants.proto index 95dc1e7d2b..bf70f59f2c 100644 --- a/proto/dolt/services/eventsapi/v1alpha1/event_constants.proto +++ b/proto/dolt/services/eventsapi/v1alpha1/event_constants.proto @@ -94,6 +94,7 @@ enum ClientEventType { PROFILE = 62; REFLOG = 63; SQL_SERVER_HEARTBEAT = 64; + REBASE = 65; } enum MetricID {