diff --git a/go/cmd/dolt/cli/arg_parser_helpers.go b/go/cmd/dolt/cli/arg_parser_helpers.go index 31be2b8b7dd..ed2aeb1acab 100644 --- a/go/cmd/dolt/cli/arg_parser_helpers.go +++ b/go/cmd/dolt/cli/arg_parser_helpers.go @@ -105,6 +105,13 @@ func CreateMergeArgParser() *argparser.ArgParser { return ap } +func CreateStashArgParser() *argparser.ArgParser { + ap := argparser.NewArgParserWithMaxArgs("stash", 3) + ap.SupportsFlag(IncludeUntrackedFlag, "u", "Untracked tables are also stashed.") + ap.SupportsFlag(AllFlag, "a", "All tables are stashed, including untracked and ignored tables.") + return ap +} + func CreateRebaseArgParser() *argparser.ArgParser { ap := argparser.NewArgParserWithMaxArgs("rebase", 1) ap.TooManyArgsErrorFunc = func(receivedArgs []string) error { diff --git a/go/cmd/dolt/cli/flags.go b/go/cmd/dolt/cli/flags.go index 07bcc9748cf..1b68267ec5c 100644 --- a/go/cmd/dolt/cli/flags.go +++ b/go/cmd/dolt/cli/flags.go @@ -42,6 +42,7 @@ const ( GraphFlag = "graph" HardResetParam = "hard" HostFlag = "host" + IncludeUntrackedFlag = "include-untracked" InteractiveFlag = "interactive" ListFlag = "list" MergesFlag = "merges" diff --git a/go/cmd/dolt/commands/stashcmds/clear.go b/go/cmd/dolt/commands/stashcmds/clear.go index 5beb5b245fc..e11805f4b1e 100644 --- a/go/cmd/dolt/commands/stashcmds/clear.go +++ b/go/cmd/dolt/commands/stashcmds/clear.go @@ -17,6 +17,8 @@ package stashcmds import ( "context" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/cmd/dolt/cli" "github.com/dolthub/dolt/go/cmd/dolt/commands" "github.com/dolthub/dolt/go/cmd/dolt/errhand" @@ -77,7 +79,7 @@ func (cmd StashClearCmd) Exec(ctx context.Context, commandStr string, args []str return 1 } - err := dEnv.DoltDB(ctx).RemoveAllStashes(ctx) + err := dEnv.DoltDB(ctx).RemoveAllStashes(ctx, doltdb.DoltCliRef) if err != nil { return commands.HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) } diff --git a/go/cmd/dolt/commands/stashcmds/drop.go b/go/cmd/dolt/commands/stashcmds/drop.go index 3ab46dff04c..98f00621225 100644 --- a/go/cmd/dolt/commands/stashcmds/drop.go +++ b/go/cmd/dolt/commands/stashcmds/drop.go @@ -20,6 +20,8 @@ import ( "strconv" "strings" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/cmd/dolt/cli" "github.com/dolthub/dolt/go/cmd/dolt/commands" "github.com/dolthub/dolt/go/cmd/dolt/errhand" @@ -95,12 +97,12 @@ func (cmd StashDropCmd) Exec(ctx context.Context, commandStr string, args []stri } func dropStashAtIdx(ctx context.Context, dEnv *env.DoltEnv, idx int) error { - stashHash, err := dEnv.DoltDB(ctx).GetStashHashAtIdx(ctx, idx) + stashHash, err := dEnv.DoltDB(ctx).GetStashHashAtIdx(ctx, idx, doltdb.DoltCliRef) if err != nil { return err } - err = dEnv.DoltDB(ctx).RemoveStashAtIdx(ctx, idx) + err = dEnv.DoltDB(ctx).RemoveStashAtIdx(ctx, idx, doltdb.DoltCliRef) if err != nil { return err } diff --git a/go/cmd/dolt/commands/stashcmds/list.go b/go/cmd/dolt/commands/stashcmds/list.go index 965f2ca0d83..97149c12a93 100644 --- a/go/cmd/dolt/commands/stashcmds/list.go +++ b/go/cmd/dolt/commands/stashcmds/list.go @@ -80,7 +80,7 @@ func (cmd StashListCmd) Exec(ctx context.Context, commandStr string, args []stri } func listStashes(ctx context.Context, dEnv *env.DoltEnv) error { - stashes, err := dEnv.DoltDB(ctx).GetStashes(ctx) + stashes, err := dEnv.DoltDB(ctx).GetCommandLineStashes(ctx) if err != nil { return err } @@ -90,7 +90,7 @@ func listStashes(ctx context.Context, dEnv *env.DoltEnv) error { if err != nil { return err } - cli.Println(fmt.Sprintf("%s: WIP on %s: %s %s", stash.Name, stash.BranchName, commitHash.String(), stash.Description)) + cli.Println(fmt.Sprintf("%s: WIP on %s: %s %s", stash.Name, stash.BranchReference, commitHash.String(), stash.Description)) } return nil } diff --git a/go/cmd/dolt/commands/stashcmds/pop.go b/go/cmd/dolt/commands/stashcmds/pop.go index 77aa51957af..18e2b0caa67 100644 --- a/go/cmd/dolt/commands/stashcmds/pop.go +++ b/go/cmd/dolt/commands/stashcmds/pop.go @@ -125,7 +125,7 @@ func (cmd StashPopCmd) Exec(ctx context.Context, commandStr string, args []strin } func applyStashAtIdx(ctx *sql.Context, dEnv *env.DoltEnv, curWorkingRoot doltdb.RootValue, idx int) (bool, error) { - stashRoot, headCommit, meta, err := dEnv.DoltDB(ctx).GetStashRootAndHeadCommitAtIdx(ctx, idx) + stashRoot, headCommit, meta, err := dEnv.DoltDB(ctx).GetStashRootAndHeadCommitAtIdx(ctx, idx, doltdb.DoltCliRef) if err != nil { return false, err } diff --git a/go/cmd/dolt/commands/stashcmds/stash.go b/go/cmd/dolt/commands/stashcmds/stash.go index 94ff40cd487..f1f260ccdc4 100644 --- a/go/cmd/dolt/commands/stashcmds/stash.go +++ b/go/cmd/dolt/commands/stashcmds/stash.go @@ -39,11 +39,6 @@ var StashCommands = cli.NewSubCommandHandlerWithUnspecified("stash", "Stash the StashPopCmd{}, }) -const ( - IncludeUntrackedFlag = "include-untracked" - AllFlag = "all" -) - var stashDocs = cli.CommandDocumentationContent{ ShortDesc: "Stash the changes in a dirty working directory away.", LongDesc: `Use dolt stash when you want to record the current state of the working directory and the index, but want to go back to a clean working directory. @@ -78,8 +73,8 @@ func (cmd StashCmd) Docs() *cli.CommandDocumentation { func (cmd StashCmd) ArgParser() *argparser.ArgParser { ap := argparser.NewArgParserWithMaxArgs(cmd.Name(), 0) - ap.SupportsFlag(IncludeUntrackedFlag, "u", "Untracked tables are also stashed.") - ap.SupportsFlag(AllFlag, "a", "All tables are stashed, including untracked and ignored tables.") + ap.SupportsFlag(cli.IncludeUntrackedFlag, "u", "Untracked tables are also stashed.") + ap.SupportsFlag(cli.AllFlag, "a", "All tables are stashed, including untracked and ignored tables.") return ap } @@ -144,7 +139,7 @@ func hasLocalChanges(ctx context.Context, dEnv *env.DoltEnv, roots doltdb.Roots, } // There are unstaged changes, is --all set? If so, nothing else matters. Stash them. - if apr.Contains(AllFlag) { + if apr.Contains(cli.AllFlag) { return true, nil } @@ -159,7 +154,7 @@ func hasLocalChanges(ctx context.Context, dEnv *env.DoltEnv, roots doltdb.Roots, } // There are unignored, unstaged tables. Is --include-untracked set. If so, nothing else matters. Stash them. - if apr.Contains(IncludeUntrackedFlag) { + if apr.Contains(cli.IncludeUntrackedFlag) { return true, nil } @@ -207,13 +202,13 @@ func stashChanges(ctx context.Context, dEnv *env.DoltEnv, apr *argparser.ArgPars // stage untracked files to include them in the stash, // but do not include them in added table set, // because they should not be staged when popped. - if apr.Contains(IncludeUntrackedFlag) || apr.Contains(AllFlag) { + if apr.Contains(cli.IncludeUntrackedFlag) || apr.Contains(cli.AllFlag) { allTblsToBeStashed, err = doltdb.UnionTableNames(ctx, roots.Staged, roots.Working) if err != nil { return err } - roots, err = actions.StageTables(ctx, roots, allTblsToBeStashed, !apr.Contains(AllFlag)) + roots, err = actions.StageTables(ctx, roots, allTblsToBeStashed, !apr.Contains(cli.AllFlag)) if err != nil { return err } @@ -242,7 +237,7 @@ func stashChanges(ctx context.Context, dEnv *env.DoltEnv, apr *argparser.ArgPars return err } - err = dEnv.DoltDB(ctx).AddStash(ctx, commit, roots.Staged, datas.NewStashMeta(curBranchName, commitMeta.Description, doltdb.FlattenTableNames(addedTblsToStage))) + err = dEnv.DoltDB(ctx).AddStash(ctx, commit, roots.Staged, datas.NewStashMeta(curBranchName, commitMeta.Description, doltdb.FlattenTableNames(addedTblsToStage)), doltdb.DoltCliRef) if err != nil { return err } diff --git a/go/libraries/doltcore/doltdb/doltdb.go b/go/libraries/doltcore/doltdb/doltdb.go index 6539d9c9180..7ed3e0dc6a4 100644 --- a/go/libraries/doltcore/doltdb/doltdb.go +++ b/go/libraries/doltcore/doltdb/doltdb.go @@ -2076,8 +2076,8 @@ func (ddb *DoltDB) GetBranchesByRootHash(ctx context.Context, rootHash hash.Hash // AddStash takes current branch head commit, stash root value and stash metadata to create a new stash. // It stores the new stash object in stash list Dataset, which can be created if it does not exist. // Otherwise, it updates the stash list Dataset as there can only be one stashes Dataset. -func (ddb *DoltDB) AddStash(ctx context.Context, head *Commit, stash RootValue, meta *datas.StashMeta) error { - stashesDS, err := ddb.db.GetDataset(ctx, ref.NewStashRef().String()) +func (ddb *DoltDB) AddStash(ctx context.Context, head *Commit, stash RootValue, meta *datas.StashMeta, stashName string) error { + stashesDS, err := ddb.db.GetDataset(ctx, ref.NewStashRef(stashName).String()) if err != nil { return err } @@ -2159,8 +2159,8 @@ func (ddb *DoltDB) GetStatistics(ctx context.Context) (prolly.Map, error) { // It removes a Stash message from stash list Dataset, which cannot be performed // by database Delete function. This function removes a single stash only and stash // list dataset does not get removed if there are no entries left. -func (ddb *DoltDB) RemoveStashAtIdx(ctx context.Context, idx int) error { - stashesDS, err := ddb.db.GetDataset(ctx, ref.NewStashRef().String()) +func (ddb *DoltDB) RemoveStashAtIdx(ctx context.Context, idx int, stashName string) error { + stashesDS, err := ddb.db.GetDataset(ctx, ref.NewStashRef(stashName).String()) if err != nil { return err } @@ -2186,7 +2186,7 @@ func (ddb *DoltDB) RemoveStashAtIdx(ctx context.Context, idx int) error { } // if the stash list is empty, remove the stash list Dataset from the database if stashListCount == 0 { - return ddb.RemoveAllStashes(ctx) + return ddb.RemoveAllStashes(ctx, stashName) } stashesDS, err = ddb.db.UpdateStashList(ctx, stashesDS, stashListAddr) @@ -2195,31 +2195,64 @@ func (ddb *DoltDB) RemoveStashAtIdx(ctx context.Context, idx int) error { // RemoveAllStashes removes the stash list Dataset from the database, // which equivalent to removing Stash entries from the stash list. -func (ddb *DoltDB) RemoveAllStashes(ctx context.Context) error { - err := ddb.deleteRef(ctx, ref.NewStashRef(), nil, "") +func (ddb *DoltDB) RemoveAllStashes(ctx context.Context, stashName string) error { + err := ddb.deleteRef(ctx, ref.NewStashRef(stashName), nil, "") if err == ErrBranchNotFound { return nil } return err } +var stashRefFilter = map[ref.RefType]struct{}{ref.StashRefType: {}} + // GetStashes returns array of Stash objects containing all stash entries in the stash list Dataset. func (ddb *DoltDB) GetStashes(ctx context.Context) ([]*Stash, error) { - stashesDS, err := ddb.db.GetDataset(ctx, ref.NewStashRef().String()) + stashRefs, err := ddb.GetRefsOfType(ctx, stashRefFilter) if err != nil { return nil, err } + var stashList []*Stash + for _, stash := range stashRefs { + reference := ref.NewStashRef(stash.String()).String() + stashDS, err := ddb.db.GetDataset(ctx, reference) + if err != nil { + return nil, err + } + newStashes, err := getStashList(ctx, stashDS, ddb.vrw, ddb.NodeStore(), reference) + if err != nil { + return nil, err + } + stashList = append(stashList, newStashes...) + } - if !stashesDS.HasHead() { - return []*Stash{}, nil + return stashList, nil +} + +func (ddb *DoltDB) GetCommandLineStashes(ctx context.Context) ([]*Stash, error) { + var stashList []*Stash + reference := ref.NewStashRef(DoltCliRef).String() + stashDS, err := ddb.db.GetDataset(ctx, reference) + if err != nil { + return nil, err + } + + // If the refs/stashes/dolt-cli is empty, hasHead will return false. + // In this case we want to end early and return no stashes. + if !stashDS.HasHead() { + return nil, nil + } + newStashes, err := getStashList(ctx, stashDS, ddb.vrw, ddb.NodeStore(), reference) + if err != nil { + return nil, err } + stashList = append(stashList, newStashes...) - return getStashList(ctx, stashesDS, ddb.vrw, ddb.NodeStore()) + return stashList, nil } // GetStashHashAtIdx returns hash address only of the stash at given index. -func (ddb *DoltDB) GetStashHashAtIdx(ctx context.Context, idx int) (hash.Hash, error) { - ds, err := ddb.db.GetDataset(ctx, ref.NewStashRef().String()) +func (ddb *DoltDB) GetStashHashAtIdx(ctx context.Context, idx int, stashName string) (hash.Hash, error) { + ds, err := ddb.db.GetDataset(ctx, ref.NewStashRef(stashName).String()) if err != nil { return hash.Hash{}, err } @@ -2233,8 +2266,8 @@ func (ddb *DoltDB) GetStashHashAtIdx(ctx context.Context, idx int) (hash.Hash, e // GetStashRootAndHeadCommitAtIdx returns root value of stash working set and head commit of the branch that the stash was made on // of the stash at given index. -func (ddb *DoltDB) GetStashRootAndHeadCommitAtIdx(ctx context.Context, idx int) (RootValue, *Commit, *datas.StashMeta, error) { - ds, err := ddb.db.GetDataset(ctx, ref.NewStashRef().String()) +func (ddb *DoltDB) GetStashRootAndHeadCommitAtIdx(ctx context.Context, idx int, stashName string) (RootValue, *Commit, *datas.StashMeta, error) { + ds, err := ddb.db.GetDataset(ctx, ref.NewStashRef(stashName).String()) if err != nil { return nil, nil, nil, err } diff --git a/go/libraries/doltcore/doltdb/stash.go b/go/libraries/doltcore/doltdb/stash.go index c4e6c4b2131..a5be6c3b0d7 100644 --- a/go/libraries/doltcore/doltdb/stash.go +++ b/go/libraries/doltcore/doltdb/stash.go @@ -26,14 +26,19 @@ import ( ) type Stash struct { - Name string - BranchName string - Description string - HeadCommit *Commit + Name string + BranchReference string + Description string + HeadCommit *Commit + StashReference string } +const ( + DoltCliRef = "dolt-cli" +) + // getStashList returns array of Stash objects containing all stash entries in the stash list map. -func getStashList(ctx context.Context, ds datas.Dataset, vrw types.ValueReadWriter, ns tree.NodeStore) ([]*Stash, error) { +func getStashList(ctx context.Context, ds datas.Dataset, vrw types.ValueReadWriter, ns tree.NodeStore, reference string) ([]*Stash, error) { v, ok := ds.MaybeHead() if !ok { return nil, errors.New("stashes not found") @@ -73,8 +78,9 @@ func getStashList(ctx context.Context, ds datas.Dataset, vrw types.ValueReadWrit } s.HeadCommit = headCommit - s.BranchName = meta.BranchName + s.BranchReference = meta.BranchName s.Description = meta.Description + s.StashReference = reference sl[i] = &s } diff --git a/go/libraries/doltcore/doltdb/system_table.go b/go/libraries/doltcore/doltdb/system_table.go index 8b0561208b3..65c1c9d6aec 100644 --- a/go/libraries/doltcore/doltdb/system_table.go +++ b/go/libraries/doltcore/doltdb/system_table.go @@ -221,6 +221,7 @@ var getGeneratedSystemTables = func() []string { GetRemotesTableName(), GetHelpTableName(), GetBackupsTableName(), + GetStashesTableName(), } } @@ -393,6 +394,10 @@ var GetBackupsTableName = func() string { return BackupsTableName } +var GetStashesTableName = func() string { + return StashesTableName +} + const ( // LogTableName is the log system table name LogTableName = "dolt_log" @@ -444,6 +449,9 @@ const ( // StatisticsTableName is the statistics system table name StatisticsTableName = "dolt_statistics" + + // StashesTableName is the stashes system table name + StashesTableName = "dolt_stashes" ) const ( diff --git a/go/libraries/doltcore/ref/ref.go b/go/libraries/doltcore/ref/ref.go index 502ef416ff5..72eef681feb 100644 --- a/go/libraries/doltcore/ref/ref.go +++ b/go/libraries/doltcore/ref/ref.go @@ -197,7 +197,7 @@ func Parse(str string) (DoltRef, error) { str = str[len(prefix):] switch rType { case StashRefType: - return NewStashRef(), nil + return NewStashRef(str), nil default: panic("unknown type " + rType) } diff --git a/go/libraries/doltcore/ref/stash_ref.go b/go/libraries/doltcore/ref/stash_ref.go index ade5287d0ad..2e18d90be00 100644 --- a/go/libraries/doltcore/ref/stash_ref.go +++ b/go/libraries/doltcore/ref/stash_ref.go @@ -18,18 +18,14 @@ import ( "strings" ) -// StashRefName is a dummy name, and there cannot be more than one stash ref. -const StashRefName = "stashes" - type StashRef struct { stash string } var _ DoltRef = StashRef{} -// NewStashRef creates a reference to a stashes list. There cannot be more than one stashRef. -func NewStashRef() StashRef { - stashName := StashRefName +// NewStashRef creates a reference to a stashes list. +func NewStashRef(stashName string) StashRef { if IsRef(stashName) { prefix := PrefixForType(StashRefType) if strings.HasPrefix(stashName, prefix) { diff --git a/go/libraries/doltcore/sqle/database.go b/go/libraries/doltcore/sqle/database.go index 8b39cce636a..37a18079deb 100644 --- a/go/libraries/doltcore/sqle/database.go +++ b/go/libraries/doltcore/sqle/database.go @@ -582,6 +582,14 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds if !resolve.UseSearchPath || isDoltgresSystemTable { dt, found = dtables.NewRemotesTable(ctx, db.ddb, lwrName), true } + case doltdb.StashesTableName, doltdb.GetStashesTableName(): + isDoltgresSystemTable, err := resolve.IsDoltgresSystemTable(ctx, tname, root) + if err != nil { + return nil, false, err + } + if !resolve.UseSearchPath || isDoltgresSystemTable { + dt, found = dtables.NewStashesTable(ctx, db.ddb, lwrName), true + } case doltdb.CommitsTableName, doltdb.GetCommitsTableName(): isDoltgresSystemTable, err := resolve.IsDoltgresSystemTable(ctx, tname, root) if err != nil { diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_stash.go b/go/libraries/doltcore/sqle/dprocedures/dolt_stash.go new file mode 100644 index 00000000000..eb212f6c49d --- /dev/null +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_stash.go @@ -0,0 +1,527 @@ +// Copyright 2022 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 dprocedures + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/dolt/go/cmd/dolt/cli" + "github.com/dolthub/dolt/go/libraries/doltcore/diff" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/env" + "github.com/dolthub/dolt/go/libraries/doltcore/env/actions" + "github.com/dolthub/dolt/go/libraries/doltcore/merge" + "github.com/dolthub/dolt/go/libraries/doltcore/ref" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" + "github.com/dolthub/dolt/go/libraries/doltcore/table/editor" + "github.com/dolthub/dolt/go/libraries/utils/argparser" + "github.com/dolthub/dolt/go/store/datas" + "github.com/dolthub/dolt/go/store/hash" +) + +// doltStash is the stored procedure version for the CLI command `dolt stash` +// and its options push, pop, drop, and clear +func doltStash(ctx *sql.Context, args ...string) (sql.RowIter, error) { + res, err := doDoltStash(ctx, args) + if err != nil { + return nil, err + } + + return rowToIter(res), nil +} + +func doDoltStash(ctx *sql.Context, args []string) (int, error) { + dbName := ctx.GetCurrentDatabase() + + dSess := dsess.DSessFromSess(ctx.Session) + dbData, ok := dSess.GetDbData(ctx, dbName) + if !ok { + return cmdFailure, fmt.Errorf("Could not load database %s", dbName) + } + if !dbData.Ddb.Format().UsesFlatbuffers() { + return cmdFailure, fmt.Errorf("stash is not supported for old storage format") + } + + roots, ok := dSess.GetRoots(ctx, dbName) + if !ok { + return cmdFailure, fmt.Errorf("Could not load roots for database %s", dbName) + } + + apr, err := cli.CreateStashArgParser().Parse(args) + if err != nil { + return cmdFailure, err + } + + if apr.NArg() < 2 { + return cmdFailure, fmt.Errorf("error: invalid arguments. Must provide valid subcommand and stash name") + } + + cmdName := apr.Arg(0) + stashName := apr.Arg(1) + idx, err := parseStashIndex(apr) + if err != nil { + return cmdFailure, err + } + + switch cmdName { + case "push": + if apr.NArg() > 2 { // Push does not take extra arguments + return cmdFailure, fmt.Errorf("error: invalid arguments. Push takes only subcommand and stash name") + } + err = doStashPush(ctx, dSess, dbData, roots, apr, stashName) + case "pop": + err = doStashPop(ctx, dbData, stashName, idx) + case "drop": + err = doStashDrop(ctx, dbData, stashName, idx) + case "clear": + if apr.NArg() > 2 { // Clear does not take extra arguments + return cmdFailure, fmt.Errorf("error: invalid arguments. Clear takes only subcommand and stash name") + } + err = doStashClear(ctx, dbData, stashName) + default: + return cmdFailure, fmt.Errorf("unknown stash subcommand %s", cmdName) + } + + if err != nil { + return cmdFailure, err + } + + return cmdSuccess, nil +} + +func doStashPush(ctx *sql.Context, dSess *dsess.DoltSession, dbData env.DbData[*sql.Context], roots doltdb.Roots, apr *argparser.ArgParseResults, stashName string) error { + hasChanges, err := hasLocalChanges(ctx, dSess, roots, apr) + if err != nil { + return err + } + if !hasChanges { + return fmt.Errorf("no local changes to save") + } + + roots, err = actions.StageModifiedAndDeletedTables(ctx, roots) + if err != nil { + return err + } + + // all tables with changes that are going to be stashed are staged at this point + allTblsToBeStashed, addedTblsToStage, err := stashedTableSets(ctx, roots) + if err != nil { + return err + } + + if apr.Contains(cli.IncludeUntrackedFlag) || apr.Contains(cli.AllFlag) { + allTblsToBeStashed, err = doltdb.UnionTableNames(ctx, roots.Staged, roots.Working) + if err != nil { + return err + } + + roots, err = actions.StageTables(ctx, roots, allTblsToBeStashed, !apr.Contains("all")) + if err != nil { + return err + } + } + + commit, commitMeta, curBranchName, err := gatherCommitData(ctx, dbData) + if err != nil { + return err + } + + err = dbData.Ddb.AddStash(ctx, commit, roots.Staged, datas.NewStashMeta(curBranchName, commitMeta.Description, doltdb.FlattenTableNames(addedTblsToStage)), stashName) + if err != nil { + return err + } + + roots.Staged = roots.Head + roots, err = actions.MoveTablesFromHeadToWorking(ctx, roots, allTblsToBeStashed) + if err != nil { + return err + } + + return updateWorkingSetFromRoots(ctx, dbData, roots) +} + +func doStashPop(ctx *sql.Context, dbData env.DbData[*sql.Context], stashName string, idx int) error { + headCommit, result, meta, err := handleMerge(ctx, dbData, stashName, idx) + if err != nil { + return err + } + + err = updateWorkingRoot(ctx, dbData, result.Root) + if err != nil { + return err + } + + roots, err := getRoots(ctx, dbData, headCommit) + if err != nil { + return err + } + + // added tables need to be staged + // since these tables are coming from a stash, don't filter for ignored table names. + roots, err = actions.StageTables(ctx, roots, doltdb.ToTableNames(meta.TablesToStage, doltdb.DefaultSchemaName), false) + if err != nil { + return err + } + + err = updateWorkingSetFromRoots(ctx, dbData, roots) + if err != nil { + return err + } + + return dbData.Ddb.RemoveStashAtIdx(ctx, idx, stashName) +} + +func doStashDrop(ctx *sql.Context, dbData env.DbData[*sql.Context], stashName string, idx int) error { + return dbData.Ddb.RemoveStashAtIdx(ctx, idx, stashName) +} + +func doStashClear(ctx *sql.Context, dbData env.DbData[*sql.Context], stashName string) error { + return dbData.Ddb.RemoveAllStashes(ctx, stashName) +} + +func stashedTableSets(ctx context.Context, roots doltdb.Roots) ([]doltdb.TableName, []doltdb.TableName, error) { + var addedTblsInStaged []doltdb.TableName + var allTbls []doltdb.TableName + staged, _, err := diff.GetStagedUnstagedTableDeltas(ctx, roots) + if err != nil { + return nil, nil, err + } + + for _, tableDelta := range staged { + tblName := tableDelta.ToName + if tableDelta.IsAdd() { + addedTblsInStaged = append(addedTblsInStaged, tableDelta.ToName) + } + if tableDelta.IsDrop() { + tblName = tableDelta.FromName + } + allTbls = append(allTbls, tblName) + } + + return allTbls, addedTblsInStaged, nil +} + +func hasLocalChanges(ctx *sql.Context, dSess *dsess.DoltSession, roots doltdb.Roots, apr *argparser.ArgParseResults) (bool, error) { + dbName := ctx.GetCurrentDatabase() + + headCommit, err := dSess.GetHeadCommit(ctx, dbName) + if err != nil { + return false, err + } + + headRoot, err := headCommit.GetRootValue(ctx) + if err != nil { + return false, err + } + + workingSet, err := dSess.WorkingSet(ctx, dbName) + if err != nil { + return false, err + } + workingRoot := workingSet.WorkingRoot() + stagedRoot := workingSet.StagedRoot() + + headHash, err := headRoot.HashOf() + if err != nil { + return false, err + } + workingHash, err := workingRoot.HashOf() + if err != nil { + return false, err + } + stagedHash, err := stagedRoot.HashOf() + if err != nil { + return false, err + } + + // Are there staged changes? If so, stash them. + if !headHash.Equal(stagedHash) { + return true, nil + } + + // No staged changes, but are there any unstaged changes? If not, no work is needed. + if headHash.Equal(workingHash) { + return false, nil + } + + // There are unstaged changes, is --all set? If so, nothing else matters. Stash them. + if apr.Contains(cli.AllFlag) { + return true, nil + } + + // --all was not set, so we can ignore tables. Is every table ignored? + allIgnored, err := diff.WorkingSetContainsOnlyIgnoredTables(ctx, roots) + if err != nil || allIgnored { + return false, err + } + + // There are unignored, unstaged tables. Is --include-untracked set? If so, nothing else matters. Stash them. + if apr.Contains(cli.IncludeUntrackedFlag) { + return true, nil + } + + // --include-untracked was not set, so we can skip untracked tables. Is every table untracked? + allUntracked, err := workingSetContainsOnlyUntrackedTables(ctx, roots) + if err != nil || allUntracked { + return false, err + } + + // There are changes to tracked tables. Stash them. + return true, nil +} + +func workingSetContainsOnlyUntrackedTables(ctx context.Context, roots doltdb.Roots) (bool, error) { + _, unstaged, err := diff.GetStagedUnstagedTableDeltas(ctx, roots) + if err != nil { + return false, err + } + + // All ignored files are also untracked files + for _, tableDelta := range unstaged { + if !tableDelta.IsAdd() { + return false, nil + } + } + + return true, nil +} + +func updateWorkingSetFromRoots(ctx *sql.Context, dbData env.DbData[*sql.Context], roots doltdb.Roots) error { + ws, err := env.WorkingSet(ctx, dbData.Ddb, dbData.Rsr) + if err == doltdb.ErrWorkingSetNotFound { + headRef, err := dbData.Rsr.CWBHeadRef(ctx) + if err != nil { + return err + } + wsRef, err := ref.WorkingSetRefForHead(headRef) + if err != nil { + return err + } + ws = doltdb.EmptyWorkingSet(wsRef) + } else if err != nil { + return err + } + + ws = ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged) + + currentWs, err := env.WorkingSet(ctx, dbData.Ddb, dbData.Rsr) + if err != doltdb.ErrWorkingSetNotFound && err != nil { + return err + } + + var h hash.Hash + if currentWs != nil { + h, err = currentWs.HashOf() + if err != nil { + return err + } + } + + wsm := &datas.WorkingSetMeta{ + Timestamp: uint64(time.Now().Unix()), + Description: "updated from dolt environment", + } + + return dbData.Ddb.UpdateWorkingSet(ctx, ws.Ref(), ws, h, wsm, nil) +} + +func parseStashIndex(apr *argparser.ArgParseResults) (int, error) { + idx := 0 + + if apr.NArg() > 2 { + stashID := apr.Arg(2) + var err error + + stashID = strings.TrimSuffix(strings.TrimPrefix(stashID, "stash@{"), "}") + idx, err = strconv.Atoi(stashID) + if err != nil { + return 0, fmt.Errorf("error: %s is not a valid reference", stashID) + } + } + + return idx, nil +} + +func bulkDbEaFactory(dbData env.DbData[*sql.Context]) editor.DbEaFactory { + tmpDir, err := dbData.Rsw.TempTableFilesDir() + if err != nil { + return nil + } + return editor.NewBulkImportTEAFactory(dbData.Ddb.ValueReadWriter(), tmpDir) +} + +func updateWorkingRoot(ctx *sql.Context, dbData env.DbData[*sql.Context], newRoot doltdb.RootValue) error { + var h hash.Hash + var wsRef ref.WorkingSetRef + headRef, err := dbData.Rsr.CWBHeadRef(ctx) + if err != nil { + return err + } + + ws, err := env.WorkingSet(ctx, dbData.Ddb, dbData.Rsr) + if err == doltdb.ErrWorkingSetNotFound { + wsRef, err = ref.WorkingSetRefForHead(headRef) + if err != nil { + return err + } + ws = doltdb.EmptyWorkingSet(wsRef).WithWorkingRoot(newRoot).WithStagedRoot(newRoot) + } else if err != nil { + return err + } else { + h, err = ws.HashOf() + if err != nil { + return err + } + + wsRef = ws.Ref() + } + + wsm := &datas.WorkingSetMeta{ + Timestamp: uint64(time.Now().Unix()), + Description: "updated from dolt environment", + } + + return dbData.Ddb.UpdateWorkingSet(ctx, wsRef, ws.WithWorkingRoot(newRoot), h, wsm, nil) +} + +// gatherCommitData is a helper function that returns the commit and commit metadata associated with the current head +// reference as well as the current branch in the form of a string. +func gatherCommitData(ctx *sql.Context, dbData env.DbData[*sql.Context]) (*doltdb.Commit, *datas.CommitMeta, string, error) { + curHeadRef, err := dbData.Rsr.CWBHeadRef(ctx) + if err != nil { + return nil, nil, "", err + } + + curBranchName := curHeadRef.String() + commitSpec, err := doltdb.NewCommitSpec(curBranchName) + if err != nil { + return nil, nil, "", err + } + optCmt, err := dbData.Ddb.Resolve(ctx, commitSpec, curHeadRef) + if err != nil { + return nil, nil, "", err + } + commit, ok := optCmt.ToCommit() + if !ok { + return nil, nil, "", doltdb.ErrGhostCommitEncountered + } + + commitMeta, err := commit.GetCommitMeta(ctx) + if err != nil { + return nil, nil, "", err + } + + return commit, commitMeta, curBranchName, nil + +} + +func handleMerge(ctx *sql.Context, dbData env.DbData[*sql.Context], stashName string, idx int) (*doltdb.Commit, *merge.Result, *datas.StashMeta, error) { + headRef, err := dbData.Rsr.CWBHeadRef(ctx) + if err != nil { + return nil, nil, nil, err + } + workingSetRef, err := ref.WorkingSetRefForHead(headRef) + if err != nil { + return nil, nil, nil, err + } + workingSet, err := dbData.Ddb.ResolveWorkingSet(ctx, workingSetRef) + if err != nil { + return nil, nil, nil, err + } + curWorkingRoot := workingSet.WorkingRoot() + + stashRoot, headCommit, meta, err := dbData.Ddb.GetStashRootAndHeadCommitAtIdx(ctx, idx, stashName) + if err != nil { + return nil, nil, nil, err + } + + hch, err := headCommit.HashOf() + if err != nil { + return nil, nil, nil, err + } + headCommitSpec, err := doltdb.NewCommitSpec(hch.String()) + if err != nil { + return nil, nil, nil, err + } + + optCmt, err := dbData.Ddb.Resolve(ctx, headCommitSpec, headRef) + if err != nil { + return nil, nil, nil, err + } + parentCommit, ok := optCmt.ToCommit() + if !ok { + // Should not be possible to get into this situation. The parent of the stashed commit + // Must have been present at the time it was created + return nil, nil, nil, err + } + + parentRoot, err := parentCommit.GetRootValue(ctx) + if err != nil { + return nil, nil, nil, err + } + + tmpDir, err := dbData.Rsw.TempTableFilesDir() + if err != nil { + return nil, nil, nil, err + } + + opts := editor.Options{Deaf: bulkDbEaFactory(dbData), Tempdir: tmpDir} + result, err := merge.MergeRoots(ctx, curWorkingRoot, stashRoot, parentRoot, stashRoot, parentCommit, opts, merge.MergeOpts{IsCherryPick: false}) + if err != nil { + return nil, nil, nil, err + } + + var tablesWithConflict []doltdb.TableName + for tbl, stats := range result.Stats { + if stats.HasConflicts() { + tablesWithConflict = append(tablesWithConflict, tbl) + } + } + + if len(tablesWithConflict) > 0 { + tblNames := strings.Join(doltdb.FlattenTableNames(tablesWithConflict), "', '") + status := fmt.Errorf("error: Your local changes to the following tables would be overwritten by applying stash %d:\n"+ + "\t{'%s'}\n"+ + "Please commit your changes or stash them before you merge.\nAborting\n", idx, tblNames) + return nil, nil, nil, status + } + + return headCommit, result, meta, nil +} + +func getRoots(ctx *sql.Context, dbData env.DbData[*sql.Context], headCommit *doltdb.Commit) (doltdb.Roots, error) { + roots := doltdb.Roots{} + + headRoot, err := headCommit.GetRootValue(ctx) + if err != nil { + return roots, err + } + ws, err := env.WorkingSet(ctx, dbData.Ddb, dbData.Rsr) + if err != nil { + return roots, err + } + + roots.Head = headRoot + roots.Working = ws.WorkingRoot() + roots.Staged = ws.StagedRoot() + + return roots, nil +} diff --git a/go/libraries/doltcore/sqle/dprocedures/init.go b/go/libraries/doltcore/sqle/dprocedures/init.go index 61aa99c562e..e1bb381db37 100644 --- a/go/libraries/doltcore/sqle/dprocedures/init.go +++ b/go/libraries/doltcore/sqle/dprocedures/init.go @@ -46,6 +46,7 @@ var DoltProcedures = []sql.ExternalStoredProcedureDetails{ {Name: "dolt_remote", Schema: int64Schema("status"), Function: doltRemote, AdminOnly: true}, {Name: "dolt_reset", Schema: int64Schema("status"), Function: doltReset}, {Name: "dolt_revert", Schema: int64Schema("status"), Function: doltRevert}, + {Name: "dolt_stash", Schema: int64Schema("status"), Function: doltStash}, {Name: "dolt_tag", Schema: int64Schema("status"), Function: doltTag}, {Name: "dolt_verify_constraints", Schema: int64Schema("violations"), Function: doltVerifyConstraints}, diff --git a/go/libraries/doltcore/sqle/dtables/stashes_table.go b/go/libraries/doltcore/sqle/dtables/stashes_table.go new file mode 100644 index 00000000000..59fcd01fdd0 --- /dev/null +++ b/go/libraries/doltcore/sqle/dtables/stashes_table.go @@ -0,0 +1,209 @@ +// Copyright 2025 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 dtables + +import ( + "fmt" + "io" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/ref" + "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/index" +) + +var _ sql.Table = (*StashesTable)(nil) +var _ sql.StatisticsTable = (*StashesTable)(nil) + +type StashesTable struct { + ddb *doltdb.DoltDB + tableName string +} + +func NewStashesTable(_ *sql.Context, ddb *doltdb.DoltDB, tableName string) sql.Table { + return &StashesTable{ddb, tableName} +} + +func (st *StashesTable) DataLength(ctx *sql.Context) (uint64, error) { + numBytesPerRow := schema.SchemaAvgLength(st.Schema()) + numRows, _, err := st.RowCount(ctx) + if err != nil { + return 0, err + } + return numBytesPerRow * numRows, nil +} + +func (st *StashesTable) RowCount(ctx *sql.Context) (uint64, bool, error) { + dbName := ctx.GetCurrentDatabase() + + if len(dbName) == 0 { + return 0, false, fmt.Errorf("Empty database name.") + } + + sess := dsess.DSessFromSess(ctx.Session) + dbData, ok := sess.GetDbData(ctx, dbName) + if !ok { + return 0, false, sql.ErrDatabaseNotFound.New(dbName) + } + + stashes, err := dbData.Ddb.GetStashes(ctx) + if err != nil { + return 0, false, err + } + return uint64(len(stashes)), true, nil +} + +// Name is a sql.Table interface function which returns the name of the table +func (st *StashesTable) Name() string { + return st.tableName +} + +// String is a sql.Table interface function which returns the name of the table +func (st *StashesTable) String() string { + return st.tableName +} + +// Schema is a sql.Table interface function that gets the sql.Schema of the remotes system table +func (st *StashesTable) Schema() sql.Schema { + return []*sql.Column{ + {Name: "name", Type: types.Text, Source: st.tableName, PrimaryKey: false, Nullable: false}, + {Name: "stash_id", Type: types.Text, Source: st.tableName, PrimaryKey: false, Nullable: false}, + {Name: "branch", Type: types.Text, Source: st.tableName, PrimaryKey: false, Nullable: false}, + {Name: "hash", Type: types.Text, Source: st.tableName, PrimaryKey: false, Nullable: false}, + {Name: "commit_message", Type: types.Text, Source: st.tableName, PrimaryKey: false, Nullable: true}, + } +} + +// Collation implements the sql.Table interface. +func (st *StashesTable) Collation() sql.CollationID { + return sql.Collation_Default +} + +// Partitions is a sql.Table interface function that returns a partition of the data. Currently the data is unpartitioned. +func (st *StashesTable) Partitions(*sql.Context) (sql.PartitionIter, error) { + return index.SinglePartitionIterFromNomsMap(nil), nil +} + +// PartitionRows is a sql.Table interface function that gets a row iterator for a partition +func (st *StashesTable) PartitionRows(ctx *sql.Context, _ sql.Partition) (sql.RowIter, error) { + return NewStashItr(ctx, st.ddb) +} + +type StashItr struct { + stashes []*doltdb.Stash + idx int +} + +// NewStashItr creates a StashItr from the current environment. +func NewStashItr(ctx *sql.Context, _ *doltdb.DoltDB) (*StashItr, error) { + dbName := ctx.GetCurrentDatabase() + + if len(dbName) == 0 { + return nil, fmt.Errorf("Empty database name.") + } + + sess := dsess.DSessFromSess(ctx.Session) + dbData, ok := sess.GetDbData(ctx, dbName) + if !ok { + return nil, sql.ErrDatabaseNotFound.New(dbName) + } + + stashes, err := dbData.Ddb.GetStashes(ctx) + if err != nil { + return nil, err + } + + return &StashItr{stashes, 0}, nil +} + +// Next retrieves the next row. It will return io.EOF if it's the last row. +// After retrieving the last row, Close will be automatically closed. +func (itr *StashItr) Next(*sql.Context) (sql.Row, error) { + if itr.idx >= len(itr.stashes) { + return nil, io.EOF + } + + defer func() { + itr.idx++ + }() + + stash := itr.stashes[itr.idx] + commitHash, err := stash.HeadCommit.HashOf() + if err != nil { + return nil, err + } + + // BranchName and StashReference are of the form refs/heads/name + // or refs/stashes/name, so we need to parse them to get names + branch := ref.NewBranchRef(stash.BranchReference).GetPath() + stashRef := ref.NewStashRef(stash.StashReference).GetPath() + + return sql.NewRow(stashRef, stash.Name, branch, commitHash.String(), stash.Description), nil +} + +// Close closes the iterator. +func (itr *StashItr) Close(*sql.Context) error { + return nil +} + +var _ sql.RowReplacer = stashWriter{nil} +var _ sql.RowUpdater = stashWriter{nil} +var _ sql.RowInserter = stashWriter{nil} +var _ sql.RowDeleter = stashWriter{nil} + +type stashWriter struct { + rt *StashesTable +} + +// Insert inserts the row given, returning an error if it cannot. Insert will be called once for each row to process +// for the insert operation, which may involve many rows. After all rows in an operation have been processed, Close +// is called. +func (bWr stashWriter) Insert(_ *sql.Context, _ sql.Row) error { + return fmt.Errorf("the dolt_stashes table is read-only; use the dolt_stash stored procedure to edit stashes") +} + +// Update the given row. Provides both the old and new rows. +func (bWr stashWriter) Update(_ *sql.Context, _ sql.Row, _ sql.Row) error { + return fmt.Errorf("the dolt_stash table is read-only; use the dolt_stash stored procedure to edit stashes") +} + +// Delete deletes the given row. Returns ErrDeleteRowNotFound if the row was not found. Delete will be called once for +// each row to process for the delete operation, which may involve many rows. After all rows have been processed, +// Close is called. +func (bWr stashWriter) Delete(_ *sql.Context, _ sql.Row) error { + return fmt.Errorf("the dolt_stash table is read-only; use the dolt_stash stored procedure to edit stashes") +} + +// StatementBegin implements the interface sql.TableEditor. Currently a no-op. +func (bWr stashWriter) StatementBegin(*sql.Context) {} + +// DiscardChanges implements the interface sql.TableEditor. Currently a no-op. +func (bWr stashWriter) DiscardChanges(_ *sql.Context, _ error) error { + return nil +} + +// StatementComplete implements the interface sql.TableEditor. Currently a no-op. +func (bWr stashWriter) StatementComplete(*sql.Context) error { + return nil +} + +// Close finalizes the delete operation, persisting the result. +func (bWr stashWriter) Close(*sql.Context) error { + return nil +} diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go b/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go index 2971986b29d..16c450aaf66 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go @@ -2119,3 +2119,9 @@ func TestDoltHelpSystemTable(t *testing.T) { defer harness.Close() RunDoltHelpSystemTableTests(t, harness) } + +func TestDoltStash(t *testing.T) { + harness := newDoltEnginetestHarness(t) + defer harness.Close() + RunDoltStashSystemTableTests(t, harness) +} diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go b/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go index aeb61a19083..21101586ab3 100755 --- a/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go @@ -1992,3 +1992,13 @@ func RunDoltHelpSystemTableTests(t *testing.T, harness DoltEnginetestHarness) { }) } } + +func RunDoltStashSystemTableTests(t *testing.T, h DoltEnginetestHarness) { + for _, script := range DoltStashTests { + func() { + h := h.NewHarness(t) + defer h.Close() + enginetest.TestScript(t, h, script) + }() + } +} diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index 4afe8b42883..f40045d6ccf 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -7951,6 +7951,7 @@ var DoltSystemVariables = []queries.ScriptTest{ {"dolt_log"}, {"dolt_remote_branches"}, {"dolt_remotes"}, + {"dolt_stashes"}, {"dolt_status"}, {"dolt_workspace_test"}, {"test"}, diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_stash.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_stash.go new file mode 100644 index 00000000000..cb984efdc32 --- /dev/null +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_stash.go @@ -0,0 +1,586 @@ +// Copyright 2025 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 enginetest + +import ( + "github.com/dolthub/go-mysql-server/enginetest/queries" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" +) + +var DoltStashTests = []queries.ScriptTest{ + { + Name: "DOLT_STASH() subcommands error on empty space.", + Assertions: []queries.ScriptTestAssertion{ + { + Query: "CALL DOLT_STASH('push', 'myStash');", + ExpectedErrStr: "no local changes to save", + }, + { + Query: "CREATE TABLE test (i int)", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash');", + ExpectedErrStr: "no local changes to save", + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + ExpectedErrStr: "No stash entries found.", + }, + { + Query: "CALL DOLT_STASH('drop', 'myStash');", + ExpectedErrStr: "No stash entries found.", + }, + { + Query: "CALL DOLT_STASH('clear','myStash');", + Expected: []sql.Row{{0}}, + }, + }, + }, + { + Name: "Simple push and pop with DOLT_STASH()", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{{"myStash", "stash@{0}", "main", doltCommit, "Created table"}}, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STASHES", + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "Clearing stash removes all entries in stash list", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A','-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'stash1')", + "INSERT INTO test VALUES (2, 'b')", + "CALL DOLT_STASH('push', 'stash2')", + " INSERT INTO test VALUES (3, 'c')", + "CALL DOLT_STASH('push', 'stash2')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM dolt_stashes;", + Expected: []sql.Row{ + {"stash1", "stash@{0}", "main", doltCommit, "Created table"}, + {"stash2", "stash@{0}", "main", doltCommit, "Created table"}, + {"stash2", "stash@{1}", "main", doltCommit, "Created table"}, + }, + }, + { + Query: "CALL DOLT_STASH('clear', 'stash2');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM dolt_stashes;", + Expected: []sql.Row{ + {"stash1", "stash@{0}", "main", doltCommit, "Created table"}, + }, + }, + }, + }, + { + Name: "Clearing and stashing again", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "main", doltCommit, "Created table"}, + }, + }, + { + Query: "CALL DOLT_STASH('clear', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "INSERT INTO test VALUES (2, 'b');", + Expected: []sql.Row{{types.NewOkResult(1)}}, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "main", doltCommit, "Created table"}, + }, + }, + }, + }, + { + Name: "Popping specific stashes", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A','-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (2, 'b')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (3, 'c')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (4, 'd')", + "CALL DOLT_STASH('push', 'myStash')", + "CALL DOLT_STASH('pop', 'myStash', 'stash@{3}')", + "CALL DOLT_STASH('pop', 'myStash', 'stash@{1}')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM test", + Expected: []sql.Row{ + {1, "a"}, + {3, "c"}, + }, + }, + }, + }, + { + Name: "Stashing on different branches", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash')", + "CALL DOLT_CHECKOUT('-b', 'br1')", + "INSERT INTO test VALUES (2, 'b')", + "CALL DOLT_STASH('push', 'myStash')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "br1", doltCommit, "Created table"}, + {"myStash", "stash@{1}", "main", doltCommit, "Created table"}, + }, + }, + }, + }, + { + Name: "Popping stash onto different branch", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "CALL DOLT_BRANCH('br1')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_COMMIT('-A', '-m', 'Added a row')", + "INSERT INTO test VALUES (2, 'b')", + "CALL DOLT_STASH('push', 'myStash')", + "CALL DOLT_CHECKOUT('br1')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "main", doltCommit, "Added a row"}, + }, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM TEST;", + Expected: []sql.Row{ + {2, "b"}, + }, + }, + }, + }, + { + Name: "Can drop specific stash", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (2, 'b')", + "CALL DOLT_COMMIT('-a', '-m', 'Added 2 b')", + "INSERT INTO test VALUES (3, 'c')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (4, 'd')", + "CALL DOLT_COMMIT('-a','-m', 'Added 4 d')", + "INSERT INTO test VALUES (5, 'c')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "main", doltCommit, "Added 2 b"}, + {"myStash", "stash@{1}", "main", doltCommit, "Created table"}, + }, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "CALL DOLT_STASH('drop', 'myStash', 'stash@{1}');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "main", doltCommit, "Added 4 d"}, + {"myStash", "stash@{1}", "main", doltCommit, "Created table"}, + }, + }, + }, + }, + { + Name: "Can pop into dirty working set without conflict", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (2, 'b')", + "CALL DOLT_STASH('pop', 'myStash')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM test", + Expected: []sql.Row{ + {1, "a"}, + {2, "b"}, + }, + }, + }, + }, + { + Name: "Can't pop into dirty working set with conflict", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A','-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_STASH('push', 'myStash')", + "INSERT INTO test VALUES (1, 'b')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + ExpectedErrStr: "error: Your local changes to the following tables would be overwritten by applying stash 0:\n\t{'test'}\n" + + "Please commit your changes or stash them before you merge.\nAborting\n", + }, + { + Query: "SELECT * FROM DOLT_STASHES;", + Expected: []sql.Row{ + {"myStash", "stash@{0}", "main", doltCommit, "Created table"}, + }, + }, + { + Query: "SELECT * FROM test;", + Expected: []sql.Row{ + {1, "b"}, + }, + }, + }, + }, + { + Name: "Can stash modified staged and working set of changes", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "INSERT INTO test VALUES (1, 'a')", + "CALL DOLT_ADD('.')", + "INSERT INTO test VALUES (2, 'b')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{ + {"test", true, "modified"}, + {"test", false, "modified"}, + }, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM test;", + Expected: []sql.Row{}, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM dolt_status;", + Expected: []sql.Row{ + {"test", false, "modified"}, + }, + }, + }, + }, + { + Name: "Can use --include-untracked on push", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_ADD('.')", + "CREATE TABLE new(id int primary key)", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{ + {"test", true, "new table"}, + {"new", false, "new table"}, + }, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash', '--include-untracked');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{}, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM dolt_status;", + Expected: []sql.Row{ + {"test", true, "new table"}, + {"new", false, "new table"}, + }, + }, + }, + }, + { + Name: "Stash with tracked and untracked tables", + SetUpScript: []string{ + "CREATE TABLE new(i INT PRIMARY KEY)", + "CALL DOLT_ADD('.')", + "INSERT INTO new VALUES (1),(2)", + "CREATE TABLE test(id INT)", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{ + {"new", true, "new table"}, + {"test", false, "new table"}, + {"new", false, "modified"}, + }, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash')", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{ + {"test", false, "new table"}, + }, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{ + {"new", true, "new table"}, + {"test", false, "new table"}, + }, + }, + }, + }, + { + Name: "stashing working set with deleted table and popping it", + SetUpScript: []string{ + "CREATE TABLE new_tab(id INT PRIMARY KEY)", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "DROP TABLE new_tab", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * FROM DOLT_STATUS;", + Expected: []sql.Row{ + {"new_tab", false, "deleted"}, + }, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SHOW TABLES;", + Expected: []sql.Row{ + {"new_tab"}, + }, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SHOW TABLES;", + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "popping stash with deleted table that is deleted already on current head", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "CALL DOLT_BRANCH('branch1');", + "CALL DOLT_CHECKOUT('-b', 'branch2');", + "DROP TABLE test;", + "CALL DOLT_COMMIT('-A','-m','Dropped test');", + "CALL DOLT_CHECKOUT('branch1');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SHOW TABLES;", + Expected: []sql.Row{ + {"test"}, + }, + }, + { + Query: "DROP TABLE test;", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "CALL DOLT_CHECKOUT('branch2');", + Expected: []sql.Row{{0, "Switched to branch 'branch2'"}}, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "popping stash with deleted table that the same table exists on current head", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "CALL DOLT_BRANCH('branch1');", + "CALL DOLT_BRANCH('branch2');", + "CALL DOLT_CHECKOUT('branch1');", + "DROP TABLE test;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "CALL DOLT_CHECKOUT('branch2');", + Expected: []sql.Row{{0, "Switched to branch 'branch2'"}}, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{ + {"test", false, "deleted"}, + }, + }, + }, + }, + { + Name: "popping stash with deleted table that different table with same name on current head gives conflict", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "CALL DOLT_BRANCH('branch1')", + "CALL DOLT_BRANCH('branch2')", + "CALL DOLT_CHECKOUT('branch1')", + "DROP TABLE test", + "CALL DOLT_STASH('push', 'myStash')", + "CALL DOLT_CHECKOUT('branch2')", + "DROP TABLE test", + "CREATE TABLE test (id BIGINT PRIMARY KEY)", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + ExpectedErrStr: "merge aborted: schema conflict found for table test \n " + + "please resolve schema conflicts before merging: \n" + + "\ttable was modified in one branch and deleted in the other", + }, + }, + }, + { + Name: "popping stash with added table with PK on current head with the exact same table is added already", + SetUpScript: []string{ + "CREATE TABLE test(pk BIGINT PRIMARY KEY, v varchar(10))", + "CALL DOLT_COMMIT('-A', '-m', 'Created table')", + "CALL DOLT_BRANCH('branch1')", + "CALL DOLT_CHECKOUT('-b', 'branch2')", + "CREATE TABLE new_test(id INT PRIMARY KEY)", + "INSERT INTO new_test VALUES (1)", + "CALL DOLT_COMMIT('-A', '-m', 'Created new_test')", + "CALL DOLT_CHECKOUT('branch1')", + "CREATE TABLE new_test(id INT PRIMARY KEY)", + "INSERT INTO new_test VALUES (1)", + "CALL DOLT_ADD('.')", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "CALL DOLT_STASH('push', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "CALL DOLT_CHECKOUT('branch2');", + Expected: []sql.Row{{0, "Switched to branch 'branch2'"}}, + }, + { + Query: "CALL DOLT_STASH('pop', 'myStash');", + SkipResultsCheck: true, + }, + { + Query: "SELECT * FROM DOLT_STATUS", + Expected: []sql.Row{}, + }, + }, + }, +} diff --git a/integration-tests/bats/ls.bats b/integration-tests/bats/ls.bats index afcc60f620f..fe6d8a42332 100755 --- a/integration-tests/bats/ls.bats +++ b/integration-tests/bats/ls.bats @@ -60,7 +60,7 @@ teardown() { @test "ls: --system shows system tables" { run dolt ls --system [ "$status" -eq 0 ] - [ "${#lines[@]}" -eq 24 ] + [ "${#lines[@]}" -eq 25 ] [[ "$output" =~ "System tables:" ]] || false [[ "$output" =~ "dolt_status" ]] || false [[ "$output" =~ "dolt_commits" ]] || false @@ -85,6 +85,7 @@ teardown() { [[ "$output" =~ "dolt_commit_diff_table_two" ]] || false [[ "$output" =~ "dolt_workspace_table_one" ]] || false [[ "$output" =~ "dolt_workspace_table_two" ]] || false + [[ "$output" =~ "dolt_stashes" ]] || false } @test "ls: --all shows tables in working set and system tables" { diff --git a/integration-tests/bats/sql-stash.bats b/integration-tests/bats/sql-stash.bats new file mode 100644 index 00000000000..73067da1c54 --- /dev/null +++ b/integration-tests/bats/sql-stash.bats @@ -0,0 +1,33 @@ +#!/usr/bin/env bats +load $BATS_TEST_DIRNAME/helper/common.bash + +setup() { + setup_common +} + +teardown() { + assert_feature_version + teardown_common +} + +@test "sql-stash: push does not affect stash" { + TESTDIRS=$(pwd)/testdirs + mkdir -p $TESTDIRS/{rem1,repo1} + + cd $TESTDIRS/repo1 + dolt init + dolt remote add origin file://../rem1 + dolt remote add test-remote file://../rem1 + dolt push origin main + dolt sql -q "create table t1 (a int primary key, b int)" + dolt add . + dolt sql -q "call dolt_stash('push', 'stash1');" + dolt push origin main + + cd $TESTDIRS + dolt clone file://rem1 repo2 + cd repo2 + run dolt sql -q "select * from dolt_stashes" + [ "$status" -eq 0 ] + [ "${#lines[@]}" -eq 0 ] +} \ No newline at end of file