414 lines
14 KiB
Go
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
|
|
}
|