diff --git a/go/cmd/dolt/cli/command.go b/go/cmd/dolt/cli/command.go index ea5edca57a..61594c8d90 100644 --- a/go/cmd/dolt/cli/command.go +++ b/go/cmd/dolt/cli/command.go @@ -100,17 +100,27 @@ type HiddenCommand interface { type SubCommandHandler struct { name string description string + // Unspecified ONLY applies when no other command has been given. This is different from how a default command would + // function, as a command that doesn't exist for this sub handler will result in an error. + Unspecified Command Subcommands []Command hidden bool } // NewSubCommandHandler returns a new SubCommandHandler instance func NewSubCommandHandler(name, description string, subcommands []Command) SubCommandHandler { - return SubCommandHandler{name, description, subcommands, false} + return SubCommandHandler{name, description, nil, subcommands, false} } +// NewHiddenSubCommandHandler returns a new SubCommandHandler instance that is hidden from display func NewHiddenSubCommandHandler(name, description string, subcommands []Command) SubCommandHandler { - return SubCommandHandler{name, description, subcommands, true} + return SubCommandHandler{name, description, nil, subcommands, true} +} + +// NewSubCommandHandlerWithUnspecified returns a new SubCommandHandler that will invoke the unspecified command ONLY if +// no direct command is given. +func NewSubCommandHandlerWithUnspecified(name, description string, hidden bool, unspecified Command, subcommands []Command) SubCommandHandler { + return SubCommandHandler{name, description, unspecified, subcommands, hidden} } func (hc SubCommandHandler) Name() string { @@ -134,43 +144,24 @@ func (hc SubCommandHandler) Hidden() bool { } func (hc SubCommandHandler) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv) int { - if len(args) < 1 { + if len(args) < 1 && hc.Unspecified == nil { hc.printUsage(commandStr) return 1 } - subCommandStr := strings.ToLower(strings.TrimSpace(args[0])) + var subCommandStr string + if len(args) > 0 { + subCommandStr = strings.ToLower(strings.TrimSpace(args[0])) + } for _, cmd := range hc.Subcommands { lwrName := strings.ToLower(cmd.Name()) - if lwrName == subCommandStr { - cmdRequiresRepo := true - if rnrCmd, ok := cmd.(RepoNotRequiredCommand); ok { - cmdRequiresRepo = rnrCmd.RequiresRepo() - } - - if cmdRequiresRepo && !hasHelpFlag(args) { - isValid := CheckEnvIsValid(dEnv) - if !isValid { - return 2 - } - } - - var evt *events.Event - if evtCmd, ok := cmd.(EventMonitoredCommand); ok { - evt = events.NewEvent(evtCmd.EventType()) - ctx = events.NewContextForEvent(ctx, evt) - } - - ret := cmd.Exec(ctx, commandStr+" "+subCommandStr, args[1:], dEnv) - - if evt != nil { - events.GlobalCollector.CloseEventAndAdd(evt) - } - - return ret + return hc.handleCommand(ctx, commandStr+" "+subCommandStr, cmd, args[1:], dEnv) } } + if hc.Unspecified != nil { + return hc.handleCommand(ctx, commandStr, hc.Unspecified, args, dEnv) + } if !isHelp(subCommandStr) { PrintErrln(color.RedString("Unknown Command " + subCommandStr)) @@ -180,6 +171,34 @@ func (hc SubCommandHandler) Exec(ctx context.Context, commandStr string, args [] return 1 } +func (hc SubCommandHandler) handleCommand(ctx context.Context, commandStr string, cmd Command, args []string, dEnv *env.DoltEnv) int { + cmdRequiresRepo := true + if rnrCmd, ok := cmd.(RepoNotRequiredCommand); ok { + cmdRequiresRepo = rnrCmd.RequiresRepo() + } + + if cmdRequiresRepo && !hasHelpFlag(args) { + isValid := CheckEnvIsValid(dEnv) + if !isValid { + return 2 + } + } + + var evt *events.Event + if evtCmd, ok := cmd.(EventMonitoredCommand); ok { + evt = events.NewEvent(evtCmd.EventType()) + ctx = events.NewContextForEvent(ctx, evt) + } + + ret := cmd.Exec(ctx, commandStr, args, dEnv) + + if evt != nil { + events.GlobalCollector.CloseEventAndAdd(evt) + } + + return ret +} + // CheckEnvIsValid validates that a DoltEnv has been initialized properly and no errors occur during load, and prints // error messages to the user if there are issues with the environment or if errors were encountered while loading it. func CheckEnvIsValid(dEnv *env.DoltEnv) bool { diff --git a/go/cmd/dolt/commands/revert.go b/go/cmd/dolt/commands/revert.go new file mode 100644 index 0000000000..95d5fb8eaf --- /dev/null +++ b/go/cmd/dolt/commands/revert.go @@ -0,0 +1,139 @@ +// Copyright 2021 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "context" + + "github.com/dolthub/dolt/go/cmd/dolt/cli" + "github.com/dolthub/dolt/go/cmd/dolt/errhand" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/env" + "github.com/dolthub/dolt/go/libraries/doltcore/merge" + "github.com/dolthub/dolt/go/libraries/utils/argparser" + "github.com/dolthub/dolt/go/libraries/utils/filesys" +) + +var revertDocs = cli.CommandDocumentationContent{ + ShortDesc: "Undo the changes introduced in a commit", + LongDesc: `Removes the changes made in a commit (or series of commits) from the working set, and then automatically commits the +result. This is done by way of a three-way merge. Given a specific commit (e.g. HEAD~1), this is similar to applying the +patch from HEAD~1..HEAD~2, giving us a patch of what to remove to effectively remove the influence of the specified +commit. If multiple commits are specified, then this process is repeated for each commit in the order specified. This +requires a clean working set. + +For now, any conflicts or constraint violations that are brought by the merge cause the command to fail.`, + Synopsis: []string{ + "...", + }, +} + +type RevertCmd struct{} + +var _ cli.Command = RevertCmd{} + +// Name implements the interface cli.Command. +func (cmd RevertCmd) Name() string { + return "revert" +} + +// Description implements the interface cli.Command. +func (cmd RevertCmd) Description() string { + return "Undo the changes introduced in a commit." +} + +// CreateMarkdown implements the interface cli.Command. +func (cmd RevertCmd) CreateMarkdown(fs filesys.Filesys, path, commandStr string) error { + ap := argparser.NewArgParser() + return CreateMarkdown(fs, path, cli.GetCommandDocumentation(commandStr, revertDocs, ap)) +} + +// createArgParser creates the argument parser for this command. +func (cmd RevertCmd) createArgParser() *argparser.ArgParser { + ap := argparser.NewArgParser() + ap.ArgListHelp = append(ap.ArgListHelp, [2]string{"revision", + "The commit revisions. If multiple revisions are given, they're applied in the order given."}) + return ap +} + +// Exec implements the interface cli.Command. +func (cmd RevertCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv) int { + ap := cmd.createArgParser() + help, usage := cli.HelpAndUsagePrinters(cli.GetCommandDocumentation(commandStr, commitDocs, ap)) + apr := cli.ParseArgsOrDie(ap, args, help) + + if apr.NArg() < 1 { + usage() + return 1 + } + headRoot, err := dEnv.HeadRoot(ctx) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + workingRoot, err := dEnv.WorkingRoot(ctx) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + headHash, err := headRoot.HashOf() + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + workingHash, err := workingRoot.HashOf() + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if !headHash.Equal(workingHash) { + cli.PrintErrln("You must commit any changes before using revert.") + return 1 + } + + headRef := dEnv.RepoState.CWBHeadRef() + commits := make([]*doltdb.Commit, apr.NArg()) + for i, arg := range apr.Args() { + commitSpec, err := doltdb.NewCommitSpec(arg) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + commit, err := dEnv.DoltDB.Resolve(ctx, commitSpec, headRef) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + commits[i] = commit + } + + workingRoot, revertMessage, err := merge.Revert(ctx, dEnv.DoltDB, workingRoot, commits) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + + workingHash, err = workingRoot.HashOf() + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if headHash.Equal(workingHash) { + cli.Println("No changes were made.") + return 0 + } + + err = dEnv.UpdateWorkingRoot(ctx, workingRoot) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + res := AddCmd{}.Exec(ctx, "add", []string{"-A"}, dEnv) + if res != 0 { + return res + } + return CommitCmd{}.Exec(ctx, "commit", []string{"-m", revertMessage}, dEnv) +} diff --git a/go/cmd/dolt/dolt.go b/go/cmd/dolt/dolt.go index 4c102af6ef..ab361afa11 100644 --- a/go/cmd/dolt/dolt.go +++ b/go/cmd/dolt/dolt.go @@ -76,6 +76,7 @@ var doltCommand = cli.NewSubCommandHandler("dolt", "it's git for data", []cli.Co commands.PullCmd{}, commands.FetchCmd{}, commands.CloneCmd{}, + commands.RevertCmd{}, credcmds.Commands, commands.LoginCmd{}, commands.VersionCmd{VersionStr: Version}, @@ -320,6 +321,7 @@ func commandNeedsMigrationCheck(args []string) bool { for _, cmd := range []cli.Command{ commands.ResetCmd{}, commands.CommitCmd{}, + commands.RevertCmd{}, commands.SqlCmd{}, sqlserver.SqlServerCmd{}, sqlserver.SqlClientCmd{}, diff --git a/go/libraries/doltcore/merge/revert.go b/go/libraries/doltcore/merge/revert.go new file mode 100644 index 0000000000..066f0aece6 --- /dev/null +++ b/go/libraries/doltcore/merge/revert.go @@ -0,0 +1,85 @@ +// Copyright 2021 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 merge + +import ( + "context" + "fmt" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" +) + +// Revert is a convenience function for a three-way merge. In particular, given some root and a collection of commits +// that are all parents of the root value, this applies a three-way merge with the following characteristics (assuming +// a commit is HEAD~1): +// +// Base: HEAD~1 +// Ours: root +// Theirs: HEAD~2 +// +// The root is updated with the merged result, and this process is repeated for each commit given, in the order given. +// Currently, we error on conflicts or constraint violations generated by the merge. +func Revert(ctx context.Context, ddb *doltdb.DoltDB, root *doltdb.RootValue, commits []*doltdb.Commit) (*doltdb.RootValue, string, error) { + revertMessage := "Revert" + + for i, baseCommit := range commits { + if i > 0 { + revertMessage += " and" + } + baseRoot, err := baseCommit.GetRootValue() + if err != nil { + return nil, "", err + } + baseMeta, err := baseCommit.GetCommitMeta() + if err != nil { + return nil, "", err + } + revertMessage = fmt.Sprintf(`%s "%s"`, revertMessage, baseMeta.Description) + + var theirRoot *doltdb.RootValue + if len(baseCommit.ParentRefs()) > 0 { + parentCM, err := ddb.ResolveParent(ctx, baseCommit, 0) + if err != nil { + return nil, "", err + } + theirRoot, err = parentCM.GetRootValue() + if err != nil { + return nil, "", err + } + } else { + theirRoot, err = doltdb.EmptyRootValue(ctx, ddb.ValueReadWriter()) + if err != nil { + return nil, "", err + } + } + + root, _, err = MergeRoots(ctx, root, theirRoot, baseRoot) + if err != nil { + return nil, "", err + } + if ok, err := root.HasConflicts(ctx); err != nil { + return nil, "", err + } else if ok { + return nil, "", fmt.Errorf("revert currently does not handle conflicts") + } + if ok, err := root.HasConstraintViolations(ctx); err != nil { + return nil, "", err + } else if ok { + return nil, "", fmt.Errorf("revert currently does not handle constraint violations") + } + } + + return root, revertMessage, nil +} diff --git a/go/libraries/doltcore/sqle/dfunctions/init.go b/go/libraries/doltcore/sqle/dfunctions/init.go index fd5b75bd09..18f3384781 100644 --- a/go/libraries/doltcore/sqle/dfunctions/init.go +++ b/go/libraries/doltcore/sqle/dfunctions/init.go @@ -32,6 +32,7 @@ var DoltFunctions = []sql.Function{ sql.Function2{Name: DoltMergeBaseFuncName, Fn: NewMergeBase}, sql.FunctionN{Name: ConstraintsVerifyFuncName, Fn: NewConstraintsVerifyFunc}, sql.FunctionN{Name: ConstraintsVerifyAllFuncName, Fn: NewConstraintsVerifyAllFunc}, + sql.FunctionN{Name: RevertFuncName, Fn: NewRevertFunc}, } // These are the DoltFunctions that get exposed to Dolthub Api. diff --git a/go/libraries/doltcore/sqle/dfunctions/revert.go b/go/libraries/doltcore/sqle/dfunctions/revert.go new file mode 100644 index 0000000000..d87a4a4ead --- /dev/null +++ b/go/libraries/doltcore/sqle/dfunctions/revert.go @@ -0,0 +1,166 @@ +// Copyright 2021 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 dfunctions + +import ( + "fmt" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/merge" + "github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" +) + +const ( + RevertFuncName = "dolt_revert" +) + +// RevertFunc represents the dolt function "dolt revert". +type RevertFunc struct { + expression.NaryExpression +} + +var _ sql.Expression = (*RevertFunc)(nil) + +// NewRevertFunc creates a new RevertFunc expression that reverts commits. +func NewRevertFunc(ctx *sql.Context, args ...sql.Expression) (sql.Expression, error) { + return &RevertFunc{expression.NaryExpression{ChildExpressions: args}}, nil +} + +// Eval implements the Expression interface. +func (r *RevertFunc) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { + dbName := ctx.GetCurrentDatabase() + dSess := dsess.DSessFromSess(ctx.Session) + ddb, ok := dSess.GetDoltDB(ctx, dbName) + if !ok { + return nil, fmt.Errorf("dolt database could not be found") + } + workingSet, err := dSess.WorkingSet(ctx, dbName) + if err != nil { + return nil, err + } + workingRoot := workingSet.WorkingRoot() + headCommit, err := dSess.GetHeadCommit(ctx, dbName) + if err != nil { + return nil, err + } + headRoot, err := headCommit.GetRootValue() + if err != nil { + return nil, err + } + headHash, err := headRoot.HashOf() + if err != nil { + return nil, err + } + workingHash, err := workingRoot.HashOf() + if err != nil { + return nil, err + } + if !headHash.Equal(workingHash) { + return nil, fmt.Errorf("you must commit any changes before using revert") + } + + headRef, err := dSess.CWBHeadRef(ctx, dbName) + if err != nil { + return nil, err + } + commits := make([]*doltdb.Commit, len(r.ChildExpressions)) + for i, expr := range r.ChildExpressions { + res, err := expr.Eval(ctx, row) + if err != nil { + return nil, err + } + revisionStr, ok := res.(string) + if !ok { + return nil, sql.ErrUnexpectedType.New(i, fmt.Sprintf("%T", res)) + } + commitSpec, err := doltdb.NewCommitSpec(revisionStr) + if err != nil { + return nil, err + } + commit, err := ddb.Resolve(ctx, commitSpec, headRef) + if err != nil { + return nil, err + } + commits[i] = commit + } + + workingRoot, revertMessage, err := merge.Revert(ctx, ddb, workingRoot, commits) + if err != nil { + return nil, err + } + workingHash, err = workingRoot.HashOf() + if err != nil { + return nil, err + } + if !headHash.Equal(workingHash) { + err = dSess.SetRoot(ctx, dbName, workingRoot) + if err != nil { + return nil, err + } + stringType := typeinfo.StringDefaultType.ToSqlType() + commitFunc, err := NewDoltCommitFunc(ctx, + expression.NewLiteral("-a", stringType), expression.NewLiteral("-m", stringType), expression.NewLiteral(revertMessage, stringType)) + if err != nil { + return nil, err + } + _, err = commitFunc.Eval(ctx, row) + if err != nil { + return nil, err + } + } + return 0, nil +} + +// String implements the Stringer interface. +func (r *RevertFunc) String() string { + return fmt.Sprint("DOLT_REVERT()") +} + +// IsNullable implements the Expression interface. +func (r *RevertFunc) IsNullable() bool { + return false +} + +// Resolved implements the Expression interface. +func (r *RevertFunc) Resolved() bool { + for _, expr := range r.ChildExpressions { + if !expr.Resolved() { + return false + } + } + return true +} + +func (r *RevertFunc) Type() sql.Type { + return sql.Int8 +} + +// Children implements the Expression interface. +func (r *RevertFunc) Children() []sql.Expression { + exprs := make([]sql.Expression, len(r.ChildExpressions)) + for i := range exprs { + exprs[i] = r.ChildExpressions[i] + } + return exprs +} + +// WithChildren implements the Expression interface. +func (r *RevertFunc) WithChildren(ctx *sql.Context, children ...sql.Expression) (sql.Expression, error) { + return NewRevertFunc(ctx, children...) +} diff --git a/integration-tests/bats/revert.bats b/integration-tests/bats/revert.bats new file mode 100644 index 0000000000..d1852eac77 --- /dev/null +++ b/integration-tests/bats/revert.bats @@ -0,0 +1,200 @@ +#!/usr/bin/env bats +load $BATS_TEST_DIRNAME/helper/common.bash + +setup() { + setup_common + dolt sql -q "CREATE TABLE test(pk BIGINT PRIMARY KEY, v1 BIGINT)" + dolt add -A + dolt commit -m "Created table" + dolt sql -q "INSERT INTO test VALUES (1, 1)" + dolt add -A + dolt commit -m "Inserted 1" + dolt sql -q "INSERT INTO test VALUES (2, 2)" + dolt add -A + dolt commit -m "Inserted 2" + dolt sql -q "INSERT INTO test VALUES (3, 3)" + dolt add -A + dolt commit -m "Inserted 3" +} + +teardown() { + assert_feature_version + teardown_common +} + +@test "revert: HEAD" { + dolt revert HEAD + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "$output" =~ "2,2" ]] || false + [[ "${#lines[@]}" = "3" ]] || false +} + +@test "revert: HEAD~1" { + dolt revert HEAD~1 + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "$output" =~ "3,3" ]] || false + [[ "${#lines[@]}" = "3" ]] || false +} + +@test "revert: HEAD & HEAD~1" { + dolt revert HEAD HEAD~1 + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "${#lines[@]}" = "2" ]] || false +} + +@test "revert: has changes in the working set" { + dolt sql -q "INSERT INTO test VALUES (4, 4)" + run dolt revert HEAD + [ "$status" -eq "1" ] + [[ "$output" =~ "changes" ]] || false +} + +@test "revert: conflicts" { + dolt sql -q "INSERT INTO test VALUES (4, 4)" + dolt add -A + dolt commit -m "Inserted 4" + dolt sql -q "REPLACE INTO test VALUES (4, 5)" + dolt add -A + dolt commit -m "Updated 4" + run dolt revert HEAD~1 + [ "$status" -eq "1" ] + [[ "$output" =~ "conflict" ]] || false +} + +@test "revert: constraint violations" { + dolt sql <<"SQL" +CREATE TABLE parent (pk BIGINT PRIMARY KEY, v1 BIGINT, INDEX(v1)); +CREATE TABLE child (pk BIGINT PRIMARY KEY, v1 BIGINT, CONSTRAINT fk_name FOREIGN KEY (v1) REFERENCES parent (v1)); +INSERT INTO parent VALUES (10, 1), (20, 2); +INSERT INTO child VALUES (1, 1), (2, 2); +SQL + dolt add -A + dolt commit -m "MC1" + dolt sql -q "DELETE FROM child WHERE pk = 2" + dolt add -A + dolt commit -m "MC2" + dolt sql -q "DELETE FROM parent WHERE pk = 20" + dolt add -A + dolt commit -m "MC3" + run dolt revert HEAD~1 + [ "$status" -eq "1" ] + [[ "$output" =~ "constraint violation" ]] || false +} + +@test "revert: too far back" { + run dolt revert HEAD~10 + [ "$status" -eq "1" ] + [[ "$output" =~ "ancestor" ]] || false +} + +@test "revert: no changes" { + run dolt revert HEAD~4 + [ "$status" -eq "0" ] + [[ "$output" =~ "No changes were made" ]] || false +} + +@test "revert: invalid hash" { + run dolt revert aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [ "$status" -eq "1" ] + [[ "$output" =~ "hash" ]] || false +} + +@test "revert: SQL HEAD" { + dolt sql -q "SELECT DOLT_REVERT('HEAD')" + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "$output" =~ "2,2" ]] || false + [[ "${#lines[@]}" = "3" ]] || false +} + +@test "revert: SQL HEAD~1" { + dolt sql -q "SELECT DOLT_REVERT('HEAD~1')" + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "$output" =~ "3,3" ]] || false + [[ "${#lines[@]}" = "3" ]] || false +} + +@test "revert: SQL HEAD & HEAD~1" { + dolt sql -q "SELECT DOLT_REVERT('HEAD', 'HEAD~1')" + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "${#lines[@]}" = "2" ]] || false +} + +@test "revert: SQL has changes in the working set" { + dolt sql -q "INSERT INTO test VALUES (4, 4)" + run dolt sql -q "SELECT DOLT_REVERT('HEAD')" + [ "$status" -eq "1" ] + [[ "$output" =~ "changes" ]] || false +} + +@test "revert: SQL conflicts" { + dolt sql -q "INSERT INTO test VALUES (4, 4)" + dolt add -A + dolt commit -m "Inserted 4" + dolt sql -q "REPLACE INTO test VALUES (4, 5)" + dolt add -A + dolt commit -m "Updated 4" + run dolt sql -q "SELECT DOLT_REVERT('HEAD~1')" + [ "$status" -eq "1" ] + [[ "$output" =~ "conflict" ]] || false +} + +@test "revert: SQL constraint violations" { + dolt sql <<"SQL" +CREATE TABLE parent (pk BIGINT PRIMARY KEY, v1 BIGINT, INDEX(v1)); +CREATE TABLE child (pk BIGINT PRIMARY KEY, v1 BIGINT, CONSTRAINT fk_name FOREIGN KEY (v1) REFERENCES parent (v1)); +INSERT INTO parent VALUES (10, 1), (20, 2); +INSERT INTO child VALUES (1, 1), (2, 2); +SQL + dolt add -A + dolt commit -m "MC1" + dolt sql -q "DELETE FROM child WHERE pk = 2" + dolt add -A + dolt commit -m "MC2" + dolt sql -q "DELETE FROM parent WHERE pk = 20" + dolt add -A + dolt commit -m "MC3" + run dolt sql -q "SELECT DOLT_REVERT('HEAD~1')" + [ "$status" -eq "1" ] + [[ "$output" =~ "constraint violation" ]] || false +} + +@test "revert: SQL too far back" { + run dolt sql -q "SELECT DOLT_REVERT('HEAD~10')" + [ "$status" -eq "1" ] + [[ "$output" =~ "ancestor" ]] || false +} + +@test "revert: SQL no changes" { + dolt sql -q "SELECT DOLT_REVERT('HEAD~4')" + run dolt sql -q "SELECT * FROM test" -r=csv + [ "$status" -eq "0" ] + [[ "$output" =~ "pk,v1" ]] || false + [[ "$output" =~ "1,1" ]] || false + [[ "$output" =~ "2,2" ]] || false + [[ "$output" =~ "3,3" ]] || false + [[ "${#lines[@]}" = "4" ]] || false +} + +@test "revert: SQL invalid hash" { + run dolt sql -q "SELECT DOLT_REVERT('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')" + [ "$status" -eq "1" ] + [[ "$output" =~ "hash" ]] || false +}