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

343 lines
11 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 cli
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/fatih/color"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/events"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
config "github.com/dolthub/dolt/go/libraries/utils/config"
"github.com/dolthub/dolt/go/store/nbs"
eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1"
)
func IsHelp(str string) bool {
str = strings.TrimSpace(str)
if len(str) == 0 {
return false
}
if str[0] != '-' {
return false
}
str = strings.ToLower(strings.TrimLeft(str, "- "))
return str == "h" || str == "help"
}
func hasHelpFlag(args []string) bool {
for _, arg := range args {
if IsHelp(arg) {
return true
}
}
return false
}
// globalArgsSpecifyDB checks if the global arguments contain --use-db or --host flags,
// which indicate that the user is manually specifying a database connection
// rather than relying on the current directory being a dolt repository.
func globalArgsSpecifyDB(cliCtx CliContext) bool {
if cliCtx == nil {
return false
}
globalArgs := cliCtx.GlobalArgs()
if globalArgs == nil {
return false
}
_, hasUseDb := globalArgs.GetValue("use-db")
_, hasHost := globalArgs.GetValue(HostFlag)
return hasUseDb || hasHost
}
// Command is the interface which defines a Dolt cli command
type Command interface {
// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
Name() string
// Description returns a description of the command
Description() string
// Exec executes the command
Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx CliContext) int
// Docs returns the documentation for this command, or nil if it's undocumented
Docs() *CommandDocumentation
// ArgParser returns the arg parser for this command
ArgParser() *argparser.ArgParser
}
// SignalCommand is an extension of Command that allows commands to install their own signal handlers, rather than use
// the global one (which cancels the global context).
type SignalCommand interface {
Command
// InstallsSignalHandlers returns whether this command manages its own signal handlers for interruption / termination.
InstallsSignalHandlers() bool
}
// Queryist is generic interface for executing queries. Commands will be provided a Queryist to perform any work using
// SQL. The Queryist can be obtained from the CliContext passed into the Exec method by calling the QueryEngine method.
type Queryist interface {
Query(ctx *sql.Context, query string) (sql.Schema, sql.RowIter, *sql.QueryFlags, error)
QueryWithBindings(ctx *sql.Context, query string, parsed sqlparser.Statement, bindings map[string]sqlparser.Expr, qFlags *sql.QueryFlags) (sql.Schema, sql.RowIter, *sql.QueryFlags, error)
}
// ShellServerQueryist is used to gather warnings in the sql-shell context when a server is running.
// We call an extra "show warnings" query, but want to avoid this in other cases, (i.e. dolt sql -q)
type ShellServerQueryist interface {
EnableGatherWarnings()
}
// This type is to store the content of a documented command, elsewhere we can transform this struct into
// other structs that are used to generate documentation at the command line and in markdown files.
type CommandDocumentationContent struct {
ShortDesc string
LongDesc string
Synopsis []string
}
// type CommandDocumentation
// RepoNotRequiredCommand is an optional interface that commands can implement if the command can be run without
// the current directory being a valid Dolt data repository. Any commands not implementing this interface are
// assumed to require that they be run from a directory containing a Dolt data repository.
type RepoNotRequiredCommand interface {
// RequiresRepo should return false if this interface is implemented, and the command does not have the requirement
// that it be run from within a data repository directory
RequiresRepo() bool
}
// EventMonitoredCommand is an optional interface that can be overridden in order to generate an event which is sent
// to the metrics system when the command is run
type EventMonitoredCommand interface {
// EventType returns the type of the event to log
EventType() eventsapi.ClientEventType
}
// HiddenCommand is an optional interface that can be overridden so that a command is hidden from the help text
type HiddenCommand interface {
// Hidden should return true if this command should be hidden from the help text
Hidden() bool
}
// SubCommandHandler is a command implementation which holds subcommands which can be called
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, 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, 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 {
return hc.name
}
func (hc SubCommandHandler) Description() string {
return hc.description
}
func (hc SubCommandHandler) RequiresRepo() bool {
return false
}
func (hc SubCommandHandler) Docs() *CommandDocumentation {
return nil
}
func (hc SubCommandHandler) ArgParser() *argparser.ArgParser {
return nil
}
func (hc SubCommandHandler) Hidden() bool {
return hc.hidden
}
func (hc SubCommandHandler) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx CliContext) int {
if len(args) < 1 && hc.Unspecified == nil {
hc.PrintUsage(commandStr)
return 1
}
var subCommandStr string
if len(args) > 0 {
subCommandStr = strings.ToLower(strings.TrimSpace(args[0]))
}
for _, cmd := range hc.Subcommands {
if strings.EqualFold(cmd.Name(), subCommandStr) {
return hc.handleCommand(ctx, commandStr+" "+subCommandStr, cmd, args[1:], dEnv, cliCtx)
}
}
if hc.Unspecified != nil {
return hc.handleCommand(ctx, commandStr, hc.Unspecified, args, dEnv, cliCtx)
}
if !IsHelp(subCommandStr) {
PrintErrln(color.RedString("Unknown Command " + subCommandStr))
return 1
}
hc.PrintUsage(commandStr)
return 0
}
func (hc SubCommandHandler) handleCommand(ctx context.Context, commandStr string, cmd Command, args []string, dEnv *env.DoltEnv, cliCtx CliContext) int {
cmdRequiresRepo := true
if rnrCmd, ok := cmd.(RepoNotRequiredCommand); ok {
cmdRequiresRepo = rnrCmd.RequiresRepo()
}
// If --use-db or --host is specified, the user is manually specifying a database/server
// connection, so we don't require the current directory to be a valid dolt repository.
if cmdRequiresRepo && !hasHelpFlag(args) && !globalArgsSpecifyDB(cliCtx) {
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)
}
// Certain commands cannot tolerate a top-level signal handler (which cancels the root context) but need to manage
// their own interrupt semantics.
if signalCmd, ok := cmd.(SignalCommand); !ok || !signalCmd.InstallsSignalHandlers() {
var stop context.CancelFunc
ctx, stop = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
}
ret := cmd.Exec(ctx, commandStr, args, dEnv, cliCtx)
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 {
if !dEnv.HasDoltDataDir() {
PrintErrln(color.RedString("The current directory is not a valid dolt repository."))
PrintErrln("run: dolt init before trying to run this command")
return false
} else if dEnv.RSLoadErr != nil {
PrintErrln(color.RedString("The current directories repository state is invalid"))
PrintErrln(dEnv.RSLoadErr.Error())
return false
} else if dEnv.DBLoadError != nil {
PrintErrln(color.RedString("Failed to load database."))
PrintErrln(dEnv.DBLoadError.Error())
if dEnv.DBLoadError == nbs.ErrUnreadableManifest {
PrintErrln("\tyou might need to upgrade your Dolt client")
PrintErrln("\tvisit https://github.com/dolthub/dolt/releases/latest/")
}
return false
}
return true
}
const (
userNameRequiredError = `Author identity unknown
*** Please tell me who you are.
Run
dolt config --global --add user.email "you@example.com"
dolt config --global --add user.name "Your Name"
to set your account's default identity.
Omit --global to set the identity only in this repository.
fatal: empty ident name not allowed
`
)
// CheckUserNameAndEmail returns true if the user name and email are set for this environment, or prints an error and
// returns false if not.
func CheckUserNameAndEmail(cfg *env.DoltCliConfig) bool {
_, err := cfg.GetString(config.UserEmailKey)
if err != nil {
PrintErr(userNameRequiredError)
return false
}
_, err = cfg.GetString(config.UserNameKey)
if err != nil {
PrintErr(userNameRequiredError)
return false
}
return true
}
func (hc SubCommandHandler) GetUsage(commandStr string) string {
str := "Valid commands for " + commandStr + " are\n"
for _, cmd := range hc.Subcommands {
if hiddenCmd, ok := cmd.(HiddenCommand); ok {
if hiddenCmd.Hidden() {
continue
}
}
str += fmt.Sprintf(" %16s - %s\n", cmd.Name(), cmd.Description())
}
return str
}
func (hc SubCommandHandler) PrintUsage(commandStr string) {
usage := hc.GetUsage(commandStr)
Println(usage)
}