github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dprocedures/dolt_checkout_helpers.go (about)

     1  // Copyright 2023 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 dprocedures
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  
    21  	"github.com/dolthub/go-mysql-server/sql"
    22  
    23  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    24  	"github.com/dolthub/dolt/go/libraries/doltcore/env"
    25  	"github.com/dolthub/dolt/go/libraries/doltcore/env/actions"
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/ref"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
    28  )
    29  
    30  // MoveWorkingSetToBranch moves the working set from the currently checked out branch onto the branch specified
    31  // by `brName`. This is a POTENTIALLY DESTRUCTIVE ACTION used during command line checkout
    32  func MoveWorkingSetToBranch(ctx *sql.Context, brName string, force bool, isNewBranch bool) error {
    33  	branchRef := ref.NewBranchRef(brName)
    34  	dSess := dsess.DSessFromSess(ctx.Session)
    35  	dbName := dSess.GetCurrentDatabase()
    36  	headRef, err := dSess.CWBHeadRef(ctx, dbName)
    37  	if err != nil {
    38  		return err
    39  	}
    40  
    41  	db, hasDb := dSess.GetDoltDB(ctx, dbName)
    42  	if !hasDb {
    43  		return fmt.Errorf("unable to load database")
    44  	}
    45  
    46  	hasRef, err := db.HasRef(ctx, branchRef)
    47  	if err != nil {
    48  		return err
    49  	}
    50  	if !hasRef {
    51  		return doltdb.ErrBranchNotFound
    52  	}
    53  
    54  	if ref.Equals(headRef, branchRef) {
    55  		return doltdb.ErrAlreadyOnBranch
    56  	}
    57  
    58  	branchHead, err := actions.BranchHeadRoot(ctx, db, brName)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	workingSetExists := true
    64  	initialWs, err := dSess.WorkingSet(ctx, dbName)
    65  	if err == doltdb.ErrWorkingSetNotFound {
    66  		// ignore, but don't reset the working set
    67  		workingSetExists = false
    68  	} else if err != nil {
    69  		return err
    70  	}
    71  
    72  	if !force {
    73  		currentRoots, hasRoots := dSess.GetRoots(ctx, dbName)
    74  		if !hasRoots {
    75  			return fmt.Errorf("unable to resolve roots for %s", dbName)
    76  		}
    77  		newBranchRoots, err := db.ResolveBranchRoots(ctx, branchRef)
    78  		if err != nil {
    79  			return err
    80  		}
    81  		if !isNewBranch {
    82  			wouldStomp, err := actions.CheckoutWouldStompWorkingSetChanges(ctx, currentRoots, newBranchRoots)
    83  			if err != nil {
    84  				return err
    85  			}
    86  			if wouldStomp {
    87  				return actions.ErrWorkingSetsOnBothBranches
    88  			}
    89  		}
    90  	}
    91  
    92  	initialRoots, hasRoots := dSess.GetRoots(ctx, dbName)
    93  	if !hasRoots {
    94  		return fmt.Errorf("unable to get roots")
    95  	}
    96  
    97  	// roots will be empty/nil if the working set is not set (working set is not set if the current branch was deleted)
    98  	if errors.Is(err, doltdb.ErrBranchNotFound) || errors.Is(err, doltdb.ErrWorkingSetNotFound) {
    99  		workingSetExists = false
   100  	} else if err != nil {
   101  		return err
   102  	}
   103  
   104  	hasChanges := false
   105  	if workingSetExists {
   106  		hasChanges, _, _, err = actions.RootHasUncommittedChanges(initialRoots)
   107  		if err != nil {
   108  			return err
   109  		}
   110  	}
   111  
   112  	dbData, ok := dSess.GetDbData(ctx, dbName)
   113  	if !ok {
   114  		return fmt.Errorf("Could not load database %s", dbName)
   115  	}
   116  
   117  	// Only if the current working set has uncommitted changes do we carry them forward to the branch being checked out.
   118  	// If this is the case, then the destination branch must *not* have any uncommitted changes, as checked by
   119  	// checkoutWouldStompWorkingSetChanges
   120  	if hasChanges {
   121  		err = transferWorkingChanges(ctx, dbName, initialRoots, branchHead, branchRef, force)
   122  		if err != nil {
   123  			return err
   124  		}
   125  	} else {
   126  		wsRef, err := ref.WorkingSetRefForHead(branchRef)
   127  		if err != nil {
   128  			return err
   129  		}
   130  
   131  		err = dSess.SwitchWorkingSet(ctx, dbName, wsRef)
   132  		if err != nil {
   133  			return err
   134  		}
   135  
   136  	}
   137  
   138  	if workingSetExists && hasChanges {
   139  		err = actions.CleanOldWorkingSet(ctx, dbData, db, dSess.Username(), dSess.Email(), initialRoots, headRef, initialWs)
   140  		if err != nil {
   141  			return err
   142  		}
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  // transferWorkingChanges computes new roots for `branchRef` by applying the changes from the staged and working sets
   149  // of `initialRoots` onto the branch head specified by `branchHead`. This is a DESTRUCTIVE ACTION used during command
   150  // line checkout, to move the working set changes onto a new branch.
   151  func transferWorkingChanges(
   152  	ctx *sql.Context,
   153  	dbName string,
   154  	initialRoots doltdb.Roots,
   155  	branchHead doltdb.RootValue,
   156  	branchRef ref.BranchRef,
   157  	force bool,
   158  ) error {
   159  	dSess := dsess.DSessFromSess(ctx.Session)
   160  
   161  	// Compute the new roots before switching the working set.
   162  	// This way, we don't leave the branch in a bad state in the event of an error.
   163  	newRoots, err := actions.RootsForBranch(ctx, initialRoots, branchHead, force)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	wsRef, err := ref.WorkingSetRefForHead(branchRef)
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	err = dSess.SwitchWorkingSet(ctx, dbName, wsRef)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	ws, err := dSess.WorkingSet(ctx, dbName)
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	newWs := ws.WithWorkingRoot(newRoots.Working).WithStagedRoot(newRoots.Staged)
   184  
   185  	err = dSess.SetWorkingSet(ctx, dbName, newWs)
   186  
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	return nil
   192  }
   193  
   194  // willModifyDb determines whether or not this operation is a no-op and can return early with a helpful message.
   195  func willModifyDb(dSess *dsess.DoltSession, data env.DbData, dbName, branchName string, updateHead bool) (bool, error) {
   196  	headRef, err := data.Rsr.CWBHeadRef()
   197  	// If we're in a detached head state, allow checking out a new branch.
   198  	if err == doltdb.ErrOperationNotSupportedInDetachedHead {
   199  		return true, nil
   200  	}
   201  	if err != nil {
   202  		return false, err
   203  	}
   204  
   205  	// If the operation won't modify either the active session or the default session, return early.
   206  	isModification := headRef.GetPath() != branchName
   207  	if updateHead {
   208  		fs, err := dSess.Provider().FileSystemForDatabase(dbName)
   209  		if err != nil {
   210  			return false, err
   211  		}
   212  		repoState, err := env.LoadRepoState(fs)
   213  		if err != nil {
   214  			return false, err
   215  		}
   216  		defaultBranch := repoState.CWBHeadRef().GetPath()
   217  		isModification = isModification || (defaultBranch != branchName)
   218  	}
   219  	return isModification, nil
   220  }
   221  
   222  func generateSuccessMessage(newBranch, upstream string) string {
   223  	result := fmt.Sprintf("Switched to branch '%s'", newBranch)
   224  	if upstream != "" {
   225  		result += fmt.Sprintf("\nbranch '%s' set up to track '%s'.", newBranch, upstream)
   226  	}
   227  	return result
   228  }