// Copyright 2019 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" "fmt" "strings" "github.com/dolthub/go-mysql-server/sql" "github.com/fatih/color" "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/utils/argparser" eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1" ) // printData structure is used to pass parsed status data to the print function // This ensures that the business logic is separate from the printing logic: first we parse the data, then we print it. type printData struct { branchName, remoteName, remoteBranchName string ahead, behind int64 conflictsPresent, showIgnoredTables, mergeActive bool stagedTables, unstagedTables, untrackedTables map[string]string constraintViolationTables, dataConflictTables, schemaConflictTables map[string]bool ignoredTables doltdb.IgnoredTables } var statusDocs = cli.CommandDocumentationContent{ ShortDesc: "Show the working status", LongDesc: `Displays working tables that differ from the current HEAD commit, tables that differ from the staged tables, and tables that are in the working tree that are not tracked by dolt. The first are what you would commit by running {{.EmphasisLeft}}dolt commit{{.EmphasisRight}}; the second and third are what you could commit by running {{.EmphasisLeft}}dolt add .{{.EmphasisRight}} before running {{.EmphasisLeft}}dolt commit{{.EmphasisRight}}.`, Synopsis: []string{""}, } type StatusCmd struct{} var _ cli.EventMonitoredCommand = StatusCmd{} // Name is returns the name of the Dolt cli command. This is what is used on the command line to invoke the command func (cmd StatusCmd) Name() string { return "status" } // Description returns a description of the command func (cmd StatusCmd) Description() string { return "Show the working tree status." } func (cmd StatusCmd) Docs() *cli.CommandDocumentation { ap := cmd.ArgParser() return cli.NewCommandDocumentation(statusDocs, ap) } func (cmd StatusCmd) ArgParser() *argparser.ArgParser { ap := argparser.NewArgParserWithMaxArgs(cmd.Name(), 0) ap.SupportsFlag(cli.ShowIgnoredFlag, "", "Show tables that are ignored (according to dolt_ignore)") return ap } func (cmd StatusCmd) EventType() eventsapi.ClientEventType { return eventsapi.ClientEventType_STATUS } func (cmd StatusCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int { ap := cmd.ArgParser() apr, _, terminate, status := ParseArgsOrPrintHelp(ap, commandStr, args, statusDocs) if terminate { return status } showIgnoredTables := apr.Contains(cli.ShowIgnoredFlag) err := PrintStatus(ctx, showIgnoredTables, cliCtx) if err != nil { return handleStatusVErr(err) } return 0 } func PrintStatus(ctx context.Context, showIgnoredTables bool, cliCtx cli.CliContext) error { // configure SQL engine qe, err := cliCtx.QueryEngine(ctx) if err != nil { return err } // get status information from the database pd, err := createPrintData(qe.Queryist, qe.Context, showIgnoredTables, cliCtx) if err != nil { return err } err = printEverything(pd) if err != nil { return err } return nil } func createPrintData(queryist cli.Queryist, sqlCtx *sql.Context, showIgnoredTables bool, cliCtx cli.CliContext) (*printData, error) { var branchName string if brName, hasBranch := cliCtx.GlobalArgs().GetValue(cli.BranchParam); hasBranch { branchName = brName } else if useDb, hasUseDb := cliCtx.GlobalArgs().GetValue(UseDbFlag); hasUseDb { _, branchName = doltdb.SplitRevisionDbName(useDb) } if branchName == "" { var err error branchName, err = getActiveBranchName(sqlCtx, queryist) if err != nil { return nil, err } } ignorePatterns, err := getIgnoredTablePatternsFromSql(queryist, sqlCtx) if err != nil { return nil, err } stagedTableNames, workingTableNames, err := getWorkingStagedTables(queryist, sqlCtx) if err != nil { return nil, err } constraintViolationTables, err := getConstraintViolationTables(queryist, sqlCtx) if err != nil { return nil, err } dataConflictTables, err := getDataConflictsTables(queryist, sqlCtx) if err != nil { return nil, err } mergeActive, err := getMergeStatus(queryist, sqlCtx) if err != nil { return nil, err } remoteName, remoteBranchName, currentBranchCommit, err := getLocalBranchInfo(queryist, sqlCtx, branchName) if err != nil { return nil, err } ahead, behind, err := getUpstreamInfo(queryist, sqlCtx, remoteName, remoteBranchName, currentBranchCommit) if err != nil { return nil, err } var statusRows []sql.Row var sqlQuery string if showIgnoredTables { sqlQuery = "select table_name,staged,status,ignored from dolt_status_ignored;" } else { sqlQuery = "select table_name,staged,status,ignored from dolt_status_ignored where ignored = false;" } statusRows, err = cli.GetRowsForSql(queryist, sqlCtx, sqlQuery) if err != nil { return nil, err } statusPresent := len(statusRows) > 0 // sort tables into categories conflictsPresent := false stagedTables := map[string]string{} unstagedTables := map[string]string{} untrackedTables := map[string]string{} unmergedTables := map[string]string{} schemaConflictTables := map[string]bool{} ignoredTables := doltdb.IgnoredTables{} if statusPresent { for _, row := range statusRows { tableName := row[0].(string) staged := row[1] status := row[2].(string) ignored, err := cli.QueryValueAsBool(row[3]) if err != nil { return nil, err } isStaged, err := cli.QueryValueAsBool(staged) if err != nil { return nil, err } shouldIgnoreTable := ignored || doltdb.IsFullTextTable(tableName) if showIgnoredTables && shouldIgnoreTable { ignoredTables.Ignore = append(ignoredTables.Ignore, doltdb.TableName{Name: tableName}) } if !isStaged && !shouldIgnoreTable { // Check for ignore conflicts even when not showing ignored tables _, err := ignorePatterns.IsTableNameIgnored(doltdb.TableName{Name: tableName}) if conflict := doltdb.AsDoltIgnoreInConflict(err); conflict != nil { ignoredTables.Conflicts = append(ignoredTables.Conflicts, *conflict) } else if err != nil { return nil, err } } shouldIgnoreTable = shouldIgnoreTable || doltdb.IsFullTextTable(tableName) switch status { case "renamed": // for renamed tables, add both source and dest changes parts := strings.Split(tableName, " -> ") srcTableName := parts[0] dstTableName := parts[1] if workingTableNames[dstTableName] { unstagedTables[srcTableName] = "deleted" untrackedTables[dstTableName] = "new table" } else if stagedTableNames[dstTableName] { stagedTables[tableName] = status } case "conflict": conflictsPresent = true if isStaged { stagedTables[tableName] = status } else { unmergedTables[tableName] = "both modified" } case "deleted", "modified", "added", "new table": if shouldIgnoreTable { continue } if isStaged { stagedTables[tableName] = status } else { if status == "new table" { untrackedTables[tableName] = status } else { unstagedTables[tableName] = status } } case "schema conflict": conflictsPresent = true schemaConflictTables[tableName] = true case "constraint violation": constraintViolationTables[tableName] = true default: panic(fmt.Sprintf("table %s, unexpected merge status: %s", tableName, status)) } } } pd := printData{ branchName: branchName, remoteName: remoteName, remoteBranchName: remoteBranchName, ahead: ahead, behind: behind, conflictsPresent: conflictsPresent, showIgnoredTables: showIgnoredTables, mergeActive: mergeActive, stagedTables: stagedTables, unstagedTables: unstagedTables, untrackedTables: untrackedTables, ignoredTables: ignoredTables, constraintViolationTables: constraintViolationTables, schemaConflictTables: schemaConflictTables, dataConflictTables: dataConflictTables, } return &pd, nil } func getUpstreamInfo(queryist cli.Queryist, sqlCtx *sql.Context, remoteName string, upstreamBranchName string, currentBranchCommit string) (ahead int64, behind int64, err error) { ahead = 0 behind = 0 if len(remoteName) > 0 || len(upstreamBranchName) > 0 { // get remote branch var q string var upstreamBranchRef string if remoteName != "" { upstreamBranchRef = fmt.Sprintf("remotes/%s/%s", remoteName, upstreamBranchName) q = fmt.Sprintf("select name, hash from dolt_remote_branches where name = '%s';", upstreamBranchRef) } else if upstreamBranchName != "" { q = fmt.Sprintf("select name, hash from dolt_branches where name = '%s'", upstreamBranchName) } upstreamBranches, err := cli.GetRowsForSql(queryist, sqlCtx, q) if err != nil { return ahead, behind, err } if len(upstreamBranches) > 1 { return ahead, behind, fmt.Errorf("runtime error: too many results returned for upstream branch %s", upstreamBranchRef) } else if len(upstreamBranches) == 0 { return ahead, behind, nil } upstreamBranchCommit := upstreamBranches[0][1].(string) q = fmt.Sprintf("call dolt_count_commits('--from', '%s', '--to', '%s')", currentBranchCommit, upstreamBranchCommit) rows, err := cli.GetRowsForSql(queryist, sqlCtx, q) if err != nil { return ahead, behind, err } if len(rows) != 1 { return ahead, behind, fmt.Errorf("could not count commits between %s and %s", currentBranchCommit, upstreamBranchCommit) } aheadDb := rows[0][0] behindDb := rows[0][1] ahead, err = getInt64ColAsInt64(aheadDb) if err != nil { return ahead, behind, err } behind, err = getInt64ColAsInt64(behindDb) if err != nil { return ahead, behind, err } } return ahead, behind, nil } func getLocalBranchInfo(queryist cli.Queryist, sqlCtx *sql.Context, branchName string) (remoteName, remoteBranchName, currentBranchCommit string, err error) { remoteName = "" currentBranchCommit = "" remoteBranchName = "" localBranches, err := cli.GetRowsForSql(queryist, sqlCtx, "select name, hash, remote, branch from dolt_branches;") if err != nil { return remoteName, remoteBranchName, currentBranchCommit, err } for _, row := range localBranches { branch := row[0].(string) if branch == branchName { currentBranchCommit = row[1].(string) remoteName = row[2].(string) remoteBranchName = row[3].(string) } } if currentBranchCommit == "" { return remoteName, remoteBranchName, currentBranchCommit, fmt.Errorf("could not find current branch commit") } return remoteName, remoteBranchName, currentBranchCommit, nil } func getMergeStatus(queryist cli.Queryist, sqlCtx *sql.Context) (bool, error) { mergeRows, err := cli.GetRowsForSql(queryist, sqlCtx, "select is_merging from dolt_merge_status;") if err != nil { return false, err } // determine if a merge is active mergeActive := false if len(mergeRows) == 1 { isMerging := mergeRows[0][0] mergeActive, err = cli.QueryValueAsBool(isMerging) if err != nil { return false, err } } else { mergeActive = true } return mergeActive, nil } func getDataConflictsTables(queryist cli.Queryist, sqlCtx *sql.Context) (map[string]bool, error) { dataConflictTables := make(map[string]bool) dataConflicts, err := cli.GetRowsForSql(queryist, sqlCtx, "select * from dolt_conflicts;") if err != nil { return nil, err } for _, row := range dataConflicts { tableName := row[0].(string) dataConflictTables[tableName] = true } return dataConflictTables, nil } func getConstraintViolationTables(queryist cli.Queryist, sqlCtx *sql.Context) (map[string]bool, error) { constraintViolationTables := make(map[string]bool) constraintViolations, err := cli.GetRowsForSql(queryist, sqlCtx, "select * from dolt_constraint_violations;") if err != nil { return nil, err } for _, row := range constraintViolations { tableName := row[0].(string) constraintViolationTables[tableName] = true } return constraintViolationTables, nil } func getWorkingStagedTables(queryist cli.Queryist, sqlCtx *sql.Context) (map[string]bool, map[string]bool, error) { stagedTableNames := make(map[string]bool) workingTableNames := make(map[string]bool) diffs, err := cli.GetRowsForSql(queryist, sqlCtx, "select * from dolt_diff where commit_hash='WORKING' OR commit_hash='STAGED';") if err != nil { return nil, nil, err } for _, row := range diffs { commitHash := row[0].(string) tableName := row[1].(string) if commitHash == "STAGED" { stagedTableNames[tableName] = true } else { workingTableNames[tableName] = true } } return stagedTableNames, workingTableNames, nil } func getIgnoredTablePatternsFromSql(queryist cli.Queryist, sqlCtx *sql.Context) (doltdb.IgnorePatterns, error) { var ignorePatterns []doltdb.IgnorePattern ignoreRows, err := cli.GetRowsForSql(queryist, sqlCtx, fmt.Sprintf("select * from %s", doltdb.IgnoreTableName)) if err != nil { return nil, err } for _, row := range ignoreRows { pattern := row[0].(string) ignoreVal := row[1] var ignore bool if ignoreString, ok := ignoreVal.(string); ok { ignore = ignoreString == "1" } else if ignoreInt, ok := ignoreVal.(int8); ok { ignore = ignoreInt == 1 } else { return nil, fmt.Errorf("unexpected type for ignore column, value = %s", ignoreVal) } ip := doltdb.NewIgnorePattern(pattern, ignore) ignorePatterns = append(ignorePatterns, ip) } return ignorePatterns, nil } func printEverything(data *printData) error { statusFmt := "\t%-18s%s" constraintViolationsExist := len(data.constraintViolationTables) > 0 changesPresent := false // branch name cli.Printf(branchHeader, data.branchName) // remote info if data.remoteName != "" || data.remoteBranchName != "" { ahead := data.ahead behind := data.behind var remoteBranchRef string if data.remoteName != "" { remoteBranchRef = fmt.Sprintf("%s/", data.remoteName) } remoteBranchRef += data.remoteBranchName if ahead > 0 && behind > 0 { cli.Printf(`Your branch and '%s' have diverged, and have %v and %v different commits each, respectively. (use "dolt pull" to update your local branch) `, remoteBranchRef, ahead, behind) changesPresent = true } else if ahead > 0 { s := "" if ahead > 1 { s = "s" } cli.Printf(`Your branch is ahead of '%s' by %v commit%s. (use "dolt push" to publish your local commits) `, remoteBranchRef, ahead, s) changesPresent = true } else if behind > 0 { s := "" if behind > 1 { s = "s" } cli.Printf(`Your branch is behind '%s' by %v commit%s, and can be fast-forwarded. (use "dolt pull" to update your local branch) `, remoteBranchRef, behind, s) changesPresent = true } else { cli.Printf("Your branch is up to date with '%s'.\n", remoteBranchRef) } } // merge info if data.mergeActive { if constraintViolationsExist && data.conflictsPresent { cli.Println(fmt.Sprintf(unmergedTablesHeader, "conflicts and constraint violations")) } else if data.conflictsPresent { cli.Println(fmt.Sprintf(unmergedTablesHeader, "conflicts")) } else if constraintViolationsExist { cli.Println(fmt.Sprintf(unmergedTablesHeader, "constraint violations")) } else { cli.Println(allMergedHeader) } } // staged tables if len(data.stagedTables) > 0 { cli.Println(stagedHeader) cli.Println(stagedHeaderHelp) for tableName, status := range data.stagedTables { if !doltdb.IsReadOnlySystemTable(doltdb.TableName{Name: tableName}) { text := fmt.Sprintf(statusFmt, status+":", tableName) greenText := color.GreenString(text) cli.Println(greenText) changesPresent = true } } } // conflicts and violations if data.conflictsPresent || constraintViolationsExist { cli.Println(unmergedPathsHeader) cli.Println(mergedTableHelp) // conflicted tables if data.conflictsPresent { // show schema conflicts for tableName := range data.schemaConflictTables { text := fmt.Sprintf(statusFmt, schemaConflictLabel, tableName) redText := color.RedString(text) cli.Println(redText) changesPresent = true } // show data conflicts for tableName := range data.dataConflictTables { text := fmt.Sprintf(statusFmt, bothModifiedLabel, tableName) redText := color.RedString(text) cli.Println(redText) changesPresent = true } } // constraint violations if constraintViolationsExist { for tableName := range data.constraintViolationTables { hasConflicts := data.dataConflictTables[tableName] || data.schemaConflictTables[tableName] if hasConflicts { continue } text := fmt.Sprintf(statusFmt, "modified", tableName) redText := color.RedString(text) cli.Println(redText) changesPresent = true } } } // unstaged tables if len(data.unstagedTables) > 0 { filteredUnstagedTables := make(map[string]string) for tableName, status := range data.unstagedTables { hasConflicts := data.dataConflictTables[tableName] || data.schemaConflictTables[tableName] hasViolations := data.constraintViolationTables[tableName] if hasConflicts || hasViolations { continue } filteredUnstagedTables[tableName] = status } if len(filteredUnstagedTables) > 0 { cli.Println() cli.Println(workingHeader) cli.Println(workingHeaderHelp) for tableName, status := range filteredUnstagedTables { text := fmt.Sprintf(statusFmt, status+":", tableName) redText := color.RedString(text) cli.Println(redText) } changesPresent = true } } // untracked tables if len(data.untrackedTables) > 0 { if changesPresent { cli.Println() } cli.Println(untrackedHeader) cli.Println(untrackedHeaderHelp) for tableName, status := range data.untrackedTables { text := fmt.Sprintf(statusFmt, status+":", tableName) redText := color.RedString(text) cli.Println(redText) } changesPresent = true } // ignored tables if data.showIgnoredTables && len(data.ignoredTables.Ignore) > 0 { if changesPresent { cli.Println() } cli.Println(ignoredHeader) cli.Println(ignoredHeaderHelp) for _, tableName := range data.ignoredTables.Ignore { text := fmt.Sprintf(statusFmt, "new table:", tableName) redText := color.RedString(text) cli.Println(redText) changesPresent = true } } if len(data.ignoredTables.Conflicts) > 0 { if changesPresent { cli.Println() } cli.Println(conflictedIgnoredHeader) cli.Println(conflictedIgnoredHeaderHelp) for _, conflict := range data.ignoredTables.Conflicts { text := fmt.Sprintf(statusFmt, "new table:", conflict.Table) redText := color.RedString(text) cli.Println(redText) changesPresent = true } } // nothing to commit if !changesPresent { cli.Println("nothing to commit, working tree clean") } return nil } func handleStatusVErr(err error) int { if err != argparser.ErrHelp { cli.PrintErrln(errhand.VerboseErrorFromError(err).Verbose()) } return 1 }