diff --git a/go/cmd/dolt/commands/merge.go b/go/cmd/dolt/commands/merge.go index 13e0ca3695..bba23d31ba 100644 --- a/go/cmd/dolt/commands/merge.go +++ b/go/cmd/dolt/commands/merge.go @@ -58,7 +58,7 @@ var fkWarningMessage = "Warning: This merge is being applied to tables that have type MergeCmd struct{} -// Name is returns the name of the Dolt cli command. This is what is used on the command line to invoke the command +// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command func (cmd MergeCmd) Name() string { return "merge" } diff --git a/go/cmd/dolt/commands/merge_base.go b/go/cmd/dolt/commands/merge_base.go new file mode 100644 index 0000000000..820604b961 --- /dev/null +++ b/go/cmd/dolt/commands/merge_base.go @@ -0,0 +1,96 @@ +// 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" + eventsapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi/v1alpha1" + "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 mergeBaseDocs = cli.CommandDocumentationContent{ + ShortDesc: `Find the common ancestor of two commits.`, + LongDesc: `Find the common ancestor of two commits, and return the ancestor's commit hash.'`, + Synopsis: []string{ + `{{.LessThan}}commit spec{{.GreaterThan}} {{.LessThan}}commit spec{{.GreaterThan}}`, + }, +} + +type MergeBaseCmd struct{} + +// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command +func (cmd MergeBaseCmd) Name() string { + return "merge-base" +} + +// Description returns a description of the command +func (cmd MergeBaseCmd) Description() string { + return mergeBaseDocs.ShortDesc +} + +// CreateMarkdown creates a markdown file containing the helptext for the command at the given path +func (cmd MergeBaseCmd) CreateMarkdown(fs filesys.Filesys, path, commandStr string) error { + ap := cmd.createArgParser() + return CreateMarkdown(fs, path, cli.GetCommandDocumentation(commandStr, mergeBaseDocs, ap)) +} + +func (cmd MergeBaseCmd) createArgParser() *argparser.ArgParser { + ap := argparser.NewArgParser() + //ap.ArgListHelp = append(ap.ArgListHelp, [2]string{"start-point", "A commit that a new branch should point at."}) + return ap +} + +// EventType returns the type of the event to log +func (cmd MergeBaseCmd) EventType() eventsapi.ClientEventType { + return eventsapi.ClientEventType_TYPE_UNSPECIFIED +} + +// Exec executes the command +func (cmd MergeBaseCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv) int { + ap := cmd.createArgParser() + help, usage := cli.HelpAndUsagePrinters(cli.GetCommandDocumentation(commandStr, mergeBaseDocs, ap)) + apr := cli.ParseArgsOrDie(ap, args, help) + + var verr errhand.VerboseError + if apr.NArg() != 2 { + verr = errhand.BuildDError("%s takes exactly 2 args", cmd.Name()).Build() + return HandleVErrAndExitCode(verr, usage) + } + + left, verr := ResolveCommitWithVErr(dEnv, apr.Arg(0)) + if verr != nil { + return HandleVErrAndExitCode(verr, usage) + } + + right, verr := ResolveCommitWithVErr(dEnv, apr.Arg(1)) + if verr != nil { + return HandleVErrAndExitCode(verr, usage) + } + + mergeBase, err := merge.MergeBase(ctx, left, right) + if err != nil { + verr = errhand.BuildDError("could not find merge-base for args %s", apr.Args()).AddCause(err).Build() + return HandleVErrAndExitCode(verr, usage) + } + + cli.Println(mergeBase.String()) + return 0 +} diff --git a/go/cmd/dolt/commands/utils.go b/go/cmd/dolt/commands/utils.go index 3659ac4dec..3f7e6a2ca4 100644 --- a/go/cmd/dolt/commands/utils.go +++ b/go/cmd/dolt/commands/utils.go @@ -84,7 +84,7 @@ func ResolveCommitWithVErr(dEnv *env.DoltEnv, cSpecStr string) (*doltdb.Commit, if err == doltdb.ErrInvalidAncestorSpec { return nil, errhand.BuildDError("'%s' could not resolve ancestor spec", cSpecStr).Build() } else if err == doltdb.ErrBranchNotFound { - return nil, errhand.BuildDError("unknown branch in commit spec: '%s'", cSpecStr).Build() + return nil, errhand.BuildDError("unknown ref in commit spec: '%s'", cSpecStr).Build() } else if doltdb.IsNotFoundErr(err) { return nil, errhand.BuildDError("'%s' not found", cSpecStr).Build() } else if err == doltdb.ErrFoundHashNotACommit { diff --git a/go/cmd/dolt/dolt.go b/go/cmd/dolt/dolt.go index 06f2819d79..cdd9f8c330 100644 --- a/go/cmd/dolt/dolt.go +++ b/go/cmd/dolt/dolt.go @@ -90,6 +90,7 @@ var doltCommand = cli.NewSubCommandHandler("dolt", "it's git for data", []cli.Co commands.GarbageCollectionCmd{}, commands.FilterBranchCmd{}, commands.VerifyConstraintsCmd{}, + commands.MergeBaseCmd{}, }) func init() { diff --git a/go/libraries/doltcore/merge/merge_base.go b/go/libraries/doltcore/merge/merge_base.go new file mode 100644 index 0000000000..30087bde92 --- /dev/null +++ b/go/libraries/doltcore/merge/merge_base.go @@ -0,0 +1,32 @@ +// 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" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + + "github.com/dolthub/dolt/go/store/hash" +) + +func MergeBase(ctx context.Context, left, right *doltdb.Commit) (base hash.Hash, err error) { + ancestor, err := doltdb.GetCommitAncestor(ctx, left, right) + if err != nil { + return base, err + } + + return ancestor.HashOf() +} diff --git a/go/libraries/doltcore/sqle/dfunctions/dolt_merge_base.go b/go/libraries/doltcore/sqle/dfunctions/dolt_merge_base.go new file mode 100644 index 0000000000..dc4fbb9a45 --- /dev/null +++ b/go/libraries/doltcore/sqle/dfunctions/dolt_merge_base.go @@ -0,0 +1,124 @@ +// 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/sqle" +) + +const DoltMergeBaseFuncName = "dolt_merge_base" + +type MergeBase struct { + expression.BinaryExpression +} + +// NewMergeBase returns a MergeBase sql function. +func NewMergeBase(left, right sql.Expression) sql.Expression { + return &MergeBase{expression.BinaryExpression{Left: left, Right: right}} +} + +// Eval implements the sql.Expression interface. +func (d MergeBase) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { + if _, ok := d.Left.Type().(sql.StringType); !ok { + return nil, sql.ErrInvalidType.New(d.Left.Type()) + } + if _, ok := d.Right.Type().(sql.StringType); !ok { + return nil, sql.ErrInvalidType.New(d.Right.Type()) + } + + leftSpec, err := d.Left.Eval(ctx, row) + if err != nil { + return nil, err + } + rightSpec, err := d.Right.Eval(ctx, row) + if err != nil { + return nil, err + } + + if leftSpec == nil || rightSpec == nil { + return nil, nil + } + + left, right, err := resolveRefSpecs(ctx, leftSpec.(string), rightSpec.(string)) + if err != nil { + return nil, err + } + + mergeBase, err := merge.MergeBase(ctx, left, right) + if err != nil { + return nil, err + } + + return mergeBase.String(), nil +} + +func resolveRefSpecs(ctx *sql.Context, leftSpec, rightSpec string) (left, right *doltdb.Commit, err error) { + lcs, err := doltdb.NewCommitSpec(leftSpec) + if err != nil { + return nil, nil, err + } + rcs, err := doltdb.NewCommitSpec(rightSpec) + if err != nil { + return nil, nil, err + } + + sess := sqle.DSessFromSess(ctx.Session) + dbName := ctx.GetCurrentDatabase() + + dbData, ok := sess.GetDbData(dbName) + if !ok { + return nil, nil, sql.ErrDatabaseNotFound.New(dbName) + } + doltDB, ok := sess.GetDoltDB(dbName) + if !ok { + return nil, nil, sql.ErrDatabaseNotFound.New(dbName) + } + + left, err = doltDB.Resolve(ctx, lcs, dbData.Rsr.CWBHeadRef()) + if err != nil { + return nil, nil, err + } + right, err = doltDB.Resolve(ctx, rcs, dbData.Rsr.CWBHeadRef()) + if err != nil { + return nil, nil, err + } + + return +} + +// String implements the sql.Expression interface. +func (d MergeBase) String() string { + return fmt.Sprintf("DOLT_MERGE_BASE(%s,%s)", d.Left.String(), d.Right.String()) +} + +// Type implements the sql.Expression interface. +func (d MergeBase) Type() sql.Type { + return sql.Text +} + +// WithChildren implements the sql.Expression interface. +func (d MergeBase) WithChildren(children ...sql.Expression) (sql.Expression, error) { + if len(children) != 2 { + return nil, sql.ErrInvalidChildrenNumber.New(d, len(children), 2) + } + return NewMergeBase(children[0], children[1]), nil +} diff --git a/go/libraries/doltcore/sqle/dfunctions/init.go b/go/libraries/doltcore/sqle/dfunctions/init.go index 8387283f5e..95a8b47052 100644 --- a/go/libraries/doltcore/sqle/dfunctions/init.go +++ b/go/libraries/doltcore/sqle/dfunctions/init.go @@ -29,6 +29,7 @@ var DoltFunctions = []sql.Function{ sql.FunctionN{Name: DoltCheckoutFuncName, Fn: NewDoltCheckoutFunc}, sql.FunctionN{Name: DoltMergeFuncName, Fn: NewDoltMergeFunc}, sql.Function0{Name: ActiveBranchFuncName, Fn: NewActiveBranchFunc}, + sql.Function2{Name: DoltMergeBaseFuncName, Fn: NewMergeBase}, } // These are the DoltFunctions that get exposed to Dolthub Api. diff --git a/integration-tests/bats/conflict-detection.bats b/integration-tests/bats/conflict-detection.bats index 774f90eef8..dce41c876d 100644 --- a/integration-tests/bats/conflict-detection.bats +++ b/integration-tests/bats/conflict-detection.bats @@ -12,7 +12,7 @@ teardown() { @test "conflict-detection: merge non-existant branch errors" { run dolt merge batmans-parents [ $status -eq 1 ] - [[ "$output" =~ "unknown branch" ]] || false + [[ "$output" =~ "unknown ref" ]] || false [[ ! "$output" =~ "panic" ]] || false } diff --git a/integration-tests/bats/merge-base.bats b/integration-tests/bats/merge-base.bats new file mode 100644 index 0000000000..f07ce7630f --- /dev/null +++ b/integration-tests/bats/merge-base.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats +load $BATS_TEST_DIRNAME/helper/common.bash + +setup() { + setup_common + + dolt sql -q "CREATE TABLE test (pk int primary key);" + dolt add -A && dolt commit -m "commit A" + dolt branch zero + + dolt sql -q "INSERT INTO test VALUES (0);" + dolt commit -am "commit B" + dolt branch one + dolt branch two + + dolt sql -q "INSERT INTO test VALUES (1);" + dolt commit -am "commit C" + + dolt checkout two + dolt sql -q "INSERT INTO test VALUES (2);" + dolt commit -am "commit D" + dolt checkout master + + # # # # # # # # # # # # # # # # # # # # # # # + # # + # <-- (zero) # + # / # + # / <-- (one) # + # / / # + # (init) -- (A) -- (B) -- (C) <-- (master) # + # \ # + # -- (D) <-- (two) # + # # + # # # # # # # # # # # # # # # # # # # # # # # +} + +teardown() { + teardown_common +} + +@test "merge-base: cli" { + run dolt merge-base master two + [ "$status" -eq 0 ] + MERGE_BASE="$output" + + run dolt merge-base master one + [ "$status" -eq 0 ] + [ "$output" = "$MERGE_BASE" ] + + run dolt merge-base one two + [ "$status" -eq 0 ] + [ "$output" = "$MERGE_BASE" ] + + dolt checkout master + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "$MERGE_BASE" ]] || false + + dolt checkout two + run dolt log + [ "$status" -eq 0 ] + [[ "$output" =~ "$MERGE_BASE" ]] || false + + dolt checkout zero + run dolt log + [ "$status" -eq 0 ] + [[ ! "$output" =~ "$MERGE_BASE" ]] || false +} + +@test "merge-base: sql" { + run dolt sql -q "SELECT message FROM dolt_log WHERE commit_hash = dolt_merge_base('master', 'zero');" -r csv + [ "$status" -eq 0 ] + [ "${lines[1]}" = "commit A" ] + + run dolt sql -q "SELECT message FROM dolt_log WHERE commit_hash = dolt_merge_base('master', 'one');" -r csv + [ "$status" -eq 0 ] + [ "${lines[1]}" = "commit B" ] + + run dolt sql -q "SELECT message FROM dolt_log WHERE commit_hash = dolt_merge_base('master', 'two');" -r csv + [ "$status" -eq 0 ] + [ "${lines[1]}" = "commit B" ] + + run dolt sql -q "SELECT message FROM dolt_log WHERE commit_hash = dolt_merge_base('master', 'master');" -r csv + [ "$status" -eq 0 ] + [ "${lines[1]}" = "commit C" ] + + # dolt_merge_base() resolves commit hashes + run dolt sql -q "SELECT dolt_merge_base('master', hashof('one')) = dolt_merge_base(hashof('master'),'one') FROM dual;" -r csv + [ "$status" -eq 0 ] + [ "${lines[1]}" = "true" ] +} diff --git a/integration-tests/bats/no-repo.bats b/integration-tests/bats/no-repo.bats index b1fb905816..780df516c9 100755 --- a/integration-tests/bats/no-repo.bats +++ b/integration-tests/bats/no-repo.bats @@ -53,6 +53,7 @@ teardown() { [[ "$output" =~ "migrate - Executes a repository migration to update to the latest format." ]] || false [[ "$output" =~ "gc - Cleans up unreferenced data from the repository." ]] || false [[ "$output" =~ "filter-branch - Edits the commit history using the provided query." ]] || false + [[ "$output" =~ "merge-base - Find the common ancestor of two commits." ]] || false } @test "no-repo: check all commands for valid help text" {