diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go b/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go index d5e1782f8d4..ee9c447b108 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go @@ -20,6 +20,7 @@ import ( "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" + "github.com/fatih/color" "github.com/dolthub/dolt/go/cmd/dolt/cli" "github.com/dolthub/dolt/go/cmd/dolt/errhand" @@ -116,110 +117,123 @@ func doDoltCheckout(ctx *sql.Context, args []string) (statusCode int, successMes } } - branchName := apr.Arg(0) - if len(branchName) == 0 { + roots, ok := dSess.GetRoots(ctx, currentDbName) + if !ok { + return 1, "", fmt.Errorf("Could not load database %s", currentDbName) + } + + // firstArg purpose depends on the context, it may be a branch, table, etc. + firstArg := apr.Arg(0) + if len(firstArg) == 0 { return 1, "", ErrEmptyBranchName } - isModification, err := willModifyDb(ctx, dSess, dbData, currentDbName, branchName, updateHead) + // check for detached HEAD state early - if the user is trying to checkout a tag or commit hash + if apr.NArg() == 1 { + err = validateNoDetachedHead(ctx, dbData.Ddb, firstArg, updateHead) + if err != nil { + return 1, "", err + } + } + + isModification, err := willModifyDb(ctx, dSess, dbData, currentDbName, firstArg, updateHead) if err != nil { return 1, "", err } if !isModification && apr.NArg() == 1 { - return 0, fmt.Sprintf("Already on branch '%s'", branchName), nil + return 0, fmt.Sprintf("Already on branch '%s'", firstArg), nil } - // Handle dolt_checkout HEAD~3 -- table1 table2 table3 - if apr.NArg() > 1 { - database := ctx.GetCurrentDatabase() - if database == "" { - return 1, "", sql.ErrNoDatabaseSelected.New() - } - err = checkoutTablesFromCommit(ctx, database, branchName, apr.Args[1:]) + // No ref explicitly specified but table(s) are + dashDashPos := apr.PositionalArgsSeparatorIndex + if dashDashPos == 0 { + err = checkoutTablesFromHead(ctx, roots, currentDbName, apr.Args) if err != nil { - return 0, "", err + return 1, "", err } - - dsess.WaitForReplicationController(ctx, rsc) - return 0, "", nil + return 0, successMessage, nil } - // Check if user wants to checkout branch. - if isBranch, err := actions.IsBranch(ctx, dbData.Ddb, branchName); err != nil { + localRefExists, err := actions.IsBranch(ctx, dbData.Ddb, firstArg) + if err != nil { return 1, "", err - } else if isBranch { - err = checkoutExistingBranch(ctx, currentDbName, branchName, apr) - if errors.Is(err, doltdb.ErrWorkingSetNotFound) { - // If there is a branch but there is no working set, - // somehow the local branch ref was created without a - // working set. This happened with old versions of dolt - // when running as a read replica, for example. Try to - // create a working set pointing at the existing branch - // HEAD and check out the branch again. - // - // TODO: This is all quite racey, but so is the - // handling in doltDB, etc. - err = createWorkingSetForLocalBranch(ctx, dbData.Ddb, branchName) - if err != nil { - return 1, "", err + } + + remoteRefs, err := actions.GetRemoteBranchRef(ctx, dbData.Ddb, firstArg) + if err != nil { + return 1, "", fmt.Errorf("unable to read remote refs from data repository: %v", err) + } + + validRemoteRefExists := remoteRefs != nil && len(remoteRefs) >= 1 + + if dashDashPos == 1 && (localRefExists || validRemoteRefExists) { + // dolt checkout --: disambiguates a tracking branch when it shares a name with local table(s). + if apr.NArg() == 1 { // assume some specified because dashDashPos is 1 + if localRefExists { + err = checkoutExistingBranchWithWorkingSetFallback(ctx, currentDbName, firstArg, apr, dSess, &rsc) + if err != nil { + return 1, "", err + } + return 0, generateSuccessMessage(firstArg, ""), nil } - // Since we've created new refs since the transaction began, we need to commit this transaction and - // start a new one to avoid not found errors after this - // TODO: this is much worse than other places we do this, because it's two layers of implicit behavior - sess := dsess.DSessFromSess(ctx.Session) - err = commitTransaction(ctx, sess, &rsc) + upstream, err := checkoutRemoteBranch(ctx, dSess, currentDbName, dbData, firstArg, apr, &rsc) if err != nil { return 1, "", err } - - err = checkoutExistingBranch(ctx, currentDbName, branchName, apr) + return 0, generateSuccessMessage(firstArg, upstream), nil } + + // git requires a local ref to already exist to check out tables + err = checkoutTablesFromCommit(ctx, currentDbName, firstArg, apr.Args[1:], &rsc) if err != nil { return 1, "", err } - return 0, generateSuccessMessage(branchName, ""), nil + return 0, "", nil } - roots, ok := dSess.GetRoots(ctx, currentDbName) - if !ok { - return 1, "", fmt.Errorf("Could not load database %s", currentDbName) + _, _, firstArgIsTable, err := actions.FindTableInRoots(ctx, roots, firstArg) + if err != nil { + return 1, "", err } - // Check if the user executed `dolt checkout .` - if apr.NArg() == 1 && apr.Arg(0) == "." { - headRef, err := dbData.Rsr.CWBHeadRef(ctx) - if err != nil { - return 1, "", err - } + // ambiguity `foo` is a table AND matches a tracking branch, but a local branch does not exist already + if validRemoteRefExists && !localRefExists && firstArgIsTable { + return 1, "", fmt.Errorf("'%s' could be both a local table and a tracking branch.\n"+ + "Please use -- to disambiguate.", firstArg) + } - ws, err := dSess.WorkingSet(ctx, currentDbName) + if apr.NArg() > 1 && !firstArgIsTable { + err = checkoutTablesFromCommit(ctx, currentDbName, firstArg, apr.Args[1:], &rsc) if err != nil { return 1, "", err } - doltDb, hasDb := dSess.GetDoltDB(ctx, currentDbName) - if !hasDb { - return 1, "", errors.New("Unable to load database") - } - err = actions.ResetHard(ctx, dbData, doltDb, dSess.Username(), dSess.Email(), "", roots, headRef, ws) + return 0, "", nil + } + + // git prioritizes local and remote refs over tables + if localRefExists { + err = checkoutExistingBranchWithWorkingSetFallback(ctx, currentDbName, firstArg, apr, dSess, &rsc) if err != nil { return 1, "", err } - return 0, "", err + return 0, generateSuccessMessage(firstArg, ""), nil } - err = checkoutTablesFromHead(ctx, roots, currentDbName, apr.Args) - if err != nil && apr.NArg() == 1 { - upstream, err := checkoutRemoteBranch(ctx, dSess, currentDbName, dbData, branchName, apr, &rsc) + if validRemoteRefExists { + upstream, err := checkoutRemoteBranch(ctx, dSess, currentDbName, dbData, firstArg, apr, &rsc) if err != nil { return 1, "", err } - successMessage = generateSuccessMessage(branchName, upstream) + return 0, generateSuccessMessage(firstArg, upstream), nil } - dsess.WaitForReplicationController(ctx, rsc) + err = checkoutTablesFromHead(ctx, roots, currentDbName, apr.Args) + if err != nil { + return 1, "", err + } - return 0, successMessage, nil + return 0, "", nil } // parseBranchArgs returns the name of the new branch and whether or not it should be created forcibly. This asserts @@ -243,6 +257,11 @@ func parseBranchArgs(apr *argparser.ArgParseResults) (newBranch string, createBr return newBranch, true, nil } + dashDashPos := apr.PositionalArgsSeparatorIndex + if dashDashPos >= 2 { + return "", false, fmt.Errorf("only one reference expected, %d given", dashDashPos) + } + return "", false, nil } @@ -301,38 +320,21 @@ func createWorkingSetForLocalBranch(ctx *sql.Context, ddb *doltdb.DoltDB, branch // checkoutRemoteBranch checks out a remote branch creating a new local branch with the same name as the remote branch // and set its upstream. The upstream persists out of sql session. Returns the name of the upstream remote and branch. -func checkoutRemoteBranch(ctx *sql.Context, dSess *dsess.DoltSession, dbName string, dbData env.DbData[*sql.Context], branchName string, apr *argparser.ArgParseResults, rsc *doltdb.ReplicationStatusController) (upstream string, err error) { +func checkoutRemoteBranch( + ctx *sql.Context, + dSess *dsess.DoltSession, + dbName string, + dbData env.DbData[*sql.Context], + branchName string, + apr *argparser.ArgParseResults, + rsc *doltdb.ReplicationStatusController, +) (upstream string, err error) { remoteRefs, err := actions.GetRemoteBranchRef(ctx, dbData.Ddb, branchName) if err != nil { return "", errors.New("fatal: unable to read from data repository") } if len(remoteRefs) == 0 { - if isTag, err := actions.IsTag(ctx, dbData.Ddb, branchName); err != nil { - return "", err - } else if isTag { - // User tried to enter a detached head state, which we don't support. - // Inform and suggest that they check-out a new branch at this tag instead. - if apr.Contains(cli.MoveFlag) { - return "", fmt.Errorf(`dolt does not support a detached head state. To create a branch at this tag, run: - dolt checkout %s -b {new_branch_name}`, branchName) - } else { - return "", fmt.Errorf(`dolt does not support a detached head state. To create a branch at this tag, run: - CALL DOLT_CHECKOUT('%s', '-b', )`, branchName) - } - } - - if doltdb.IsValidCommitHash(branchName) { - // User tried to enter a detached head state, which we don't support. - // Inform and suggest that they check-out a new branch at this commit instead. - if apr.Contains(cli.MoveFlag) { - return "", fmt.Errorf(`dolt does not support a detached head state. To create a branch at this commit instead, run: - dolt checkout %s -b {new_branch_name}`, branchName) - } else { - return "", fmt.Errorf(`dolt does not support a detached head state. To create a branch at this commit instead, run: - CALL DOLT_CHECKOUT('%s', '-b', )`, branchName) - } - } return "", fmt.Errorf("error: could not find %s", branchName) } else if len(remoteRefs) == 1 { remoteRef := remoteRefs[0] @@ -375,16 +377,30 @@ func checkoutRemoteBranch(ctx *sql.Context, dSess *dsess.DoltSession, dbName str return "", err } + dsess.WaitForReplicationController(ctx, *rsc) + return remoteRef.GetPath(), nil } else { - return "", fmt.Errorf("'%s' matched multiple (%v) remote tracking branches", branchName, len(remoteRefs)) + hint := `hint: If you meant to check out a remote tracking branch, on, e.g., 'origin', +hint: you can do so by fully qualifying the name with the --track option: +hint: +hint: dolt checkout --track origin/ +` + return "", fmt.Errorf(color.YellowString(hint)+"'%s' matched multiple (%v) remote tracking branches", branchName, len(remoteRefs)) } } // checkoutNewBranch creates a new branch and makes it the active branch for the session. // If isMove is true, this function also moves the working set from the current branch into the new branch. // Returns the name of the new branch and the remote upstream branch (empty string if not applicable.) -func checkoutNewBranch(ctx *sql.Context, dbName string, dbData env.DbData[*sql.Context], apr *argparser.ArgParseResults, rsc *doltdb.ReplicationStatusController, isMove bool) (newBranchName string, remoteAndBranch string, err error) { +func checkoutNewBranch( + ctx *sql.Context, + dbName string, + dbData env.DbData[*sql.Context], + apr *argparser.ArgParseResults, + rsc *doltdb.ReplicationStatusController, + isMove bool, +) (newBranchName string, remoteAndBranch string, err error) { var remoteName, remoteBranchName string var startPt = "head" var refSpec ref.RefSpec @@ -473,7 +489,12 @@ func checkoutNewBranch(ctx *sql.Context, dbName string, dbData env.DbData[*sql.C } // checkoutExistingBranch updates the active branch reference to point to an already existing branch. -func checkoutExistingBranch(ctx *sql.Context, dbName string, branchName string, apr *argparser.ArgParseResults) error { +func checkoutExistingBranch( + ctx *sql.Context, + dbName string, + branchName string, + apr *argparser.ArgParseResults, +) error { wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(branchName)) if err != nil { return err @@ -497,6 +518,58 @@ func checkoutExistingBranch(ctx *sql.Context, dbName string, branchName string, return nil } +// checkoutExistingBranchWithWorkingSetFallback checks out an existing branch, and if the working set does not exist, +// it creates a new working set for the branch pointing to the existing branch HEAD. This resolves the issue where +// a local branch ref was created without a working set, such as when running as a read replica with an old version of dolt. +func checkoutExistingBranchWithWorkingSetFallback( + ctx *sql.Context, + dbName string, + branchName string, + apr *argparser.ArgParseResults, + dSess *dsess.DoltSession, + rsc *doltdb.ReplicationStatusController, +) error { + dbData, ok := dSess.GetDbData(ctx, dbName) + if !ok { + return fmt.Errorf("could not load database %s", dbName) + } + + err := checkoutExistingBranch(ctx, dbName, branchName, apr) + if errors.Is(err, doltdb.ErrWorkingSetNotFound) { + // If there is a branch but there is no working set, + // somehow the local branch ref was created without a + // working set. This happened with old versions of dolt + // when running as a read replica, for example. Try to + // create a working set pointing at the existing branch + // HEAD and check out the branch again. + // + // TODO: This is all quite racey, but so is the + // handling in doltDB, etc. + err = createWorkingSetForLocalBranch(ctx, dbData.Ddb, branchName) + if err != nil { + return err + } + + // Since we've created new refs since the transaction began, we need to commit this transaction and + // start a new one to avoid not found errors after this + // TODO: this is much worse than other places we do this, because it's two layers of implicit behavior + sess := dsess.DSessFromSess(ctx.Session) + err = commitTransaction(ctx, sess, rsc) + if err != nil { + return err + } + + err = checkoutExistingBranch(ctx, dbName, branchName, apr) + if err != nil { + return err + } + } else if err != nil { + return err + } + + return nil +} + // checkoutTablesFromCommit checks out the tables named from the branch named and overwrites those tables in the // staged and working roots. func checkoutTablesFromCommit( @@ -504,6 +577,7 @@ func checkoutTablesFromCommit( databaseName string, commitRef string, tables []string, + rsc *doltdb.ReplicationStatusController, ) error { dSess := dsess.DSessFromSess(ctx.Session) dbData, ok := dSess.GetDbData(ctx, databaseName) @@ -559,7 +633,7 @@ func checkoutTablesFromCommit( return err } if !tableExistsInHead { - return fmt.Errorf("table %s does not exist in %s", table, commitRef) + return fmt.Errorf("tablespec '%s' did not match any table(s) known to dolt", table) // commitref not mentioned in git } tableNames[i] = name } @@ -570,6 +644,7 @@ func checkoutTablesFromCommit( return err } + dsess.WaitForReplicationController(ctx, *rsc) return dSess.SetWorkingSet(ctx, databaseName, ws.WithStagedRoot(newRoot).WithWorkingRoot(newRoot)) } @@ -585,22 +660,32 @@ func doGlobalCheckout(ctx *sql.Context, branchName string, isForce bool, isNewBr } // checkoutTablesFromHead checks out the tables named from the current head and overwrites those tables in the -// working root. The working root is then set as the new staged root. +// working root. The working root is then set as the new staged root. Necessary since tables may exist outside +// of HEAD commit func checkoutTablesFromHead(ctx *sql.Context, roots doltdb.Roots, name string, tables []string) error { - tableNames := make([]doltdb.TableName, len(tables)) + var tableNames []doltdb.TableName + var err error - for i, table := range tables { - tbl, _, exists, err := actions.FindTableInRoots(ctx, roots, table) + if len(tables) == 1 && tables[0] == "." { + tableNames, err = doltdb.UnionTableNames(ctx, roots.Working) if err != nil { return err } - if !exists { - return fmt.Errorf("error: given tables do not exist") + } else { + tableNames = make([]doltdb.TableName, len(tables)) + for i, table := range tables { + tbl, _, exists, err := actions.FindTableInRoots(ctx, roots, table) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("tablespec '%s' did not match any table(s) known to dolt", table) // commitref not mentioned in git + } + tableNames[i] = tbl } - tableNames[i] = tbl } - roots, err := actions.MoveTablesFromHeadToWorking(ctx, roots, tableNames) + roots, err = actions.MoveTablesFromHeadToWorking(ctx, roots, tableNames) if err != nil { if doltdb.IsRootValUnreachable(err) { rt := doltdb.GetUnreachableRootType(err) @@ -615,3 +700,36 @@ func checkoutTablesFromHead(ctx *sql.Context, roots doltdb.Roots, name string, t dSess := dsess.DSessFromSess(ctx.Session) return dSess.SetRoots(ctx, name, roots) } + +// validateNoDetachedHead checks if the given branchName refers to a tag or commit hash +// which would result in a detached HEAD state, which Dolt doesn't support. +// Returns an error with appropriate message if it's a detached HEAD state, nil otherwise. +func validateNoDetachedHead(ctx *sql.Context, ddb *doltdb.DoltDB, branchName string, isMoveFlag bool) error { + if isTag, err := actions.IsTag(ctx, ddb, branchName); err != nil { + return err + } else if isTag { + // User tried to enter a detached head state, which we don't support. + // Inform and suggest that they check-out a new branch at this tag instead. + if isMoveFlag { + return fmt.Errorf(`dolt does not support a detached head state. To create a branch at this tag, run: + dolt checkout %s -b {new_branch_name}`, branchName) + } else { + return fmt.Errorf(`dolt does not support a detached head state. To create a branch at this tag, run: + CALL DOLT_CHECKOUT('%s', '-b', )`, branchName) + } + } + + if doltdb.IsValidCommitHash(branchName) { + // User tried to enter a detached head state, which we don't support. + // Inform and suggest that they check-out a new branch at this commit instead. + if isMoveFlag { + return fmt.Errorf(`dolt does not support a detached head state. To create a branch at this commit instead, run: + dolt checkout %s -b {new_branch_name}`, branchName) + } else { + return fmt.Errorf(`dolt does not support a detached head state. To create a branch at this commit instead, run: + CALL DOLT_CHECKOUT('%s', '-b', )`, branchName) + } + } + + return nil +} diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go b/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go index 85b04d23780..89651aed631 100755 --- a/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go @@ -1026,6 +1026,9 @@ func RunDoltCheckoutTests(t *testing.T, h DoltEnginetestHarness) { func() { h := h.NewHarness(t) defer h.Close() + if script.Name == "dolt_checkout with tracking branch and table with same name" { + h.UseLocalFileSystem() + } enginetest.TestScript(t, h, script) }() } @@ -1047,12 +1050,18 @@ func RunDoltCheckoutPreparedTests(t *testing.T, h DoltEnginetestHarness) { func() { h := h.NewHarness(t) defer h.Close() + if script.Name == "dolt_checkout with tracking branch and table with same name" { + h.UseLocalFileSystem() + } enginetest.TestScript(t, h, script) }() } h = h.NewHarness(t) - defer h.Close() + defer func() { + time.Sleep(200 * time.Millisecond) // Give time for OS/process to release lock + h.Close() + }() engine, err := h.NewEngine(t) require.NoError(t, err) readOnlyEngine, err := h.NewReadOnlyEngine(engine.EngineAnalyzer().Catalog.DbProvider) diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index 626702d6a0c..2d6b154fa41 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -3388,7 +3388,51 @@ var DoltCheckoutScripts = []queries.ScriptTest{ }, { Query: "call dolt_checkout('HEAD', 't3')", - ExpectedErrStr: "table t3 does not exist in HEAD", + ExpectedErrStr: "tablespec 't3' did not match any table(s) known to dolt", + }, + }, + }, + { + Name: "dolt_checkout with tracking branch and table with same name", + SetUpScript: []string{ + "call dolt_remote('add','origin','file://../remote-repo-483');", + "create table feature (id int primary key, value int);", + "insert into feature values (1, 100);", + "call dolt_add('.');", + "call dolt_commit('-m', 'Add feature table');", + "call dolt_checkout('-b', 'feature');", + "insert into feature values (2, 200);", + "call dolt_add('.');", + "call dolt_commit('-m', 'Add row to feature table');", + "call dolt_push('origin', 'feature');", + "call dolt_checkout('main');", + "update feature set value = 101 where id = 1;", + "call dolt_branch('-D', 'feature');", // remove local branch to force remote tracking branch ambiguity + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_checkout('--', 'feature');", + Expected: []sql.Row{{0, ""}}, + }, + { + Query: "select * from feature order by id;", + Expected: []sql.Row{{1, 100}}, + }, + { + Query: "call dolt_checkout('feature')", + ExpectedErrStr: "'feature' could be both a local table and a tracking branch.\nPlease use -- to disambiguate.", + }, + { + Query: "call dolt_checkout('feature', '--');", + Expected: []sql.Row{{0, "Switched to branch 'feature'\nbranch 'feature' set up to track 'origin/feature'."}}, + }, + { + Query: "select active_branch();", + Expected: []sql.Row{{"feature"}}, + }, + { + Query: "select * from feature order by id;", + Expected: []sql.Row{{1, 100}, {2, 200}}, }, }, }, diff --git a/integration-tests/bats/checkout.bats b/integration-tests/bats/checkout.bats index 85a974df7ac..ba38b68d527 100755 --- a/integration-tests/bats/checkout.bats +++ b/integration-tests/bats/checkout.bats @@ -377,9 +377,6 @@ SQL [[ ! "$output" =~ "76543" ]] || false } - - - @test "checkout: attempting to checkout a detached head shows a suggestion instead" { dolt sql -q "create table test (id int primary key);" dolt add . @@ -800,3 +797,364 @@ SQL [[ "$output" =~ "feature2-change" ]] || false } + +@test "checkout: table and branch name conflict with -- separator" { + # setup a table with the same name as a branch we'll create + dolt sql -q "create table feature (id int primary key, value int);" + dolt sql -q "insert into feature values (1, 100);" + dolt add . + dolt commit -m "Add feature table" + + # create a branch with the same name as the table + dolt checkout -b feature + + dolt sql -q "insert into feature values (2, 200);" + dolt add . + dolt commit -m "Add row to feature table" + + dolt checkout main + + # modify the feature table + dolt sql -q "update feature set value = 101 where id = 1;" + + # use -- to explicitly indicate we want to checkout the table, not the branch + run dolt checkout -- feature + + # verify the table was reset (not switched to feature branch) + run dolt sql -q "select * from feature;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,100" ]] || false + [[ ! "$output" =~ "101" ]] || false + + # verify we're still on main branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* main" ]] || false +} + +@test "checkout: explicit branch checkout with -- separator" { + # setup a table with the same name as a branch + dolt sql -q "create table feature (id int primary key, value int);" + dolt sql -q "insert into feature values (1, 100);" + dolt add . + dolt commit -m "Add feature table" + + # create a branch with the same name as the table + dolt checkout -b feature + dolt sql -q "update feature set value = 200 where id = 1;" + dolt add . + dolt commit -m "Update feature value on feature branch" + + dolt checkout main + + # use explicit branch reference + dolt checkout feature -- + + # verify we switched to feature branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* feature" ]] || false + + # verify we have the feature branch version of the table + run dolt sql -q "select * from feature;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,200" ]] || false +} + +@test "checkout: checkout specific table from branch" { + # setup tables + dolt sql -q "create table users (id int primary key, name varchar(50));" + dolt sql -q "create table products (id int primary key, name varchar(50));" + dolt sql -q "insert into users values (1, 'Alice');" + dolt sql -q "insert into products values (1, 'Widget');" + dolt add . + dolt commit -m "Add initial tables" + + # create a branch with different data + dolt checkout -b feature + dolt sql -q "update users set name = 'Bob' where id = 1;" + dolt sql -q "update products set name = 'Gadget' where id = 1;" + dolt add . + dolt commit -m "Update data on feature branch" + + dolt checkout main + + # checkout only the users table from feature branch + dolt checkout feature -- users + + # verify we got the users table from feature branch + run dolt sql -q "select * from users;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,Bob" ]] || false + + # verify products table is still from main + run dolt sql -q "select * from products;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,Widget" ]] || false + + # verify we're still on main + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* main" ]] || false +} + +@test "checkout: remote tracking branch shorthand" { + mkdir -p remote-repo + mkdir -p local-repo + cd local-repo + dolt init + dolt remote add origin file://../remote-repo + dolt push -u origin main + + # setup initial commit + dolt sql -q "create table test (id int primary key, val int);" + dolt sql -q "insert into test values (1, 100);" + dolt add . + dolt commit -m "Initial commit" + + # create a feature branch and push + dolt checkout -b feature + dolt sql -q "update test set val = 200 where id = 1;" + dolt add . + dolt commit -m "Update on feature branch" + dolt push origin feature + + # verify the remote tracking branch exists + run dolt branch -a + [ "$status" -eq 0 ] + [[ "$output" =~ "remotes/origin/feature" ]] || false + + # use DWIM to checkout and create a local branch from remote tracking branch + dolt checkout main + dolt branch -D feature # delete local feature branch if it exists + dolt checkout feature + + # verify we're now on a local feature branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* feature" ]] || false + + # verify the data from the feature branch + run dolt sql -q "select * from test;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,200" ]] || false +} + +@test "checkout: error on ambiguous name matching tracking branch and table" { + mkdir -p remote-repo + mkdir -p local-repo + cd local-repo + dolt init + dolt remote add origin file://../remote-repo + dolt push -u origin main + + # create a branch called 'feature' on the remote + dolt sql -q "create table test (id int primary key, val int);" + dolt sql -q "insert into test values (1, 50);" + dolt add . + dolt commit -m "Initial commit" + dolt push origin main + dolt checkout -b feature + dolt sql -q "update test set val = 200 where id = 1;" + dolt add . + dolt commit -m "Update on feature branch" + dolt push origin feature + + # verify remote tracking branch exists + run dolt branch -a + [ "$status" -eq 0 ] + [[ "$output" =~ "remotes/origin/feature" ]] || false + + # create a table with the same name as the tracking branch + dolt checkout main + dolt sql -q "create table feature (id int primary key, value int);" + dolt sql -q "insert into feature values (1, 100);" + dolt add . + dolt commit -m "Create table with same name as tracking branch" + + # try to checkout "feature" without disambiguation + # this should fail because it could refer to either the table or the tracking branch + dolt branch -D feature # delete local branch since this only happens when it does not exist + run dolt checkout feature + [ "$status" -ne 0 ] + [[ "$output" =~ "could be both a local table and a tracking branch" ]] || false + [[ "$output" =~ "Please use -- to disambiguate" ]] || false + + # verify we're still on main + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* main" ]] || false + + # test that we can disambiguate with -- for the table + dolt checkout -- feature + + # verify table was restored from HEAD + run dolt sql -q "select * from feature;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,100" ]] || false + + # test that we can disambiguate for the branch using -- + dolt checkout feature -- + + # verify we're now on a local feature branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* feature" ]] || false +} + +@test "checkout: default to local branch checkout after disambiguation" { + mkdir -p remote-repo + mkdir -p local-repo + cd local-repo + dolt init + dolt remote add origin file://../remote-repo + dolt push -u origin main + + # create a branch called 'feature' on the remote + dolt sql -q "create table test (id int primary key, val int);" + dolt sql -q "insert into test values (1, 50);" + dolt add . + dolt commit -m "Initial commit" + dolt push origin main + dolt checkout -b feature + dolt sql -q "update test set val = 200 where id = 1;" + dolt add . + dolt commit -m "Update on feature branch" + dolt push origin feature + + # verify remote tracking branch exists + run dolt branch -a + [ "$status" -eq 0 ] + [[ "$output" =~ "remotes/origin/feature" ]] || false + + # create a table with the same name as the tracking branch + dolt checkout main + dolt sql -q "create table feature (id int primary key, value int);" + dolt sql -q "insert into feature values (1, 100);" + dolt add . + dolt commit -m "Create table with same name as tracking branch" + + # try to checkout "feature" without disambiguation + # this should fail because it could refer to either the table or the tracking branch + dolt branch -D feature # delete local branch since this only happens when it does not exist + + # test that we can disambiguate for the branch using -- + dolt checkout feature -- + + # verify we're now on a local feature branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* feature" ]] || false + + run dolt checkout main + [ "$status" -eq 0 ] + [[ "$output" =~ "Switched to branch 'main'" ]] || false + + run dolt checkout feature + [ "$status" -eq 0 ] + [[ "$output" =~ "Switched to branch 'feature'" ]] || false +} + +@test "checkout: error with multiple refs using --" { + # setup branches and tables + dolt sql -q "create table feature (id int primary key, value int);" + dolt add . + dolt commit -m "Add feature table" + + # create multiple branches + dolt branch branch1 + dolt branch branch2 + + # attempt to checkout with multiple refs, which should fail + run dolt checkout branch1 branch2 -- feature + [ "$status" -ne 0 ] + [[ "$output" =~ "only one reference" ]] || false + + # verify we're still on main branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* main" ]] || false +} + +@test "checkout: checkout multiple tables using --" { + # setup multiple tables + dolt sql -q "create table table1 (id int primary key, value int);" + dolt sql -q "create table table2 (id int primary key, name varchar(50));" + dolt sql -q "insert into table1 values (1, 100);" + dolt sql -q "insert into table2 values (1, 'original');" + dolt add . + dolt commit -m "Add initial tables" + + # create feature branch with modifications to both tables + dolt checkout -b feature + dolt sql -q "update table1 set value = 200 where id = 1;" + dolt sql -q "update table2 set name = 'modified' where id = 1;" + dolt add . + dolt commit -m "Update tables on feature branch" + + # go back to main and make different changes + dolt checkout main + dolt sql -q "update table1 set value = 150 where id = 1;" + dolt sql -q "update table2 set name = 'changed' where id = 1;" + + # checkout multiple tables from feature branch + dolt checkout feature -- table1 table2 + + # verify both tables were updated from feature branch + run dolt sql -q "select * from table1;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,200" ]] || false + + run dolt sql -q "select * from table2;" -r csv + [ "$status" -eq 0 ] + [[ "$output" =~ "1,modified" ]] || false + + # verify we're still on main branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* main" ]] || false +} + +@test "checkout: more than one remote share same branch name" { + # setup two remotes with the same branch name + mkdir -p remote1 + mkdir -p remote2 + dolt remote add origin file://remote1 + dolt remote add origin2 file://remote2 + + # create a branch on both remotes + dolt checkout -b feature + dolt sql -q "create table test (id int primary key, value int);" + dolt sql -q "insert into test values (1, 100);" + dolt add . + dolt commit -m "Add feature table" + dolt push origin feature + dolt push origin2 feature + + # verify both remotes have the feature branch + run dolt branch -a + [ "$status" -eq 0 ] + [[ "$output" =~ "remotes/origin/feature" ]] || false + [[ "$output" =~ "remotes/origin2/feature" ]] || false + + dolt checkout main + dolt branch -D feature # delete local feature branch to cause ambiguity + + # try to checkout feature without disambiguation, should fail + run dolt checkout feature + [ "$status" -ne 0 ] + echo "$output" + [[ "$output" =~ "'feature' matched multiple (2) remote tracking branches" ]] || false + + run dolt checkout --track origin/feature + [ "$status" -eq 0 ] + echo "$output" + [[ "$output" =~ "Switched to branch 'feature'" ]] || false + [[ "$output" =~ "branch 'feature' set up to track 'origin/feature'" ]] || false + + # verify we're still on main branch + run dolt branch + [ "$status" -eq 0 ] + [[ "$output" =~ "* feature" ]] || false +} + diff --git a/integration-tests/bats/empty-repo.bats b/integration-tests/bats/empty-repo.bats index c5bf7496f3a..40c1090faf5 100755 --- a/integration-tests/bats/empty-repo.bats +++ b/integration-tests/bats/empty-repo.bats @@ -135,7 +135,7 @@ teardown() { @test "empty-repo: dolt checkout non-existent branch" { run dolt checkout foo [ "$status" -ne 0 ] - [ "$output" = "error: could not find foo" ] + [[ "$output" =~ "tablespec 'foo' did not match any table(s) known to dolt" ]] || false } @test "empty-repo: create and checkout a branch" { diff --git a/integration-tests/bats/remotes.bats b/integration-tests/bats/remotes.bats index 376256cc6f2..9427a6604d5 100644 --- a/integration-tests/bats/remotes.bats +++ b/integration-tests/bats/remotes.bats @@ -1546,7 +1546,6 @@ SQL run dolt pull [ "$status" -eq 0 ] - echo "$output" [[ "$output" =~ "Everything up-to-date" ]] || false }