github.com/samcontesse/bitbucket-cascade-merge@v0.0.0-20230227091349-c5ec053235b5/git.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/libgit2/git2go/v34" 7 "strings" 8 "time" 9 ) 10 11 const ( 12 DefaultMaster = "master" 13 DefaultRemoteName = "origin" 14 DefaultRemoteReferencePrefix = "refs/heads/" 15 DefaultCommitReferenceName = "HEAD" 16 ) 17 18 type Client struct { 19 Repository *git.Repository 20 RemoteCallbacks git.RemoteCallbacks 21 Author *Author 22 } 23 24 type Credentials struct { 25 Username string 26 Password string 27 } 28 29 type ClientOptions struct { 30 Path string 31 URL string 32 Author *Author 33 Credentials *Credentials 34 } 35 36 func (c *Client) CascadeMerge(branchName string, options *CascadeOptions) *CascadeMergeState { 37 38 if options == nil { 39 options = &CascadeOptions{ 40 DevelopmentName: "develop", 41 ReleasePrefix: "release/", 42 } 43 } 44 45 err := c.RemoveLocalBranches() 46 if err != nil { 47 return &CascadeMergeState{error: err} 48 } 49 50 err = c.Fetch() 51 if err != nil { 52 return &CascadeMergeState{error: err} 53 } 54 55 cascade, err := c.BuildCascade(options, branchName) 56 if err != nil { 57 return &CascadeMergeState{error: err} 58 } 59 60 source := branchName 61 62 err = c.Checkout(source) 63 if err != nil { 64 return &CascadeMergeState{error: err} 65 } 66 67 err = c.Reset(source) 68 if err != nil { 69 return &CascadeMergeState{error: err} 70 } 71 72 for target := cascade.Next(); target != ""; target = cascade.Next() { 73 err = c.Checkout(target) 74 if err != nil { 75 return &CascadeMergeState{Source: source, Target: target, error: err} 76 } 77 78 err = c.Reset(target) 79 if err != nil { 80 return &CascadeMergeState{Source: source, Target: target, error: err} 81 } 82 83 err = c.MergeBranches(source, target) 84 if err != nil { 85 return &CascadeMergeState{Source: source, Target: target, error: err} 86 } 87 88 err := c.Push(target) 89 if err != nil { 90 return &CascadeMergeState{Source: source, Target: target, error: err} 91 } 92 93 source = target 94 } 95 96 return nil 97 } 98 99 func (c *Client) Commit(message string, path ...string) (*git.Oid, error) { 100 index, err := c.Repository.Index() 101 if err != nil { 102 return nil, err 103 } 104 defer index.Free() 105 106 var parent *git.Commit 107 head, _ := c.Repository.Head() 108 if head != nil { 109 parent, err = c.Repository.LookupCommit(head.Target()) 110 if err != nil { 111 return nil, err 112 } 113 defer parent.Free() 114 defer head.Free() 115 } 116 117 for _, p := range path { 118 err = index.AddByPath(p) 119 if err != nil { 120 return nil, err 121 } 122 } 123 124 oid, err := index.WriteTree() 125 if err != nil { 126 return nil, err 127 } 128 129 err = index.Write() 130 if err != nil { 131 return nil, err 132 } 133 134 tree, err := c.Repository.LookupTree(oid) 135 if err != nil { 136 return nil, err 137 } 138 defer tree.Free() 139 140 signature := &git.Signature{ 141 Name: c.Author.Name, 142 Email: c.Author.Email, 143 When: time.Now(), 144 } 145 146 if parent != nil { 147 return c.Repository.CreateCommit(DefaultCommitReferenceName, signature, signature, message, tree, parent) 148 } else { 149 return c.Repository.CreateCommit(DefaultCommitReferenceName, signature, signature, message, tree) 150 } 151 } 152 153 func (c *Client) Checkout(branchName string) error { 154 checkoutOpts := &git.CheckoutOpts{ 155 Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutAllowConflicts | git.CheckoutUseTheirs, 156 } 157 158 var commit *git.Commit 159 remoteBranch, err := c.Repository.LookupBranch(DefaultRemoteName+"/"+branchName, git.BranchRemote) 160 if remoteBranch != nil { 161 // read remote branch commit 162 commit, err = c.Repository.LookupCommit(remoteBranch.Target()) 163 if err != nil { 164 return err 165 } 166 defer commit.Free() 167 defer remoteBranch.Free() 168 } else { 169 // read head commit 170 head, _ := c.Repository.Head() 171 if head != nil { 172 commit, err = c.Repository.LookupCommit(head.Target()) 173 if err != nil { 174 return err 175 } 176 defer commit.Free() 177 defer head.Free() 178 } 179 } 180 181 localBranch, _ := c.Repository.LookupBranch(branchName, git.BranchLocal) 182 if localBranch == nil { 183 // creating local branch 184 localBranch, err = c.Repository.CreateBranch(branchName, commit, false) 185 if err != nil { 186 return err 187 } 188 189 // setting upstream to origin branch 190 if remoteBranch != nil { 191 err = localBranch.SetUpstream(DefaultRemoteName + "/" + branchName) 192 if err != nil { 193 return err 194 } 195 } 196 } 197 if localBranch == nil { 198 return errors.New("error while locating/creating local branch") 199 } 200 defer localBranch.Free() 201 202 // getting the tree for the branch 203 localCommit, err := c.Repository.LookupCommit(localBranch.Target()) 204 if err != nil { 205 return err 206 } 207 defer localCommit.Free() 208 209 tree, err := c.Repository.LookupTree(localCommit.TreeId()) 210 if err != nil { 211 return err 212 } 213 defer tree.Free() 214 215 // checkout the tree 216 err = c.Repository.CheckoutTree(tree, checkoutOpts) 217 if err != nil { 218 return err 219 } 220 // setting the Head to point to our branch 221 c.Repository.SetHead("refs/heads/" + branchName) 222 return nil 223 } 224 225 func (c *Client) Push(branchName string) error { 226 remote, err := c.Repository.Remotes.Lookup(DefaultRemoteName) 227 if err != nil { 228 return err 229 } 230 defer remote.Free() 231 232 err = remote.Push([]string{DefaultRemoteReferencePrefix + branchName}, &git.PushOptions{RemoteCallbacks: c.RemoteCallbacks}) 233 234 if err != nil { 235 return err 236 } 237 238 return nil 239 } 240 241 func (c *Client) Fetch() error { 242 remote, err := c.Repository.Remotes.Lookup(DefaultRemoteName) 243 if err != nil { 244 return err 245 } 246 defer remote.Free() 247 248 var refs []string 249 err = remote.Fetch(refs, &git.FetchOptions{RemoteCallbacks: c.RemoteCallbacks, Prune: git.FetchPruneOn}, "") 250 251 if err != nil { 252 return err 253 } 254 255 return nil 256 } 257 258 // Reset current HEAD to the remote branch 259 func (c *Client) Reset(branchName string) error { 260 branch, err := c.Repository.LookupBranch(fmt.Sprintf("%s/%s", DefaultRemoteName, branchName), git.BranchRemote) 261 if err != nil { 262 return err 263 } 264 defer branch.Free() 265 266 commit, err := c.Repository.LookupCommit(branch.Target()) 267 if err != nil { 268 return err 269 } 270 defer commit.Free() 271 272 err = c.Repository.ResetToCommit(commit, git.ResetHard, &git.CheckoutOpts{}) 273 if err != nil { 274 return err 275 } 276 277 return nil 278 } 279 280 func (c *Client) BuildCascade(options *CascadeOptions, startBranch string) (*Cascade, error) { 281 cascade := Cascade{ 282 Branches: make([]string, 0), 283 Current: 0, 284 } 285 286 iterator, err := c.Repository.NewBranchIterator(git.BranchRemote) 287 if err != nil { 288 return nil, err 289 } 290 291 iterator.ForEach(func(branch *git.Branch, branchType git.BranchType) error { 292 shorthand := branch.Shorthand() 293 branchName := strings.TrimPrefix(shorthand, DefaultRemoteName+"/") 294 if branchName == options.DevelopmentName || strings.HasPrefix(branchName, options.ReleasePrefix) { 295 cascade.Append(branchName) 296 } 297 return nil 298 }) 299 300 cascade.Slice(startBranch) 301 302 return &cascade, nil 303 } 304 305 func (c *Client) MergeBranches(sourceBranchName string, destinationBranchName string) error { 306 // assuming that these two branches are local already 307 sourceBranch, err := c.Repository.LookupBranch(sourceBranchName, git.BranchLocal) 308 if err != nil { 309 return err 310 } 311 defer sourceBranch.Free() 312 313 destinationBranch, err := c.Repository.LookupBranch(destinationBranchName, git.BranchLocal) 314 if err != nil { 315 return err 316 } 317 defer destinationBranch.Free() 318 319 // assuming we are already checkout as the destination branch 320 sourceAnnCommit, err := c.Repository.AnnotatedCommitFromRef(sourceBranch.Reference) 321 if err != nil { 322 return err 323 } 324 defer sourceAnnCommit.Free() 325 326 // getting repo head 327 head, err := c.Repository.Head() 328 if err != nil { 329 return err 330 } 331 332 // do merge analysis 333 mergeHeads := make([]*git.AnnotatedCommit, 1) 334 mergeHeads[0] = sourceAnnCommit 335 analysis, _, err := c.Repository.MergeAnalysis(mergeHeads) 336 337 // branches are already merged? 338 if analysis&git.MergeAnalysisNone != 0 || analysis&git.MergeAnalysisUpToDate != 0 { 339 return nil 340 } 341 342 // should merge 343 if analysis&git.MergeAnalysisNormal == 0 { 344 return errors.New("merge analysis returned as not normal merge") 345 } 346 347 // options for merge 348 mergeOpts, _ := git.DefaultMergeOptions() 349 mergeOpts.FileFavor = git.MergeFileFavorNormal 350 mergeOpts.TreeFlags = git.MergeTreeFailOnConflict 351 352 // options for checkout 353 checkoutOpts := &git.CheckoutOpts{ 354 Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutUseTheirs, 355 } 356 357 // merge action 358 if err = c.Repository.Merge(mergeHeads, &mergeOpts, checkoutOpts); err != nil { 359 return err 360 } 361 362 // getting repo index 363 index, err := c.Repository.Index() 364 if err != nil { 365 return err 366 } 367 defer index.Free() 368 369 // checking for conflicts 370 if index.HasConflicts() { 371 return errors.New("merge resulted in conflicts, please solve the conflicts before merging") 372 } 373 374 // getting last commit from source 375 commit, err := c.Repository.LookupCommit(sourceBranch.Target()) 376 if err != nil { 377 return err 378 } 379 defer commit.Free() 380 381 // getting signature 382 signature := commit.Author() 383 384 // writing tree to index 385 treeId, err := index.WriteTree() 386 if err != nil { 387 return err 388 } 389 390 // getting the created tree 391 tree, err := c.Repository.LookupTree(treeId) 392 if err != nil { 393 return err 394 } 395 defer tree.Free() 396 397 // getting head's commit 398 currentDestinationCommit, err := c.Repository.LookupCommit(head.Target()) 399 if err != nil { 400 return err 401 } 402 403 // commit 404 _, err = c.Repository.CreateCommit(DefaultCommitReferenceName, signature, signature, "Automatic merge "+sourceBranchName+" into "+destinationBranchName, 405 tree, currentDestinationCommit, commit) 406 if err != nil { 407 return err 408 } 409 410 err = c.Repository.StateCleanup() 411 if err != nil { 412 return err 413 } 414 415 return nil 416 } 417 418 func (c *Client) RemoveLocalBranches() error { 419 iterator, err := c.Repository.NewBranchIterator(git.BranchLocal) 420 if err != nil { 421 return err 422 } 423 424 iterator.ForEach(func(branch *git.Branch, branchType git.BranchType) error { 425 if DefaultMaster != branch.Shorthand() { 426 err = branch.Delete() 427 if err != nil { 428 return err 429 } 430 } 431 return nil 432 }) 433 434 return nil 435 } 436 437 func (c *Client) Close() { 438 c.Repository.Free() 439 } 440 441 func NewClient(options *ClientOptions) (*Client, error) { 442 443 if options == nil || !options.Validate() { 444 return nil, errors.New("invalid client options") 445 } 446 447 var r *git.Repository 448 var cb git.RemoteCallbacks 449 var err error 450 451 // try to open an existing repository 452 r, err = git.OpenRepository(options.Path) 453 454 // create fetch options (credentials callback) 455 cb = options.CreateRemoteCallbacks() 456 457 if err != nil { 458 // try clone the given url with the given credentials 459 r, err = git.Clone(options.URL, options.Path, &git.CloneOptions{FetchOptions: git.FetchOptions{RemoteCallbacks: cb}}) 460 if err != nil { 461 return nil, fmt.Errorf("cannot initialize repository at %s : %s", options.URL, err) 462 } 463 } 464 465 if r == nil { 466 return nil, errors.New("error while initializing repository") 467 } 468 469 return &Client{ 470 Repository: r, 471 RemoteCallbacks: cb, 472 Author: options.Author, 473 }, nil 474 475 } 476 477 func (o *ClientOptions) Validate() bool { 478 if len(o.URL) > 0 && len(o.Path) > 0 { 479 return true 480 } 481 return false 482 } 483 484 func (o *ClientOptions) CreateRemoteCallbacks() git.RemoteCallbacks { 485 if c := o.Credentials; c != nil { 486 return git.RemoteCallbacks{ 487 CredentialsCallback: makeCredentialsCallback(c.Username, c.Password), 488 } 489 } 490 return git.RemoteCallbacks{} 491 } 492 493 func makeCredentialsCallback(username, password string) git.CredentialsCallback { 494 return func(url, u string, ct git.CredType) (*git.Cred, error) { 495 cred, err := git.NewCredUserpassPlaintext(username, password) 496 return cred, err 497 } 498 }