github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/env/actions/branch.go (about)

     1  // Copyright 2019 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package actions
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  
    22  	errorKinds "gopkg.in/src-d/go-errors.v1"
    23  
    24  	"github.com/dolthub/dolt/go/libraries/doltcore/branch_control"
    25  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/env"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/ref"
    28  	"github.com/dolthub/dolt/go/store/hash"
    29  )
    30  
    31  var ErrAlreadyExists = errors.New("already exists")
    32  var ErrCOBranchDelete = errorKinds.NewKind("Cannot delete checked out branch '%s'")
    33  var ErrUnmergedBranch = errorKinds.NewKind("branch '%s' is not fully merged")
    34  var ErrWorkingSetsOnBothBranches = errors.New("checkout would overwrite uncommitted changes on target branch")
    35  
    36  func RenameBranch(ctx context.Context, dbData env.DbData, oldBranch, newBranch string, remoteDbPro env.RemoteDbProvider, force bool, rsc *doltdb.ReplicationStatusController) error {
    37  	oldRef := ref.NewBranchRef(oldBranch)
    38  	newRef := ref.NewBranchRef(newBranch)
    39  
    40  	// TODO: This function smears the branch updates across multiple commits of the datas.Database.
    41  
    42  	err := CopyBranchOnDB(ctx, dbData.Ddb, oldBranch, newBranch, force, rsc)
    43  	if err != nil {
    44  		return err
    45  	}
    46  
    47  	fromWSRef, err := ref.WorkingSetRefForHead(oldRef)
    48  	if err != nil {
    49  		if !errors.Is(err, ref.ErrWorkingSetUnsupported) {
    50  			return err
    51  		}
    52  	} else {
    53  		toWSRef, err := ref.WorkingSetRefForHead(newRef)
    54  		if err != nil {
    55  			return err
    56  		}
    57  		// We always `force` here, because the CopyBranch up
    58  		// above created a new branch and it will have a
    59  		// working set.
    60  		err = dbData.Ddb.CopyWorkingSet(ctx, fromWSRef, toWSRef, true /* force */)
    61  		if err != nil {
    62  			return err
    63  		}
    64  	}
    65  
    66  	return DeleteBranch(ctx, dbData, oldBranch, DeleteOptions{Force: true, AllowDeletingCurrentBranch: true}, remoteDbPro, rsc)
    67  }
    68  
    69  func CopyBranch(ctx context.Context, dEnv *env.DoltEnv, oldBranch, newBranch string, force bool) error {
    70  	return CopyBranchOnDB(ctx, dEnv.DoltDB, oldBranch, newBranch, force, nil)
    71  }
    72  
    73  func CopyBranchOnDB(ctx context.Context, ddb *doltdb.DoltDB, oldBranch, newBranch string, force bool, rsc *doltdb.ReplicationStatusController) error {
    74  	oldRef := ref.NewBranchRef(oldBranch)
    75  	newRef := ref.NewBranchRef(newBranch)
    76  
    77  	hasOld, oldErr := ddb.HasRef(ctx, oldRef)
    78  
    79  	if oldErr != nil {
    80  		return oldErr
    81  	}
    82  
    83  	hasNew, newErr := ddb.HasRef(ctx, newRef)
    84  
    85  	if newErr != nil {
    86  		return newErr
    87  	}
    88  
    89  	if !hasOld {
    90  		return doltdb.ErrBranchNotFound
    91  	} else if !force && hasNew {
    92  		return ErrAlreadyExists
    93  	} else if !doltdb.IsValidUserBranchName(newBranch) {
    94  		return doltdb.ErrInvBranchName
    95  	}
    96  
    97  	cs, _ := doltdb.NewCommitSpec(oldBranch)
    98  	cm, err := ddb.Resolve(ctx, cs, nil)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	commit, ok := cm.ToCommit()
   104  	if !ok {
   105  		return doltdb.ErrGhostCommitEncountered
   106  	}
   107  	return ddb.NewBranchAtCommit(ctx, newRef, commit, rsc)
   108  }
   109  
   110  type DeleteOptions struct {
   111  	Force                      bool
   112  	Remote                     bool
   113  	AllowDeletingCurrentBranch bool
   114  }
   115  
   116  func DeleteBranch(ctx context.Context, dbData env.DbData, brName string, opts DeleteOptions, remoteDbPro env.RemoteDbProvider, rsc *doltdb.ReplicationStatusController) error {
   117  	var branchRef ref.DoltRef
   118  	if opts.Remote {
   119  		var err error
   120  		branchRef, err = ref.NewRemoteRefFromPathStr(brName)
   121  		if err != nil {
   122  			return err
   123  		}
   124  	} else {
   125  		branchRef = ref.NewBranchRef(brName)
   126  		headRef, err := dbData.Rsr.CWBHeadRef()
   127  		if err != nil {
   128  			return err
   129  		}
   130  		if !opts.AllowDeletingCurrentBranch && ref.Equals(headRef, branchRef) {
   131  			return ErrCOBranchDelete.New(brName)
   132  		}
   133  	}
   134  
   135  	return DeleteBranchOnDB(ctx, dbData, branchRef, opts, remoteDbPro, rsc)
   136  }
   137  
   138  func DeleteBranchOnDB(ctx context.Context, dbdata env.DbData, branchRef ref.DoltRef, opts DeleteOptions, pro env.RemoteDbProvider, rsc *doltdb.ReplicationStatusController) error {
   139  	ddb := dbdata.Ddb
   140  	hasRef, err := ddb.HasRef(ctx, branchRef)
   141  
   142  	if err != nil {
   143  		return err
   144  	} else if !hasRef {
   145  		return doltdb.ErrBranchNotFound
   146  	}
   147  
   148  	if !opts.Force && !opts.Remote {
   149  		// check to see if the branch is fully merged into its parent
   150  		trackedBranches, err := dbdata.Rsr.GetBranches()
   151  		if err != nil {
   152  			return err
   153  		}
   154  
   155  		trackedBranch, hasUpstream := trackedBranches.Get(branchRef.GetPath())
   156  		if hasUpstream {
   157  			err = validateBranchMergedIntoUpstream(ctx, dbdata, branchRef, trackedBranch.Remote, pro)
   158  			if err != nil {
   159  				return err
   160  			}
   161  		} else {
   162  			err = validateBranchMergedIntoCurrentWorkingBranch(ctx, dbdata, branchRef)
   163  			if err != nil {
   164  				return err
   165  			}
   166  		}
   167  	}
   168  
   169  	wsRef, err := ref.WorkingSetRefForHead(branchRef)
   170  	if err != nil {
   171  		if !errors.Is(err, ref.ErrWorkingSetUnsupported) {
   172  			return err
   173  		}
   174  	} else {
   175  		err = ddb.DeleteWorkingSet(ctx, wsRef)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	return ddb.DeleteBranch(ctx, branchRef, rsc)
   182  }
   183  
   184  // validateBranchMergedIntoCurrentWorkingBranch returns an error if the given branch is not fully merged into the HEAD of the current branch.
   185  func validateBranchMergedIntoCurrentWorkingBranch(ctx context.Context, dbdata env.DbData, branch ref.DoltRef) error {
   186  	branchSpec, err := doltdb.NewCommitSpec(branch.GetPath())
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	optCmt, err := dbdata.Ddb.Resolve(ctx, branchSpec, nil)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	branchHead, ok := optCmt.ToCommit()
   196  	if !ok {
   197  		return doltdb.ErrGhostCommitEncountered
   198  	}
   199  
   200  	cwbCs, err := doltdb.NewCommitSpec("HEAD")
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	headRef, err := dbdata.Rsr.CWBHeadRef()
   206  	if err != nil {
   207  		return err
   208  	}
   209  	optCmt, err = dbdata.Ddb.Resolve(ctx, cwbCs, headRef)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	cwbHead, ok := optCmt.ToCommit()
   214  	if !ok {
   215  		return doltdb.ErrGhostCommitEncountered
   216  	}
   217  
   218  	isMerged, err := branchHead.CanFastForwardTo(ctx, cwbHead)
   219  	if err != nil {
   220  		if errors.Is(err, doltdb.ErrUpToDate) {
   221  			return nil
   222  		}
   223  		if errors.Is(err, doltdb.ErrIsAhead) {
   224  			return ErrUnmergedBranch.New(branch.GetPath())
   225  		}
   226  
   227  		return err
   228  	}
   229  
   230  	if !isMerged {
   231  		return ErrUnmergedBranch.New(branch.GetPath())
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  // validateBranchMergedIntoUpstream returns an error if the branch provided is not fully merged into its upstream
   238  func validateBranchMergedIntoUpstream(ctx context.Context, dbdata env.DbData, branch ref.DoltRef, remoteName string, pro env.RemoteDbProvider) error {
   239  	remotes, err := dbdata.Rsr.GetRemotes()
   240  	if err != nil {
   241  		return err
   242  	}
   243  	remote, ok := remotes.Get(remoteName)
   244  	if !ok {
   245  		// TODO: skip error?
   246  		return fmt.Errorf("remote %s not found", remoteName)
   247  	}
   248  
   249  	remoteDb, err := pro.GetRemoteDB(ctx, dbdata.Ddb.ValueReadWriter().Format(), remote, false)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	cs, err := doltdb.NewCommitSpec(branch.GetPath())
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	optCmt, err := remoteDb.Resolve(ctx, cs, nil)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	remoteBranchHead, ok := optCmt.ToCommit()
   264  	if !ok {
   265  		return doltdb.ErrGhostCommitEncountered
   266  	}
   267  
   268  	optCmt, err = dbdata.Ddb.Resolve(ctx, cs, nil)
   269  	if err != nil {
   270  		return err
   271  	}
   272  	localBranchHead, ok := optCmt.ToCommit()
   273  	if !ok {
   274  		return doltdb.ErrGhostCommitEncountered
   275  	}
   276  
   277  	canFF, err := localBranchHead.CanFastForwardTo(ctx, remoteBranchHead)
   278  	if err != nil {
   279  		if errors.Is(err, doltdb.ErrUpToDate) {
   280  			return nil
   281  		}
   282  		if errors.Is(err, doltdb.ErrIsAhead) {
   283  			return ErrUnmergedBranch.New(branch.GetPath())
   284  		}
   285  		return err
   286  	}
   287  
   288  	if !canFF {
   289  		return ErrUnmergedBranch.New(branch.GetPath())
   290  	}
   291  
   292  	return nil
   293  }
   294  
   295  func CreateBranchWithStartPt(ctx context.Context, dbData env.DbData, newBranch, startPt string, force bool, rsc *doltdb.ReplicationStatusController) error {
   296  	err := createBranch(ctx, dbData, newBranch, startPt, force, rsc)
   297  
   298  	if err != nil {
   299  		if err == ErrAlreadyExists {
   300  			return fmt.Errorf("fatal: A branch named '%s' already exists.", newBranch)
   301  		} else if err == doltdb.ErrInvBranchName {
   302  			return fmt.Errorf("fatal: '%s' is an invalid branch name.", newBranch)
   303  		} else if err == doltdb.ErrInvHash || doltdb.IsNotACommit(err) {
   304  			return fmt.Errorf("fatal: '%s' is not a commit and a branch '%s' cannot be created from it", startPt, newBranch)
   305  		} else {
   306  			return fmt.Errorf("fatal: Unexpected error creating branch '%s' : %v", newBranch, err)
   307  		}
   308  	}
   309  	err = branch_control.AddAdminForContext(ctx, newBranch)
   310  	if err != nil {
   311  		return err
   312  	}
   313  
   314  	return nil
   315  }
   316  
   317  func CreateBranchOnDB(ctx context.Context, ddb *doltdb.DoltDB, newBranch, startingPoint string, force bool, headRef ref.DoltRef, rsc *doltdb.ReplicationStatusController) error {
   318  	branchRef := ref.NewBranchRef(newBranch)
   319  	hasRef, err := ddb.HasRef(ctx, branchRef)
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	if !force && hasRef {
   325  		return ErrAlreadyExists
   326  	}
   327  
   328  	if !doltdb.IsValidUserBranchName(newBranch) {
   329  		return doltdb.ErrInvBranchName
   330  	}
   331  
   332  	cs, err := doltdb.NewCommitSpec(startingPoint)
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	optCmt, err := ddb.Resolve(ctx, cs, headRef)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	cm, ok := optCmt.ToCommit()
   343  	if !ok {
   344  		return doltdb.ErrGhostCommitEncountered
   345  	}
   346  
   347  	err = ddb.NewBranchAtCommit(ctx, branchRef, cm, rsc)
   348  	if err != nil {
   349  		return err
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  func createBranch(ctx context.Context, dbData env.DbData, newBranch, startingPoint string, force bool, rsc *doltdb.ReplicationStatusController) error {
   356  	headRef, err := dbData.Rsr.CWBHeadRef()
   357  	if err != nil {
   358  		return err
   359  	}
   360  	return CreateBranchOnDB(ctx, dbData.Ddb, newBranch, startingPoint, force, headRef, rsc)
   361  }
   362  
   363  var emptyHash = hash.Hash{}
   364  
   365  func IsBranch(ctx context.Context, ddb *doltdb.DoltDB, str string) (bool, error) {
   366  	return IsBranchOnDB(ctx, ddb, str)
   367  }
   368  
   369  func IsBranchOnDB(ctx context.Context, ddb *doltdb.DoltDB, str string) (bool, error) {
   370  	dref := ref.NewBranchRef(str)
   371  	return ddb.HasRef(ctx, dref)
   372  }
   373  
   374  func MaybeGetCommit(ctx context.Context, dEnv *env.DoltEnv, str string) (*doltdb.Commit, error) {
   375  	cs, err := doltdb.NewCommitSpec(str)
   376  
   377  	if err == nil {
   378  		headRef, err := dEnv.RepoStateReader().CWBHeadRef()
   379  		if err != nil {
   380  			return nil, err
   381  		}
   382  		optCmt, err := dEnv.DoltDB.Resolve(ctx, cs, headRef)
   383  		if err != nil && errors.Is(err, doltdb.ErrBranchNotFound) {
   384  			return nil, nil
   385  		}
   386  		if err != nil && errors.Is(err, doltdb.ErrHashNotFound) {
   387  			return nil, nil
   388  		}
   389  		if err != nil {
   390  			return nil, err
   391  		}
   392  
   393  		cm, ok := optCmt.ToCommit()
   394  		if ok {
   395  			return cm, nil
   396  		}
   397  	}
   398  
   399  	return nil, nil
   400  }