Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go/cmd/dolt/cli/arg_parser_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ func CreateCheckoutArgParser() *argparser.ArgParser {
func CreateCherryPickArgParser() *argparser.ArgParser {
ap := argparser.NewArgParserWithMaxArgs("cherrypick", 1)
ap.SupportsFlag(AbortParam, "", "Abort the current conflict resolution process, and revert all changes from the in-process cherry-pick operation.")
ap.SupportsFlag(ContinueFlag, "", "Continue the current cherry-pick operation after conflicts have been resolved.")
ap.SupportsFlag(AllowEmptyFlag, "", "Allow empty commits to be cherry-picked. "+
"Note that use of this option only keeps commits that were initially empty. "+
"Commits which become empty, due to a previous commit, will cause cherry-pick to fail.")
Expand Down
75 changes: 69 additions & 6 deletions go/cmd/dolt/commands/cherry-pick.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ If any data conflicts, schema conflicts, or constraint violations are detected d

var ErrCherryPickConflictsOrViolations = errors.NewKind("error: Unable to apply commit cleanly due to conflicts " +
"or constraint violations. Please resolve the conflicts and/or constraint violations, then use `dolt add` " +
"to add the tables to the staged set, and `dolt commit` to commit the changes and finish cherry-picking. \n" +
"to add the tables to the staged set, and `dolt cherry-pick --continue` to complete the cherry-pick. \n" +
"To undo all changes from this cherry-pick operation, use `dolt cherry-pick --abort`.\n" +
"For more information on handling conflicts, see: https://docs.dolthub.com/concepts/dolt/git/conflicts")

Expand Down Expand Up @@ -96,8 +96,19 @@ func (cmd CherryPickCmd) Exec(ctx context.Context, commandStr string, args []str
return 1
}

// Check for mutually exclusive flags
if apr.Contains(cli.AbortParam) && apr.Contains(cli.ContinueFlag) {
err = fmt.Errorf("error: --continue and --abort are mutually exclusive")
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

if apr.Contains(cli.AbortParam) {
err = cherryPickAbort(queryist.Queryist, queryist.Context)
err = cherryPickAbort(queryist.Context, queryist.Queryist)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

if apr.Contains(cli.ContinueFlag) {
err = cherryPickContinue(queryist.Context, queryist.Queryist)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

Expand All @@ -117,11 +128,11 @@ func (cmd CherryPickCmd) Exec(ctx context.Context, commandStr string, args []str
return HandleVErrAndExitCode(errhand.BuildDError("cherry-picking multiple commits is not supported yet").SetPrintUsage().Build(), usage)
}

err = cherryPick(queryist.Queryist, queryist.Context, apr, args)
err = cherryPick(queryist.Context, queryist.Queryist, apr, args)
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}

func cherryPick(queryist cli.Queryist, sqlCtx *sql.Context, apr *argparser.ArgParseResults, args []string) error {
func cherryPick(sqlCtx *sql.Context, queryist cli.Queryist, apr *argparser.ArgParseResults, args []string) error {
cherryStr := apr.Arg(0)
if len(cherryStr) == 0 {
return fmt.Errorf("error: cannot cherry-pick empty string")
Expand Down Expand Up @@ -201,7 +212,7 @@ hint: commit your changes (dolt commit -am \"<message>\") or reset them (dolt re

if succeeded {
// on success, print the commit info
commit, err := getCommitInfo(queryist, sqlCtx, commitHash)
commit, err := getCommitInfo(sqlCtx, queryist, commitHash)
if commit == nil || err != nil {
return fmt.Errorf("error: failed to get commit metadata for ref '%s': %v", commitHash, err)
}
Expand All @@ -220,7 +231,7 @@ hint: commit your changes (dolt commit -am \"<message>\") or reset them (dolt re
}
}

func cherryPickAbort(queryist cli.Queryist, sqlCtx *sql.Context) error {
func cherryPickAbort(sqlCtx *sql.Context, queryist cli.Queryist) error {
query := "call dolt_cherry_pick('--abort')"
_, err := cli.GetRowsForSql(queryist, sqlCtx, query)
if err != nil {
Expand All @@ -235,6 +246,58 @@ func cherryPickAbort(queryist cli.Queryist, sqlCtx *sql.Context) error {
return nil
}

func cherryPickContinue(sqlCtx *sql.Context, queryist cli.Queryist) error {
query := "call dolt_cherry_pick('--continue')"
rows, err := cli.GetRowsForSql(queryist, sqlCtx, query)
if err != nil {
return err
}

if len(rows) != 1 {
return fmt.Errorf("error: unexpected number of rows returned from dolt_cherry_pick: %d", len(rows))
}
if len(rows[0]) != 4 {
return fmt.Errorf("error: unexpected number of columns returned from dolt_cherry_pick: %d", len(rows[0]))
}

row := rows[0]

// We expect to get an error if there were problems, but we also could get any of the conflicts and
// vacation counts being greater than 0 if there were problems. If we got here without an error,
// but we have conflicts or violations, we should report and stop.
dataConflicts, err := getInt64ColAsInt64(row[1])
if err != nil {
return fmt.Errorf("Unable to parse data_conflicts column: %w", err)
}
schemaConflicts, err := getInt64ColAsInt64(row[2])
if err != nil {
return fmt.Errorf("Unable to parse schema_conflicts column: %w", err)
}
constraintViolations, err := getInt64ColAsInt64(row[3])
if err != nil {
return fmt.Errorf("Unable to parse constraint_violations column: %w", err)
}
if dataConflicts > 0 || schemaConflicts > 0 || constraintViolations > 0 {
return ErrCherryPickConflictsOrViolations.New()
}

commitHash := fmt.Sprintf("%v", row[0])

commit, err := getCommitInfo(sqlCtx, queryist, commitHash)
if commit == nil || err != nil {
return fmt.Errorf("error: failed to get commit metadata for ref '%s': %v", commitHash, err)
}

cli.ExecuteWithStdioRestored(func() {
pager := outputpager.Start()
defer pager.Stop()

PrintCommitInfo(pager, 0, false, false, "auto", commit)
})

return nil
}

func hasStagedAndUnstagedChanged(queryist cli.Queryist, sqlCtx *sql.Context) (hasStagedChanges bool, hasUnstagedChanges bool, err error) {
stagedTables, unstagedTables, err := GetDoltStatus(queryist, sqlCtx)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func performCommit(ctx context.Context, commandStr string, args []string, cliCtx
return 0, true
}

commit, err := getCommitInfo(queryist.Queryist, queryist.Context, "HEAD")
commit, err := getCommitInfo(queryist.Context, queryist.Queryist, "HEAD")
if cli.ExecuteWithStdioRestored != nil {
cli.ExecuteWithStdioRestored(func() {
pager := outputpager.Start()
Expand Down
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func logCommits(apr *argparser.ArgParseResults, commitHashes []sql.Row, queryist
var commitsInfo []CommitInfo
for _, hash := range commitHashes {
cmHash := hash[0].(string)
commit, err := getCommitInfoWithOptions(queryist, sqlCtx, cmHash, opts)
commit, err := getCommitInfoWithOptions(sqlCtx, queryist, cmHash, opts)
if commit == nil {
return fmt.Errorf("no commits found for ref %s", cmHash)
}
Expand Down
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func printMergeStats(fastForward bool,
}

if !apr.Contains(cli.NoCommitFlag) && !apr.Contains(cli.NoFFParam) && !fastForward && noConflicts {
commit, err := getCommitInfo(queryist, sqlCtx, "HEAD")
commit, err := getCommitInfo(sqlCtx, queryist, "HEAD")
if err != nil {
cli.Println("merge finished, but failed to get commit info")
cli.Println(err.Error())
Expand Down
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func (cmd PullCmd) Exec(ctx context.Context, commandStr string, args []string, d
if remoteHash != "" && headHash != "" {
cli.Println("Updating", headHash+".."+remoteHash)
}
commit, err := getCommitInfo(queryist.Queryist, queryist.Context, "HEAD")
commit, err := getCommitInfo(queryist.Context, queryist.Queryist, "HEAD")
if err != nil {
cli.Println("pull finished, but failed to get commit info")
cli.Println(err.Error())
Expand Down
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/revert.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (cmd RevertCmd) Exec(ctx context.Context, commandStr string, args []string,
return 1
}

commit, err := getCommitInfo(queryist.Queryist, queryist.Context, "HEAD")
commit, err := getCommitInfo(queryist.Context, queryist.Queryist, "HEAD")
if err != nil {
cli.Printf("Revert completed, but failure to get commit details occurred: %s\n", err.Error())
return 1
Expand Down
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ func getCommitSpecPretty(queryist cli.Queryist, sqlCtx *sql.Context, commitRef s
commitRef = strings.TrimPrefix(commitRef, "#")
}

commit, err = getCommitInfo(queryist, sqlCtx, commitRef)
commit, err = getCommitInfo(sqlCtx, queryist, commitRef)
if err != nil {
return commit, fmt.Errorf("error: failed to get commit metadata for ref '%s': %v", commitRef, err)
}
Expand Down
6 changes: 3 additions & 3 deletions go/cmd/dolt/commands/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,11 +701,11 @@ type commitInfoOptions struct {
}

// getCommitInfo returns the commit info for the given ref.
func getCommitInfo(queryist cli.Queryist, sqlCtx *sql.Context, ref string) (*CommitInfo, error) {
return getCommitInfoWithOptions(queryist, sqlCtx, ref, commitInfoOptions{})
func getCommitInfo(sqlCtx *sql.Context, queryist cli.Queryist, ref string) (*CommitInfo, error) {
return getCommitInfoWithOptions(sqlCtx, queryist, ref, commitInfoOptions{})
}

func getCommitInfoWithOptions(queryist cli.Queryist, sqlCtx *sql.Context, ref string, opts commitInfoOptions) (*CommitInfo, error) {
func getCommitInfoWithOptions(sqlCtx *sql.Context, queryist cli.Queryist, ref string, opts commitInfoOptions) (*CommitInfo, error) {
hashOfHead, err := getHashOf(queryist, sqlCtx, "HEAD")
if err != nil {
return nil, fmt.Errorf("error getting hash of HEAD: %v", err)
Expand Down
137 changes: 127 additions & 10 deletions go/libraries/doltcore/cherry_pick/cherry_pick.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,110 @@ func AbortCherryPick(ctx *sql.Context, dbName string) error {
return doltSession.SetWorkingSet(ctx, dbName, newWs)
}

// ContinueCherryPick continues a cherry-pick merge that was paused due to conflicts.
// It checks that conflicts have been resolved and creates the final commit with the
// original commit's metadata.
func ContinueCherryPick(ctx *sql.Context, dbName string) (string, int, int, int, error) {
doltSession := dsess.DSessFromSess(ctx.Session)

ws, err := doltSession.WorkingSet(ctx, dbName)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("fatal: unable to load working set: %w", err)
}

if !ws.MergeActive() {
return "", 0, 0, 0, fmt.Errorf("error: There is no cherry-pick merge to continue")
}
mergeState := ws.MergeState()

// Count conflicts and violations similar to the first pass
workingRoot := ws.WorkingRoot()
stagedRoot := ws.StagedRoot()

// Count data conflicts
conflictTables, err := doltdb.TablesWithDataConflicts(ctx, workingRoot)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: unable to check for conflicts: %w", err)
}
dataConflictCount := len(conflictTables)

// Count schema conflicts from merge state
schemaConflictCount := len(mergeState.TablesWithSchemaConflicts())

// Count constraint violations
violationTables, err := doltdb.TablesWithConstraintViolations(ctx, workingRoot)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: unable to check for constraint violations: %w", err)
}
constraintViolationCount := len(violationTables)

// If there are any conflicts or violations, return the counts with an error
if dataConflictCount > 0 || schemaConflictCount > 0 || constraintViolationCount > 0 {
return "", dataConflictCount, schemaConflictCount, constraintViolationCount, nil
}

// This is a little strict. Technically, you could use the dolt_workspace table to stage something
// and result in different roots.
// TODO: test with ignored and local tables - because this strictness will probably cause issues.
isClean, err := rootsEqual(stagedRoot, workingRoot)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: unable to compare staged and working roots: %w", err)
}
if !isClean {
return "", 0, 0, 0, fmt.Errorf("error: cannot continue cherry-pick with unstaged changes")
}

cherryCommit := mergeState.Commit()
if cherryCommit == nil {
return "", 0, 0, 0, fmt.Errorf("error: unable to get original commit from merge state")
}

cherryCommitMeta, err := cherryCommit.GetCommitMeta(ctx)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: unable to get commit metadata: %w", err)
}

// Create the commit with the original commit's metadata
commitProps := actions.CommitStagedProps{
Message: cherryCommitMeta.Description,
Date: cherryCommitMeta.Time(),
AllowEmpty: false, // in a conflict workflow, never will be 'true'
Name: cherryCommitMeta.Name,
Email: cherryCommitMeta.Email,
}

roots, ok := doltSession.GetRoots(ctx, dbName)
if !ok {
return "", 0, 0, 0, fmt.Errorf("fatal: unable to load roots for %s", dbName)
}

pendingCommit, err := doltSession.NewPendingCommit(ctx, dbName, roots, commitProps)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: failed to create pending commit: %w", err)
}
if pendingCommit == nil {
return "", 0, 0, 0, fmt.Errorf("error: no changes to commit")
}

clearedWs := ws.ClearMerge()
err = doltSession.SetWorkingSet(ctx, dbName, clearedWs)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: failed to clear merge state: %w", err)
}

commit, err := doltSession.DoltCommit(ctx, dbName, doltSession.GetTransaction(), pendingCommit)
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: failed to execute commit: %w", err)
}

commitHash, err := commit.HashOf()
if err != nil {
return "", 0, 0, 0, fmt.Errorf("error: failed to get commit hash: %w", err)
}

return commitHash.String(), 0, 0, 0, nil
}

// cherryPick checks that the current working set is clean, verifies the cherry-pick commit is not a merge commit
// or a commit without parent commit, performs merge and returns the new working set root value and
// the commit message of cherry-picked commit as the commit message of the new commit created during this command.
Expand Down Expand Up @@ -371,22 +475,35 @@ func cherryPick(ctx *sql.Context, dSess *dsess.DoltSession, roots doltdb.Roots,

// If any of the merge stats show a data or schema conflict or a constraint
// violation, record that a merge is in progress.
hasArtifacts := false
for _, stats := range result.Stats {
if stats.HasArtifacts() {
ws, err := dSess.WorkingSet(ctx, dbName)
if err != nil {
return nil, "", nil, err
}
newWorkingSet := ws.StartCherryPick(cherryCommit, cherryStr)
err = dSess.SetWorkingSet(ctx, dbName, newWorkingSet)
if err != nil {
return nil, "", nil, err
}

hasArtifacts = true
break
}
}

// Also check if there are any constraint violations in the result root
// This handles the case where violations weren't tracked in stats
if !hasArtifacts && result.Root != nil {
violationTables, err := doltdb.TablesWithConstraintViolations(ctx, result.Root)
if err == nil && len(violationTables) > 0 {
hasArtifacts = true
}
}

if hasArtifacts {
ws, err := dSess.WorkingSet(ctx, dbName)
if err != nil {
return nil, "", nil, err
}
newWorkingSet := ws.StartCherryPick(cherryCommit, cherryStr)
err = dSess.SetWorkingSet(ctx, dbName, newWorkingSet)
if err != nil {
return nil, "", nil, err
}
}

return result, cherryCommitMeta.Description, cherryCommit, nil
}

Expand Down
8 changes: 8 additions & 0 deletions go/libraries/doltcore/sqle/dprocedures/dolt_cherry_pick.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,18 @@ func doDoltCherryPick(ctx *sql.Context, args []string) (string, int, int, int, e
return "", 0, 0, 0, err
}

if apr.Contains(cli.AbortParam) && apr.Contains(cli.ContinueFlag) {
return "", 0, 0, 0, fmt.Errorf("error: --continue and --abort are mutually exclusive")
}

if apr.Contains(cli.AbortParam) {
return "", 0, 0, 0, cherry_pick.AbortCherryPick(ctx, dbName)
}

if apr.Contains(cli.ContinueFlag) {
return cherry_pick.ContinueCherryPick(ctx, dbName)
}

// we only support cherry-picking a single commit for now.
if apr.NArg() == 0 {
return "", 0, 0, 0, ErrEmptyCherryPick
Expand Down
Loading
Loading