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 }