Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

657 lines
19 KiB
Go

// 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
}