diff --git a/go/cmd/dolt/commands/log.go b/go/cmd/dolt/commands/log.go index b925cb8045..536c9290ec 100644 --- a/go/cmd/dolt/commands/log.go +++ b/go/cmd/dolt/commands/log.go @@ -23,6 +23,7 @@ import ( "github.com/fatih/color" "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/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/env" @@ -41,14 +42,18 @@ const ( parentsParam = "parents" decorateParam = "decorate" oneLineParam = "oneline" + notParam = "not" ) type logOpts struct { - numLines int - showParents bool - minParents int - decoration string - oneLine bool + numLines int + showParents bool + minParents int + decoration string + oneLine bool + excludingCommitSpec *doltdb.CommitSpec + commitSpec *doltdb.CommitSpec + tableName string } type logNode struct { @@ -63,9 +68,23 @@ var logDocs = cli.CommandDocumentationContent{ ShortDesc: `Show commit logs`, LongDesc: `Shows the commit logs -The command takes options to control what is shown and how.`, +The command takes options to control what is shown and how. + +{{.EmphasisLeft}}dolt log{{.EmphasisRight}} + Lists commit logs from current HEAD when no options provided. + +{{.EmphasisLeft}}dolt log {{.EmphasisRight}} + Lists commit logs starting from revision. + +{{.EmphasisLeft}}dolt log {{.EmphasisRight}} + Lists commit logs starting from revision, only including commits with changes to table. + +{{.EmphasisLeft}}dolt log ..{{.EmphasisRight}} +{{.EmphasisLeft}}dolt log --not {{.EmphasisRight}} +{{.EmphasisLeft}}dolt log ^ {{.EmphasisRight}} + Different ways to list two dot logs. These will list commit logs for revisionA, while excluding commits from revisionB. The table option is not supported for two dot log.`, Synopsis: []string{ - `[-n {{.LessThan}}num_commits{{.GreaterThan}}] [{{.LessThan}}commit{{.GreaterThan}}] [[--] {{.LessThan}}table{{.GreaterThan}}]`, + `[-n {{.LessThan}}num_commits{{.GreaterThan}}] [{{.LessThan}}revision-range{{.GreaterThan}}] [[--] {{.LessThan}}table{{.GreaterThan}}]`, }, } @@ -99,6 +118,7 @@ func (cmd LogCmd) ArgParser() *argparser.ArgParser { ap.SupportsFlag(parentsParam, "", "Shows all parents of each commit in the log.") ap.SupportsString(decorateParam, "", "decorate_fmt", "Shows refs next to commits. Valid options are short, full, no, and auto") ap.SupportsFlag(oneLineParam, "", "Shows logs in a compact format.") + ap.SupportsString(notParam, "", "revision", "Excludes commits from revision.") return ap } @@ -117,6 +137,20 @@ func (cmd LogCmd) logWithLoggerFunc(ctx context.Context, commandStr string, args return 1 } + opts, err := parseLogArgs(ctx, dEnv, apr) + if err != nil { + return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage) + } + if opts.commitSpec == nil { + opts.commitSpec = dEnv.RepoStateReader().CWBHeadSpec() + } + if len(opts.tableName) > 0 { + return handleErrAndExit(logTableCommits(ctx, dEnv, opts)) + } + return logCommits(ctx, dEnv, opts) +} + +func parseLogArgs(ctx context.Context, dEnv *env.DoltEnv, apr *argparser.ArgParseResults) (*logOpts, error) { minParents := apr.GetIntOrDefault(minParentsParam, 0) if apr.Contains(mergesParam) { minParents = 2 @@ -126,44 +160,168 @@ func (cmd LogCmd) logWithLoggerFunc(ctx context.Context, commandStr string, args switch decorateOption { case "short", "full", "auto", "no": default: - cli.PrintErrln(color.HiRedString("fatal: invalid --decorate option: " + decorateOption)) - return 1 + return nil, fmt.Errorf("fatal: invalid --decorate option: %s", decorateOption) } - opts := logOpts{ + + opts := &logOpts{ numLines: apr.GetIntOrDefault(numLinesParam, -1), showParents: apr.Contains(parentsParam), minParents: minParents, oneLine: apr.Contains(oneLineParam), decoration: decorateOption, } + cs, notCs, tableName, err := parseRefsAndTable(ctx, apr, dEnv) + if err != nil { + return nil, err + } + opts.commitSpec = cs + opts.excludingCommitSpec = notCs + opts.tableName = tableName - // Just dolt log - if apr.NArg() == 0 { - return logCommits(ctx, dEnv, dEnv.RepoStateReader().CWBHeadSpec(), opts) - } else if apr.NArg() == 1 { // dolt log - argIsRef := actions.ValidateIsRef(ctx, apr.Arg(0), dEnv.DoltDB, dEnv.RepoStateReader()) - - if argIsRef { - cs, err := doltdb.NewCommitSpec(apr.Arg(0)) - if err != nil { - cli.PrintErrln(color.HiRedString("invalid commit %s\n", apr.Arg(0))) - } - return logCommits(ctx, dEnv, cs, opts) - } else { - return handleErrAndExit(logTableCommits(ctx, dEnv, opts, dEnv.RepoStateReader().CWBHeadSpec(), apr.Arg(0))) + excludingRef, ok := apr.GetValue(notParam) + if ok { + if opts.excludingCommitSpec != nil { + return nil, fmt.Errorf("cannot use --not argument with two dots or ref with ^") } - } else { // dolt log ref table - cs, err := doltdb.NewCommitSpec(apr.Arg(0)) + if len(opts.tableName) > 0 { + return nil, fmt.Errorf("cannot use --not argument with table") + } + cs, err := doltdb.NewCommitSpec(excludingRef) if err != nil { - cli.PrintErrln(color.HiRedString("invalid commit %s\n", apr.Arg(0))) + return nil, fmt.Errorf("invalid commit %s\n", excludingRef) } - return handleErrAndExit(logTableCommits(ctx, dEnv, opts, cs, apr.Arg(1))) + opts.excludingCommitSpec = cs + } + + return opts, nil +} + +func parseRefsAndTable(ctx context.Context, apr *argparser.ArgParseResults, dEnv *env.DoltEnv) (*doltdb.CommitSpec, *doltdb.CommitSpec, string, error) { + switch apr.NArg() { + // dolt log + case 0: + return nil, nil, "", nil + + // dolt log + case 1: + return getCommitOrTableFromString(ctx, apr.Arg(0), dEnv, true) + + // dolt log + case 2: + firstCs, firstExNotCs, _, err := getCommitOrTableFromString(ctx, apr.Arg(0), dEnv, false) + if err != nil { + return nil, nil, "", err + } + + secondCs, secondExNotCs, tableName, err := getCommitOrTableFromString(ctx, apr.Arg(1), dEnv, false) + if err != nil { + return nil, nil, "", err + } + + if len(tableName) > 0 { + if firstExNotCs != nil { + return nil, nil, "", fmt.Errorf("Providing tableName for two dot log not yet supported") + } + // dolt log
+ return firstCs, nil, tableName, nil + } + + if firstCs != nil && secondCs != nil { + commit, err := dEnv.DoltDB.Resolve(ctx, firstCs, dEnv.RepoStateReader().CWBHeadRef()) + if err != nil { + return nil, nil, "", err + } + + // Handles table name matching branch name (dolt log
) + exists, err := tableExists(ctx, commit, apr.Arg(1)) + if err != nil { + return nil, nil, "", err + } + + if exists { + return firstCs, nil, apr.Arg(1), nil + } + + return nil, nil, "", fmt.Errorf("Cannot provide two commit refs") // dolt log + } + if firstExNotCs != nil && secondExNotCs != nil { + return nil, nil, "", fmt.Errorf("Cannot exclude two commit refs") // dolt log ^ ^ + } + // dolt log ^ + if firstExNotCs != nil && secondCs != nil { + return secondCs, firstExNotCs, "", nil + } + // dolt log ^ + if firstCs != nil && secondExNotCs != nil { + return firstCs, secondExNotCs, "", nil + } + + return nil, nil, "", nil + + default: + return nil, nil, "", fmt.Errorf("Cannot provide more than 3 arguments") } } -func logCommits(ctx context.Context, dEnv *env.DoltEnv, cs *doltdb.CommitSpec, opts logOpts) int { - commit, err := dEnv.DoltDB.Resolve(ctx, cs, dEnv.RepoStateReader().CWBHeadRef()) +func getCommitOrTableFromString(ctx context.Context, str string, dEnv *env.DoltEnv, canDot bool) (*doltdb.CommitSpec, *doltdb.CommitSpec, string, error) { + // ... + if strings.Contains(str, "...") { + return nil, nil, "", fmt.Errorf("Three dot dolt log not supported") + } + // .. + if strings.Contains(str, "..") { + if !canDot { + return nil, nil, "", fmt.Errorf("Cannot use two dot when 2 arguments provided") + } + refs := strings.Split(str, "..") + notCs, err := getCommitSpec(refs[0]) + if err != nil { + return nil, nil, "", err + } + + cs, err := getCommitSpec(refs[1]) + if err != nil { + return nil, nil, "", err + } + + return cs, notCs, "", nil + } + + // ^ + if strings.Contains(str, "^") { + commit := strings.TrimPrefix(str, "^") + notCs, err := getCommitSpec(commit) + if err != nil { + return nil, nil, "", err + } + return nil, notCs, "", err + } + + argIsRef := actions.ValidateIsRef(ctx, str, dEnv.DoltDB, dEnv.RepoStateReader()) + // + if argIsRef { + cs, err := getCommitSpec(str) + if err != nil { + return nil, nil, "", err + } + return cs, nil, "", nil + } + + //
+ return nil, nil, str, nil +} + +func getCommitSpec(commit string) (*doltdb.CommitSpec, error) { + cs, err := doltdb.NewCommitSpec(commit) + if err != nil { + return nil, fmt.Errorf("invalid commit %s\n", commit) + } + return cs, nil +} + +func logCommits(ctx context.Context, dEnv *env.DoltEnv, opts *logOpts) int { + commit, err := dEnv.DoltDB.Resolve(ctx, opts.commitSpec, dEnv.RepoStateReader().CWBHeadRef()) if err != nil { cli.PrintErrln(color.HiRedString("Fatal error: cannot get HEAD commit for current branch.")) return 1 @@ -226,7 +384,26 @@ func logCommits(ctx context.Context, dEnv *env.DoltEnv, cs *doltdb.CommitSpec, o matchFunc := func(commit *doltdb.Commit) (bool, error) { return commit.NumParents() >= opts.minParents, nil } - commits, err := commitwalk.GetTopNTopoOrderedCommitsMatching(ctx, dEnv.DoltDB, h, opts.numLines, matchFunc) + + var commits []*doltdb.Commit + if opts.excludingCommitSpec == nil { + commits, err = commitwalk.GetTopNTopoOrderedCommitsMatching(ctx, dEnv.DoltDB, h, opts.numLines, matchFunc) + } else { + excludingCommit, err := dEnv.DoltDB.Resolve(ctx, opts.excludingCommitSpec, dEnv.RepoStateReader().CWBHeadRef()) + if err != nil { + cli.PrintErrln(color.HiRedString("Fatal error: cannot get excluding commit for current branch.")) + return 1 + } + + excludingHash, err := excludingCommit.HashOf() + + if err != nil { + cli.PrintErrln(color.HiRedString("Fatal error: failed to get commit hash")) + return 1 + } + + commits, err = commitwalk.GetDotDotRevisions(ctx, dEnv.DoltDB, h, dEnv.DoltDB, excludingHash, opts.numLines) + } if err != nil { cli.PrintErrln("Error retrieving commit.") @@ -280,20 +457,20 @@ func tableExists(ctx context.Context, commit *doltdb.Commit, tableName string) ( return ok, nil } -func logTableCommits(ctx context.Context, dEnv *env.DoltEnv, opts logOpts, cs *doltdb.CommitSpec, tableName string) error { - commit, err := dEnv.DoltDB.Resolve(ctx, cs, dEnv.RepoStateReader().CWBHeadRef()) +func logTableCommits(ctx context.Context, dEnv *env.DoltEnv, opts *logOpts) error { + commit, err := dEnv.DoltDB.Resolve(ctx, opts.commitSpec, dEnv.RepoStateReader().CWBHeadRef()) if err != nil { return err } // Check that the table exists in the head commit - exists, err := tableExists(ctx, commit, tableName) + exists, err := tableExists(ctx, commit, opts.tableName) if err != nil { return err } if !exists { - return fmt.Errorf("error: table %s does not exist", tableName) + return fmt.Errorf("error: table %s does not exist", opts.tableName) } h, err := commit.HashOf() @@ -340,7 +517,7 @@ func logTableCommits(ctx context.Context, dEnv *env.DoltEnv, opts logOpts, cs *d return err } - ok, err := didTableChangeBetweenRootValues(ctx, childRV, parentRV, tableName) + ok, err := didTableChangeBetweenRootValues(ctx, childRV, parentRV, opts.tableName) if err != nil { return err } @@ -387,7 +564,7 @@ func logRefs(pager *outputpager.Pager, comm logNode) { pager.Writer.Write([]byte("\033[33m) \033[0m")) } -func logCompact(pager *outputpager.Pager, opts logOpts, commits []logNode) { +func logCompact(pager *outputpager.Pager, opts *logOpts, commits []logNode) { for _, comm := range commits { if len(comm.parentHashes) < opts.minParents { return @@ -413,7 +590,7 @@ func logCompact(pager *outputpager.Pager, opts logOpts, commits []logNode) { } } -func logDefault(pager *outputpager.Pager, opts logOpts, commits []logNode) { +func logDefault(pager *outputpager.Pager, opts *logOpts, commits []logNode) { for _, comm := range commits { if len(comm.parentHashes) < opts.minParents { return @@ -451,7 +628,7 @@ func logDefault(pager *outputpager.Pager, opts logOpts, commits []logNode) { } } -func logToStdOut(opts logOpts, commits []logNode) { +func logToStdOut(opts *logOpts, commits []logNode) { if cli.ExecuteWithStdioRestored == nil { return } diff --git a/go/libraries/doltcore/env/actions/commitwalk/commitwalk.go b/go/libraries/doltcore/env/actions/commitwalk/commitwalk.go index a0d715c600..4821b30f64 100644 --- a/go/libraries/doltcore/env/actions/commitwalk/commitwalk.go +++ b/go/libraries/doltcore/env/actions/commitwalk/commitwalk.go @@ -159,7 +159,7 @@ func newQueue() *q { // // Roughly mimics `git log main..feature`. func GetDotDotRevisions(ctx context.Context, includedDB *doltdb.DoltDB, includedHead hash.Hash, excludedDB *doltdb.DoltDB, excludedHead hash.Hash, num int) ([]*doltdb.Commit, error) { - commitList := make([]*doltdb.Commit, 0, num) + var commitList []*doltdb.Commit q := newQueue() if err := q.SetInvisible(ctx, excludedDB, excludedHead); err != nil { return nil, err diff --git a/integration-tests/bats/log.bats b/integration-tests/bats/log.bats index 19a685b758..d7b8b7269f 100755 --- a/integration-tests/bats/log.bats +++ b/integration-tests/bats/log.bats @@ -40,6 +40,115 @@ teardown() { [[ ! "$output" =~ "BRANCH1" ]] || false } +@test "log: two dot log" { + dolt sql -q "create table testtable (pk int PRIMARY KEY)" + dolt add . + dolt commit -m "commit 1 MAIN" + dolt commit --allow-empty -m "commit 2 MAIN" + dolt checkout -b branch1 + dolt commit --allow-empty -m "commit 1 BRANCH1" + dolt commit --allow-empty -m "commit 2 BRANCH1" + dolt commit --allow-empty -m "commit 3 BRANCH1" + + run dolt log branch1 + [ $status -eq 0 ] + [[ "$output" =~ "MAIN" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + run dolt log main + [ $status -eq 0 ] + [[ "$output" =~ "MAIN" ]] || false + [[ ! "$output" =~ "BRANCH1" ]] || false + dolt checkout main + dolt commit --allow-empty -m "commit 3 AFTER" + + # Valid two dot + run dolt log main..branch1 + [ $status -eq 0 ] + [[ ! "$output" =~ "MAIN" ]] || false + [[ ! "$output" =~ "AFTER" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + run dolt log ^main branch1 + [ $status -eq 0 ] + [[ ! "$output" =~ "MAIN" ]] || false + [[ ! "$output" =~ "AFTER" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + run dolt log branch1 ^main + [ $status -eq 0 ] + [[ ! "$output" =~ "MAIN" ]] || false + [[ ! "$output" =~ "AFTER" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + run dolt log branch1 --not main + [ $status -eq 0 ] + [[ ! "$output" =~ "MAIN" ]] || false + [[ ! "$output" =~ "AFTER" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + run dolt log branch1..main + [ $status -eq 0 ] + [[ ! "$output" =~ "MAIN" ]] || false + [[ "$output" =~ "AFTER" ]] || false + [[ ! "$output" =~ "BRANCH1" ]] || false + run dolt log main..main + [ $status -eq 0 ] + + # Invalid two dot + run dolt log main..branch1 testtable + [ $status -eq 1 ] + run dolt log testtable main..branch1 + [ $status -eq 1 ] + run dolt log ^main branch1 testtable + [ $status -eq 1 ] + run dolt log branch1 testtable --not main + [ $status -eq 1 ] + run dolt log main..branch1 main + [ $status -eq 1 ] + run dolt log main main..branch1 + [ $status -eq 1 ] + run dolt log ^main ^branch1 + [ $status -eq 1 ] + run dolt log main branch1 + [ $status -eq 1 ] + run dolt log ^main testtable + [ $status -eq 1 ] + run dolt log ^branch1 --not main + [ $status -eq 1 ] + run dolt log main..branch1 --not main + [ $status -eq 1 ] + run dolt log branch1 --not main branch1 + [ $status -eq 1 ] + run dolt log branch1 --not main --not branch1 + [ $status -eq 1 ] + + run dolt log main...branch1 + [ $status -eq 1 ] +} + +@test "log: branch name and table name are the same" { + dolt commit --allow-empty -m "commit 1 MAIN" + dolt commit --allow-empty -m "commit 2 MAIN" + dolt checkout -b myname + dolt sql -q "create table myname (pk int PRIMARY KEY)" + dolt add . + dolt commit -m "commit 1 BRANCH1" + dolt commit --allow-empty -m "commit 2 BRANCH1" + dolt commit --allow-empty -m "commit 3 BRANCH1" + + # Should default to branch name if one argument provided + run dolt log myname + [ $status -eq 0 ] + [[ "$output" =~ "MAIN" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + + # Should default to first argument as branch name, second argument as table name (if table exists) if two arguments provided + run dolt log myname myname + [ $status -eq 0 ] + [[ ! "$output" =~ "MAIN" ]] || false + [[ "$output" =~ "BRANCH1" ]] || false + + # Table main does not exist + run dolt log main main + [ $status -eq 1 ] +} + @test "log: with -n specified" { dolt sql -q "create table test (pk int, c1 int, primary key(pk))" dolt add test