fork of go-git with some jj specific features
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v4.3.1 262 lines 6.8 kB view raw
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}