github.com/yourbase/yb@v0.7.1/cmd/yb/best_ancestor.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "gopkg.in/src-d/go-git.v4" 10 "gopkg.in/src-d/go-git.v4/plumbing" 11 "gopkg.in/src-d/go-git.v4/plumbing/object" 12 "gopkg.in/src-d/go-git.v4/plumbing/storer" 13 "zombiezen.com/go/log" 14 ) 15 16 func fastFindAncestor(ctx context.Context, r *git.Repository) (h plumbing.Hash, c int, branchName string, err error) { 17 /* 18 go doc gopkg.in/src-d/go-git.v4/plumbing/object Commit.MergeBase 19 20 func (c *Commit) MergeBase(other *Commit) ([]*Commit, error) 21 MergeBase mimics the behavior of `git merge-base actual other`, returning 22 the best common ancestor between the actual and the passed one. The best 23 common ancestors can not be reached from other common ancestors. 24 */ 25 c = -1 26 27 ref, err := r.Head() 28 if err != nil { 29 err = fmt.Errorf("find ancestor: %w", err) 30 return 31 } 32 33 remoteBranch := findRemoteBranch(ctx, ref, r) 34 branchName = ref.Name().Short() 35 if remoteBranch == nil { 36 if branchName == "master" { 37 // Well, we don't need to try it again 38 err = fmt.Errorf("find ancestor: unable to find remote master branch") 39 return 40 } 41 //Search again, on master 42 ref, err = r.Reference(plumbing.NewBranchReferenceName("master"), false) 43 if err != nil { 44 err = fmt.Errorf("find ancestor: %w", err) 45 return 46 } 47 remoteBranch = findRemoteBranch(ctx, ref, r) 48 branchName = ref.Name().Short() // "master" 49 } 50 51 headCommit, err := r.CommitObject(ref.Hash()) 52 if err != nil { 53 err = fmt.Errorf("find ancestor: %w", err) 54 return 55 } 56 remoteCommit, err := r.CommitObject(remoteBranch.Hash()) 57 if err != nil { 58 err = fmt.Errorf("find ancestor: %w", err) 59 return 60 } 61 commonAncestors, err := remoteCommit.MergeBase(headCommit) 62 if err != nil { 63 err = fmt.Errorf("find ancestor: %w", err) 64 return 65 } 66 67 for i, ancestor := range commonAncestors { 68 log.Infof(ctx, "Merge-base commit #%d '%v': %v", i, ancestor.Hash.String(), strings.ReplaceAll(fmt.Sprintf("%12s (...)", ancestor.Message), "\n", " ")) 69 } 70 // For now we'll return the first one 71 if len(commonAncestors) > 0 { 72 h = commonAncestors[0].Hash 73 } 74 75 // Show remote name from branch complete name 76 remoteName := remoteBranch.Name().String() 77 remoteRefRegexp := regexp.MustCompile(`refs/remotes/(\w+)/(\w+)`) 78 79 if remoteRefRegexp.MatchString(remoteName) { 80 submatches := remoteRefRegexp.FindStringSubmatch(remoteName) 81 82 if len(submatches) > 1 { 83 remoteInfo := submatches[1] 84 if config, err := r.Config(); err == nil { 85 remoteInfo = fmt.Sprintf("\"%s [ %s ]\"", remoteInfo, config.Remotes[submatches[1]].URLs[0]) 86 } 87 log.Infof(ctx, "Remote found for branch '%v': %v", branchName, remoteInfo) 88 } else { 89 log.Errorf(ctx, "Unable to parse remote branch '%v'", branchName) 90 } 91 } 92 93 // Count commits between head and 'h' 94 commitIter, err := r.Log(&git.LogOptions{}) 95 if err != nil { 96 err = fmt.Errorf("find ancestor: %w", err) 97 return 98 } 99 x := 0 100 err = commitIter.ForEach(func(cmt *object.Commit) error { 101 x++ 102 if cmt.Hash.String() == h.String() { 103 // Stop here 104 c = x 105 return storer.ErrStop 106 } 107 return nil 108 }) 109 110 return 111 } 112 113 func findRemoteBranch(ctx context.Context, reference *plumbing.Reference, r *git.Repository) (remoteBranch *plumbing.Reference) { 114 branchIter, _ := r.References() 115 _ = branchIter.ForEach(func(rem *plumbing.Reference) error { 116 if rem.Name().IsRemote() { 117 log.Debugf(ctx, "Branch found: %v, %s ?== %s", rem, rem.Name().Short(), reference.Name().Short()) 118 if strings.HasSuffix(rem.Name().Short(), reference.Name().Short()) { 119 remoteBranch = rem 120 return storer.ErrStop 121 } 122 } 123 return nil 124 }) 125 return 126 }