Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit 855acd2

Browse files
committed
Create merge-base feature
Signed-off-by: David Pordomingo <[email protected]>
1 parent 923642a commit 855acd2

File tree

4 files changed

+940
-0
lines changed

4 files changed

+940
-0
lines changed

merge_base.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package git
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"gopkg.in/src-d/go-git.v4/plumbing"
8+
"gopkg.in/src-d/go-git.v4/plumbing/object"
9+
"gopkg.in/src-d/go-git.v4/plumbing/storer"
10+
)
11+
12+
// errIsReachable is thrown when first commit is an ancestor of the second
13+
var errIsReachable = fmt.Errorf("first is reachable from second")
14+
15+
// MergeBase mimics the behavior of `git merge-base first second`, returning the
16+
// best common ancestor of the two passed commits
17+
// The best common ancestors can not be reached from other common ancestors
18+
func MergeBase(
19+
first *object.Commit,
20+
second *object.Commit,
21+
) ([]*object.Commit, error) {
22+
23+
// use sortedByCommitDateDesc strategy
24+
sorted := sortByCommitDateDesc(first, second)
25+
newer := sorted[0]
26+
older := sorted[1]
27+
28+
newerHistory, err := ancestorsIndex(older, newer)
29+
if err == errIsReachable {
30+
return []*object.Commit{older}, nil
31+
}
32+
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
var res []*object.Commit
38+
inNewerHistory := isInIndexCommitFilter(newerHistory)
39+
resIter := object.NewFilterCommitIter(older, &inNewerHistory, &inNewerHistory)
40+
err = resIter.ForEach(func(commit *object.Commit) error {
41+
res = append(res, commit)
42+
return nil
43+
})
44+
45+
return Independents(res)
46+
}
47+
48+
// IsAncestor returns true if the candidate commit is ancestor of the target one
49+
// It returns an error if the history is not transversable
50+
// It mimics the behavior of `git merge --is-ancestor candidate target`
51+
func IsAncestor(
52+
candidate *object.Commit,
53+
target *object.Commit,
54+
) (bool, error) {
55+
_, err := ancestorsIndex(candidate, target)
56+
if err == errIsReachable {
57+
return true, nil
58+
}
59+
60+
return false, nil
61+
}
62+
63+
// ancestorsIndex returns a map with the ancestors of the starting commit if the
64+
// excluded one is not one of them. It returns errIsReachable if the excluded commit
65+
// is ancestor of the starting, or another error if the history is not transversable.
66+
func ancestorsIndex(excluded, starting *object.Commit) (map[plumbing.Hash]struct{}, error) {
67+
if excluded.Hash.String() == starting.Hash.String() {
68+
return nil, errIsReachable
69+
}
70+
71+
startingHistory := map[plumbing.Hash]struct{}{}
72+
startingIter := object.NewCommitIterBSF(starting, nil, nil)
73+
err := startingIter.ForEach(func(commit *object.Commit) error {
74+
if commit.Hash == excluded.Hash {
75+
return errIsReachable
76+
}
77+
78+
startingHistory[commit.Hash] = struct{}{}
79+
return nil
80+
})
81+
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
return startingHistory, nil
87+
}
88+
89+
// Independents returns a subset of the passed commits, that are not reachable the others
90+
// It mimics the behavior of `git merge-base --independent commit...`.
91+
func Independents(commits []*object.Commit) ([]*object.Commit, error) {
92+
// use sortedByCommitDateDesc strategy
93+
cleaned := sortByCommitDateDesc(commits...)
94+
cleaned = removeDuplicated(cleaned)
95+
return independents(cleaned, map[plumbing.Hash]bool{}, 0)
96+
}
97+
98+
func independents(
99+
candidates []*object.Commit,
100+
excluded map[plumbing.Hash]bool,
101+
start int,
102+
) ([]*object.Commit, error) {
103+
if len(candidates) == 1 {
104+
return candidates, nil
105+
}
106+
107+
res := candidates
108+
for i := start; i < len(candidates); i++ {
109+
from := candidates[i]
110+
others := remove(res, from)
111+
fromHistoryIter := object.NewCommitIterBSF(from, excluded, nil)
112+
err := fromHistoryIter.ForEach(func(fromAncestor *object.Commit) error {
113+
for _, other := range others {
114+
if fromAncestor.Hash == other.Hash {
115+
res = remove(res, other)
116+
others = remove(others, other)
117+
}
118+
}
119+
120+
if len(res) == 1 {
121+
return storer.ErrStop
122+
}
123+
124+
excluded[fromAncestor.Hash] = true
125+
return nil
126+
})
127+
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
if len(res) < len(candidates) {
133+
return independents(res, excluded, indexOf(res, from)+1)
134+
}
135+
136+
}
137+
138+
return res, nil
139+
}
140+
141+
// sortByCommitDateDesc returns the passed commits, sorted by `committer.When desc`
142+
//
143+
// Following this strategy, it is tried to reduce the time needed when walking
144+
// the history from one commit to reach the others. It is assumed that ancestors
145+
// use to be committed before its descendant;
146+
// That way `Independents(A^, A)` will be processed as being `Independents(A, A^)`;
147+
// so starting by `A` it will be reached `A^` way sooner than walking from `A^`
148+
// to the initial commit, and then from `A` to `A^`.
149+
func sortByCommitDateDesc(commits ...*object.Commit) []*object.Commit {
150+
sorted := make([]*object.Commit, len(commits))
151+
copy(sorted, commits)
152+
sort.Slice(sorted, func(i, j int) bool {
153+
return sorted[i].Committer.When.After(sorted[j].Committer.When)
154+
})
155+
156+
return sorted
157+
}
158+
159+
// indexOf returns the first position where target was found in the passed commits
160+
func indexOf(commits []*object.Commit, target *object.Commit) int {
161+
for i, commit := range commits {
162+
if target.Hash == commit.Hash {
163+
return i
164+
}
165+
}
166+
167+
return -1
168+
}
169+
170+
// remove returns the passed commits excluding the commit toDelete
171+
func remove(commits []*object.Commit, toDelete *object.Commit) []*object.Commit {
172+
res := make([]*object.Commit, len(commits))
173+
j := 0
174+
for _, commit := range commits {
175+
if commit.Hash == toDelete.Hash {
176+
continue
177+
}
178+
179+
res[j] = commit
180+
j++
181+
}
182+
183+
return res[:j]
184+
}
185+
186+
// removeDuplicated removes duplicated commits from the passed slice of commits
187+
func removeDuplicated(commits []*object.Commit) []*object.Commit {
188+
seen := make(map[plumbing.Hash]struct{}, len(commits))
189+
res := make([]*object.Commit, len(commits))
190+
j := 0
191+
for _, commit := range commits {
192+
if _, ok := seen[commit.Hash]; ok {
193+
continue
194+
}
195+
196+
seen[commit.Hash] = struct{}{}
197+
res[j] = commit
198+
j++
199+
}
200+
201+
return res[:j]
202+
}
203+
204+
// isInIndexCommitFilter returns a commitFilter that returns true
205+
// if the commit is in the passed index.
206+
func isInIndexCommitFilter(index map[plumbing.Hash]struct{}) object.CommitFilter {
207+
return func(c *object.Commit) bool {
208+
_, ok := index[c.Hash]
209+
return ok
210+
}
211+
}

0 commit comments

Comments
 (0)