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

414 lines
14 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"
"errors"
"fmt"
"strings"
"time"
"github.com/dolthub/go-mysql-server/sql"
"github.com/gocraft/dbr/v2"
"github.com/gocraft/dbr/v2/dialect"
"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/doltcore/env/actions"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dprocedures"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/config"
"github.com/dolthub/dolt/go/store/util/outputpager"
eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1"
)
var pullDocs = cli.CommandDocumentationContent{
ShortDesc: "Fetch from and integrate with another repository or a local branch",
LongDesc: `Incorporates changes from a remote repository into the current branch. In its default mode, {{.EmphasisLeft}}dolt pull{{.EmphasisRight}} is shorthand for {{.EmphasisLeft}}dolt fetch{{.EmphasisRight}} followed by {{.EmphasisLeft}}dolt merge <remote>/<branch>{{.EmphasisRight}}.
More precisely, dolt pull runs {{.EmphasisLeft}}dolt fetch{{.EmphasisRight}} with the given parameters and calls {{.EmphasisLeft}}dolt merge{{.EmphasisRight}} to merge the retrieved branch {{.EmphasisLeft}}HEAD{{.EmphasisRight}} into the current branch.
With {{.EmphasisLeft}}--rebase{{.EmphasisRight}}, it runs {{.EmphasisLeft}}dolt rebase{{.EmphasisRight}} instead of {{.EmphasisLeft}}dolt merge{{.EmphasisRight}} after fetching. If the rebase encounters data conflicts, it will pause and allow you to resolve them, then continue with {{.EmphasisLeft}}dolt rebase --continue{{.EmphasisRight}}.
`,
Synopsis: []string{
`[{{.LessThan}}remote{{.GreaterThan}}, [{{.LessThan}}remoteBranch{{.GreaterThan}}]]`,
},
}
type PullCmd struct{}
// 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 PullCmd) Name() string {
return "pull"
}
// Description returns a description of the command
func (cmd PullCmd) Description() string {
return "Fetch from a dolt remote data repository and merge."
}
func (cmd PullCmd) Docs() *cli.CommandDocumentation {
ap := cli.CreatePullArgParser()
return cli.NewCommandDocumentation(pullDocs, ap)
}
func (cmd PullCmd) ArgParser() *argparser.ArgParser {
return cli.CreatePullArgParser()
}
// EventType returns the type of the event to log
func (cmd PullCmd) EventType() eventsapi.ClientEventType {
return eventsapi.ClientEventType_PULL
}
// Exec executes the command
func (cmd PullCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cli.CreatePullArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, pullDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
if apr.NArg() > 2 {
verr := errhand.VerboseErrorFromError(actions.ErrInvalidPullArgs)
return HandleVErrAndExitCode(verr, usage)
}
if apr.ContainsAll(cli.CommitFlag, cli.NoCommitFlag) {
verr := errhand.VerboseErrorFromError(errors.New(fmt.Sprintf(ErrConflictingFlags, cli.CommitFlag, cli.NoCommitFlag)))
return HandleVErrAndExitCode(verr, usage)
}
if apr.ContainsAll(cli.SquashParam, cli.NoFFParam) {
verr := errhand.VerboseErrorFromError(errors.New(fmt.Sprintf(ErrConflictingFlags, cli.SquashParam, cli.NoFFParam)))
return HandleVErrAndExitCode(verr, usage)
}
if apr.ContainsAll(cli.FFOnlyParam, cli.NoFFParam) {
verr := errhand.VerboseErrorFromError(errors.New(fmt.Sprintf(ErrConflictingFlags, cli.FFOnlyParam, cli.NoFFParam)))
return HandleVErrAndExitCode(verr, usage)
}
if apr.ContainsAll(cli.FFOnlyParam, cli.SquashParam) {
verr := errhand.VerboseErrorFromError(errors.New(fmt.Sprintf(ErrConflictingFlags, cli.FFOnlyParam, cli.SquashParam)))
return HandleVErrAndExitCode(verr, usage)
}
if apr.Contains(cli.RebaseParam) && apr.ContainsAny(cli.SquashParam, cli.NoFFParam, cli.FFOnlyParam, cli.NoCommitFlag) {
var conflicting string
switch {
case apr.Contains(cli.SquashParam):
conflicting = cli.SquashParam
case apr.Contains(cli.NoFFParam):
conflicting = cli.NoFFParam
case apr.Contains(cli.FFOnlyParam):
conflicting = cli.FFOnlyParam
case apr.Contains(cli.NoCommitFlag):
conflicting = cli.NoCommitFlag
}
verr := errhand.VerboseErrorFromError(errors.New(fmt.Sprintf(ErrConflictingFlags, cli.RebaseParam, conflicting)))
return HandleVErrAndExitCode(verr, usage)
}
// This command may create a commit, so we need user identity
if !cli.CheckUserNameAndEmail(cliCtx.Config()) {
bdr := errhand.BuildDError("Could not determine name and/or email.")
bdr.AddDetails("Log into DoltHub: dolt login")
bdr.AddDetails("OR add name to config: dolt config [--global|--local] --add %[1]s \"FIRST LAST\"", config.UserNameKey)
bdr.AddDetails("OR add email to config: dolt config [--global|--local] --add %[1]s \"EMAIL_ADDRESS\"", config.UserEmailKey)
return HandleVErrAndExitCode(bdr.Build(), usage)
}
queryist, err := cliCtx.QueryEngine(ctx)
if err != nil {
cli.Println(err.Error())
return 1
}
if apr.Contains(cli.RebaseParam) && queryist.IsRemote {
msg := fmt.Sprintf(cli.RemoteUnsupportedMsg, commandStr)
cli.Println(msg)
return 1
}
query, err := constructInterpolatedDoltPullQuery(apr)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
errChan := make(chan error)
go func() {
defer close(errChan)
// allows pulls (merges) that create conflicts to stick
_, _, _, err = queryist.Queryist.Query(queryist.Context, "set @@dolt_force_transaction_commit = 1")
if err != nil {
errChan <- err
return
}
if apr.Contains(cli.RebaseParam) {
_, _, _, err = queryist.Queryist.Query(queryist.Context, "set @@dolt_allow_commit_conflicts = 1")
if err != nil {
errChan <- err
return
}
}
// save current head for diff summaries after pull
headHash, err := getHashOf(queryist.Queryist, queryist.Context, "HEAD")
if err != nil {
cli.Println("failed to get hash of HEAD, pull not started")
errChan <- err
}
_, rowIter, _, err := queryist.Queryist.Query(queryist.Context, query)
if err != nil {
if apr.Contains(cli.RebaseParam) && isRebaseConflictError(err) {
if checkoutErr := syncCliBranchToSqlSessionBranch(queryist.Context, dEnv); checkoutErr != nil {
// The checkout of the dolt_rebase_<> branch failed, we might want to `--abort`, but chances
// are that won't work either. We are gonna be in a weird state no matter what, so just print the error.
cli.Println("warning: failed to sync CLI branch after rebase conflict:", checkoutErr.Error())
}
}
errChan <- err
return
}
rows, err := sql.RowIterToRows(queryist.Context, rowIter)
if err != nil {
errChan <- err
return
}
if len(rows) != 1 {
err = fmt.Errorf("Runtime error: merge operation returned unexpected number of rows: %d", len(rows))
errChan <- err
return
}
row := rows[0]
upToDate, err := everythingUpToDate(row)
if err != nil {
errChan <- err
return
}
if upToDate {
if !apr.Contains(cli.SilentFlag) {
cli.Println("\nEverything up-to-date")
}
return
}
if apr.Contains(cli.RebaseParam) {
if !apr.Contains(cli.SilentFlag) {
if len(row) > 2 && row[2] != nil {
msg, ok := row[2].(string)
if ok && msg != "" {
cli.Println(msg)
}
}
}
return
}
// if merge is called with '--no-commit', we need to commit the sql transaction or the staged changes will be lost
_, _, _, err = queryist.Queryist.Query(queryist.Context, "COMMIT")
if err != nil {
errChan <- err
return
}
remoteHash, remoteRef, err := getRemoteHashForPull(apr, queryist.Context, queryist.Queryist)
if err != nil {
cli.Println("pull finished, but failed to get hash of remote ref")
cli.Println(err.Error())
}
if apr.Contains(cli.ForceFlag) {
if remoteHash != "" && headHash != "" {
cli.Println("Updating", headHash+".."+remoteHash)
}
commit, err := getCommitInfo(queryist.Context, queryist.Queryist, "HEAD")
if err != nil {
cli.Println("pull finished, but failed to get commit info")
cli.Println(err.Error())
return
}
if cli.ExecuteWithStdioRestored != nil {
cli.ExecuteWithStdioRestored(func() {
pager := outputpager.Start()
defer pager.Stop()
PrintCommitInfo(pager, 0, false, false, "auto", commit)
})
}
} else {
fastFwd := getFastforward(row, dprocedures.PullProcFFIndex)
var success int
if apr.Contains(cli.NoCommitFlag) {
success = printMergeStats(fastFwd, apr, queryist.Queryist, queryist.Context, usage, headHash, remoteHash, "HEAD", "STAGED")
} else {
success = printMergeStats(fastFwd, apr, queryist.Queryist, queryist.Context, usage, headHash, remoteHash, "HEAD", remoteRef)
}
if success == 1 {
errChan <- errors.New(" ") //return a non-nil error for the correct exit code but no further messages to print
return
}
}
}()
spinner := TextSpinner{}
if !apr.Contains(cli.SilentFlag) {
cli.Print(spinner.next() + " Pulling...")
defer func() {
cli.DeleteAndPrint(len(" Pulling...")+1, "")
}()
}
for {
select {
case err := <-errChan:
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
case <-ctx.Done():
if ctx.Err() != nil {
switch ctx.Err() {
case context.DeadlineExceeded:
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("timeout exceeded")), usage)
case context.Canceled:
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("pull cancelled by force")), usage)
default:
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(errors.New("error cancelling context: "+ctx.Err().Error())), usage)
}
}
return HandleVErrAndExitCode(nil, usage)
case <-time.After(time.Millisecond * 50):
if !apr.Contains(cli.SilentFlag) {
cli.DeleteAndPrint(len(" Pulling...")+1, spinner.next()+" Pulling...")
}
}
}
}
// constructInterpolatedDoltPullQuery constructs the sql query necessary to call the DOLT_PULL() function.
// Also interpolates this query to prevent sql injection.
func constructInterpolatedDoltPullQuery(apr *argparser.ArgParseResults) (string, error) {
var params []interface{}
var args []string
for _, arg := range apr.Args {
args = append(args, "?")
params = append(params, arg)
}
if apr.Contains(cli.SquashParam) {
args = append(args, "'--squash'")
}
if apr.Contains(cli.NoFFParam) {
args = append(args, "'--no-ff'")
}
if apr.Contains(cli.FFOnlyParam) {
args = append(args, "'--ff-only'")
}
if apr.Contains(cli.ForceFlag) {
args = append(args, "'--force'")
}
if apr.Contains(cli.CommitFlag) {
args = append(args, "'--commit'")
}
if apr.Contains(cli.NoCommitFlag) {
args = append(args, "'--no-commit'")
}
if apr.Contains(cli.NoEditFlag) {
args = append(args, "'--no-edit'")
}
if apr.Contains(cli.RebaseParam) {
args = append(args, "'--rebase'")
}
if apr.Contains(cli.PruneFlag) {
args = append(args, "'--prune'")
}
if user, hasUser := apr.GetValue(cli.UserFlag); hasUser {
args = append(args, "'--user'")
args = append(args, "?")
params = append(params, user)
}
query := "call dolt_pull(" + strings.Join(args, ", ") + ")"
interpolatedQuery, err := dbr.InterpolateForDialect(query, params, dialect.MySQL)
if err != nil {
return "", err
}
return interpolatedQuery, nil
}
// getRemoteHashForPull gets the hash of the remote branch being merged in and the ref to the remote head
func getRemoteHashForPull(apr *argparser.ArgParseResults, sqlCtx *sql.Context, queryist cli.Queryist) (remoteHash, remoteRef string, err error) {
var remote, branch string
if apr.NArg() < 2 {
if apr.NArg() == 0 {
remote, err = getDefaultRemote(sqlCtx, queryist)
if err != nil {
return "", "", err
}
} else {
remote = apr.Args[0]
}
rows, err := cli.GetRowsForSql(queryist, sqlCtx, "select name from dolt_remote_branches")
if err != nil {
return "", "", err
}
for _, row := range rows {
ref := row[0].(string)
if ref == "remotes/"+remote+"/main" {
branch = "main"
break
}
}
if branch == "" {
ref := rows[0][0].(string)
branch = strings.TrimPrefix(ref, "remotes/"+remote+"/")
}
} else {
remote = apr.Args[0]
branch = apr.Args[1]
}
if remote == "" || branch == "" {
return "", "", errors.New("pull finished successfully but remote and/or branch provided is empty")
}
remoteHash, err = getHashOf(queryist, sqlCtx, remote+"/"+branch)
if err != nil {
return "", "", err
}
return remoteHash, remote + "/" + branch, nil
}
// getDefaultRemote gets the name of the default remote.
func getDefaultRemote(sqlCtx *sql.Context, queryist cli.Queryist) (string, error) {
rows, err := cli.GetRowsForSql(queryist, sqlCtx, "select name from dolt_remotes")
if err != nil {
return "", err
}
if len(rows) == 0 {
return "", env.ErrNoRemote
}
if len(rows) == 1 {
return rows[0][0].(string), nil
}
for _, row := range rows {
if row[0].(string) == "origin" {
return "origin", nil
}
}
return rows[0][0].(string), nil
}