fork of go-git with some jj specific features
1package git
2
3import (
4 "io"
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/utils/diff"
10
11 "github.com/sergi/go-diff/diffmatchpatch"
12)
13
14// References returns a slice of Commits for the file at "path", starting from
15// the commit provided that contains the file from the provided path. The last
16// commit into the returned slice is the commit where the file was created.
17// If the provided commit does not contains the specified path, a nil slice is
18// returned. The commits are sorted in commit order, newer to older.
19//
20// Caveats:
21//
22// - Moves and copies are not currently supported.
23//
24// - Cherry-picks are not detected unless there are no commits between them and
25// therefore can appear repeated in the list. (see git path-id for hints on how
26// to fix this).
27func references(c *object.Commit, path string) ([]*object.Commit, error) {
28 var result []*object.Commit
29 seen := make(map[plumbing.Hash]struct{})
30 if err := walkGraph(&result, &seen, c, path); err != nil {
31 return nil, err
32 }
33
34 // TODO result should be returned without ordering
35 sortCommits(result)
36
37 // for merges of identical cherry-picks
38 return removeComp(path, result, equivalent)
39}
40
41type commitSorterer struct {
42 l []*object.Commit
43}
44
45func (s commitSorterer) Len() int {
46 return len(s.l)
47}
48
49func (s commitSorterer) Less(i, j int) bool {
50 return s.l[i].Committer.When.Before(s.l[j].Committer.When)
51}
52
53func (s commitSorterer) Swap(i, j int) {
54 s.l[i], s.l[j] = s.l[j], s.l[i]
55}
56
57// SortCommits sorts a commit list by commit date, from older to newer.
58func sortCommits(l []*object.Commit) {
59 s := &commitSorterer{l}
60 sort.Sort(s)
61}
62
63// Recursive traversal of the commit graph, generating a linear history of the
64// path.
65func walkGraph(result *[]*object.Commit, seen *map[plumbing.Hash]struct{}, current *object.Commit, path string) error {
66 // check and update seen
67 if _, ok := (*seen)[current.Hash]; ok {
68 return nil
69 }
70 (*seen)[current.Hash] = struct{}{}
71
72 // if the path is not in the current commit, stop searching.
73 if _, err := current.File(path); err != nil {
74 return nil
75 }
76
77 // optimization: don't traverse branches that does not
78 // contain the path.
79 parents, err := parentsContainingPath(path, current)
80 if err != nil {
81 return err
82 }
83 switch len(parents) {
84 // if the path is not found in any of its parents, the path was
85 // created by this commit; we must add it to the revisions list and
86 // stop searching. This includes the case when current is the
87 // initial commit.
88 case 0:
89 *result = append(*result, current)
90 return nil
91 case 1: // only one parent contains the path
92 // if the file contents has change, add the current commit
93 different, err := differentContents(path, current, parents)
94 if err != nil {
95 return err
96 }
97 if len(different) == 1 {
98 *result = append(*result, current)
99 }
100 // in any case, walk the parent
101 return walkGraph(result, seen, parents[0], path)
102 default: // more than one parent contains the path
103 // TODO: detect merges that had a conflict, because they must be
104 // included in the result here.
105 for _, p := range parents {
106 err := walkGraph(result, seen, p, path)
107 if err != nil {
108 return err
109 }
110 }
111 }
112 return nil
113}
114
115func parentsContainingPath(path string, c *object.Commit) ([]*object.Commit, error) {
116 // TODO: benchmark this method making git.object.Commit.parent public instead of using
117 // an iterator
118 var result []*object.Commit
119 iter := c.Parents()
120 for {
121 parent, err := iter.Next()
122 if err == io.EOF {
123 return result, nil
124 }
125 if err != nil {
126 return nil, err
127 }
128 if _, err := parent.File(path); err == nil {
129 result = append(result, parent)
130 }
131 }
132}
133
134// Returns an slice of the commits in "cs" that has the file "path", but with different
135// contents than what can be found in "c".
136func differentContents(path string, c *object.Commit, cs []*object.Commit) ([]*object.Commit, error) {
137 result := make([]*object.Commit, 0, len(cs))
138 h, found := blobHash(path, c)
139 if !found {
140 return nil, object.ErrFileNotFound
141 }
142 for _, cx := range cs {
143 if hx, found := blobHash(path, cx); found && h != hx {
144 result = append(result, cx)
145 }
146 }
147 return result, nil
148}
149
150// blobHash returns the hash of a path in a commit
151func blobHash(path string, commit *object.Commit) (hash plumbing.Hash, found bool) {
152 file, err := commit.File(path)
153 if err != nil {
154 var empty plumbing.Hash
155 return empty, found
156 }
157 return file.Hash, true
158}
159
160type contentsComparatorFn func(path string, a, b *object.Commit) (bool, error)
161
162// Returns a new slice of commits, with duplicates removed. Expects a
163// sorted commit list. Duplication is defined according to "comp". It
164// will always keep the first commit of a series of duplicated commits.
165func removeComp(path string, cs []*object.Commit, comp contentsComparatorFn) ([]*object.Commit, error) {
166 result := make([]*object.Commit, 0, len(cs))
167 if len(cs) == 0 {
168 return result, nil
169 }
170 result = append(result, cs[0])
171 for i := 1; i < len(cs); i++ {
172 equals, err := comp(path, cs[i], cs[i-1])
173 if err != nil {
174 return nil, err
175 }
176 if !equals {
177 result = append(result, cs[i])
178 }
179 }
180 return result, nil
181}
182
183// Equivalent commits are commits whose patch is the same.
184func equivalent(path string, a, b *object.Commit) (bool, error) {
185 numParentsA := a.NumParents()
186 numParentsB := b.NumParents()
187
188 // the first commit is not equivalent to anyone
189 // and "I think" merges can not be equivalent to anything
190 if numParentsA != 1 || numParentsB != 1 {
191 return false, nil
192 }
193
194 diffsA, err := patch(a, path)
195 if err != nil {
196 return false, err
197 }
198 diffsB, err := patch(b, path)
199 if err != nil {
200 return false, err
201 }
202
203 return sameDiffs(diffsA, diffsB), nil
204}
205
206func patch(c *object.Commit, path string) ([]diffmatchpatch.Diff, error) {
207 // get contents of the file in the commit
208 file, err := c.File(path)
209 if err != nil {
210 return nil, err
211 }
212 content, err := file.Contents()
213 if err != nil {
214 return nil, err
215 }
216
217 // get contents of the file in the first parent of the commit
218 var contentParent string
219 iter := c.Parents()
220 parent, err := iter.Next()
221 if err != nil {
222 return nil, err
223 }
224 file, err = parent.File(path)
225 if err != nil {
226 contentParent = ""
227 } else {
228 contentParent, err = file.Contents()
229 if err != nil {
230 return nil, err
231 }
232 }
233
234 // compare the contents of parent and child
235 return diff.Do(content, contentParent), nil
236}
237
238func sameDiffs(a, b []diffmatchpatch.Diff) bool {
239 if len(a) != len(b) {
240 return false
241 }
242 for i := range a {
243 if !sameDiff(a[i], b[i]) {
244 return false
245 }
246 }
247 return true
248}
249
250func sameDiff(a, b diffmatchpatch.Diff) bool {
251 if a.Type != b.Type {
252 return false
253 }
254 switch a.Type {
255 case 0:
256 return countLines(a.Text) == countLines(b.Text)
257 case 1, -1:
258 return a.Text == b.Text
259 default:
260 panic("unreachable")
261 }
262}