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 }