aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--_examples/blame/main.go48
-rw-r--r--blame.go612
-rw-r--r--blame_test.go134
-rw-r--r--plumbing/object/commit.go11
-rw-r--r--plumbing/object/commit_test.go70
-rw-r--r--references.go264
-rw-r--r--references_test.go401
7 files changed, 647 insertions, 893 deletions
diff --git a/_examples/blame/main.go b/_examples/blame/main.go
new file mode 100644
index 0000000..3ffae17
--- /dev/null
+++ b/_examples/blame/main.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/go-git/go-git/v5"
+ . "github.com/go-git/go-git/v5/_examples"
+)
+
+// Basic example of how to blame a repository.
+func main() {
+ CheckArgs("<url>", "<file_to_blame>")
+ url := os.Args[1]
+ path := os.Args[2]
+
+ tmp, err := os.MkdirTemp("", "go-git-blame-*")
+ CheckIfError(err)
+
+ defer os.RemoveAll(tmp)
+
+ // Clone the given repository.
+ Info("git clone %s %s", url, tmp)
+ r, err := git.PlainClone(
+ tmp,
+ false,
+ &git.CloneOptions{
+ URL: url,
+ Tags: git.NoTags,
+ },
+ )
+ CheckIfError(err)
+
+ // Retrieve the branch's HEAD, to then get the HEAD commit.
+ ref, err := r.Head()
+ CheckIfError(err)
+
+ c, err := r.CommitObject(ref.Hash())
+ CheckIfError(err)
+
+ Info("git blame %s", path)
+
+ // Blame the given file/path.
+ br, err := git.Blame(c, path)
+ CheckIfError(err)
+
+ fmt.Printf("%s", br.String())
+}
diff --git a/blame.go b/blame.go
index 43634b3..2a877dc 100644
--- a/blame.go
+++ b/blame.go
@@ -2,16 +2,18 @@ package git
import (
"bytes"
+ "container/heap"
"errors"
"fmt"
+ "io"
"strconv"
- "strings"
"time"
"unicode/utf8"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/utils/diff"
+ "github.com/sergi/go-diff/diffmatchpatch"
)
// BlameResult represents the result of a Blame operation.
@@ -29,67 +31,86 @@ type BlameResult struct {
func Blame(c *object.Commit, path string) (*BlameResult, error) {
// The file to blame is identified by the input arguments:
// commit and path. commit is a Commit object obtained from a Repository. Path
- // represents a path to a specific file contained into the repository.
+ // represents a path to a specific file contained in the repository.
//
- // Blaming a file is a two step process:
+ // Blaming a file is done by walking the tree in reverse order trying to find where each line was last modified.
//
- // 1. Create a linear history of the commits affecting a file. We use
- // revlist.New for that.
+ // When a diff is found it cannot immediately assume it came from that commit, as it may have come from 1 of its
+ // parents, so it will first try to resolve those diffs from its parents, if it couldn't find the change in its
+ // parents then it will assign the change to itself.
//
- // 2. Then build a graph with a node for every line in every file in
- // the history of the file.
+ // When encountering 2 parents that have made the same change to a file it will choose the parent that was merged
+ // into the current branch first (this is determined by the order of the parents inside the commit).
//
- // Each node is assigned a commit: Start by the nodes in the first
- // commit. Assign that commit as the creator of all its lines.
- //
- // Then jump to the nodes in the next commit, and calculate the diff
- // between the two files. Newly created lines get
- // assigned the new commit as its origin. Modified lines also get
- // this new commit. Untouched lines retain the old commit.
- //
- // All this work is done in the assignOrigin function which holds all
- // the internal relevant data in a "blame" struct, that is not
- // exported.
- //
- // TODO: ways to improve the efficiency of this function:
- // 1. Improve revlist
- // 2. Improve how to traverse the history (example a backward traversal will
- // be much more efficient)
- //
- // TODO: ways to improve the function in general:
- // 1. Add memoization between revlist and assign.
- // 2. It is using much more memory than needed, see the TODOs below.
+ // This currently works on a line by line basis, if performance becomes an issue it could be changed to work with
+ // hunks rather than lines. Then when encountering diff hunks it would need to split them where necessary.
b := new(blame)
b.fRev = c
b.path = path
+ b.q = new(priorityQueue)
- // get all the file revisions
- if err := b.fillRevs(); err != nil {
+ file, err := b.fRev.File(path)
+ if err != nil {
return nil, err
}
-
- // calculate the line tracking graph and fill in
- // file contents in data.
- if err := b.fillGraphAndData(); err != nil {
+ finalLines, err := file.Lines()
+ if err != nil {
return nil, err
}
+ finalLength := len(finalLines)
- file, err := b.fRev.File(b.path)
+ needsMap := make([]lineMap, finalLength)
+ for i := range needsMap {
+ needsMap[i] = lineMap{i, i, nil, -1}
+ }
+ contents, err := file.Contents()
if err != nil {
return nil, err
}
- finalLines, err := file.Lines()
+ b.q.Push(&queueItem{
+ nil,
+ nil,
+ c,
+ path,
+ contents,
+ needsMap,
+ 0,
+ false,
+ 0,
+ })
+ items := make([]*queueItem, 0)
+ for {
+ items = items[:0]
+ for {
+ if b.q.Len() == 0 {
+ return nil, errors.New("invalid state: no items left on the blame queue")
+ }
+ item := b.q.Pop()
+ items = append(items, item)
+ next := b.q.Peek()
+ if next == nil || next.Hash != item.Commit.Hash {
+ break
+ }
+ }
+ finished, err := b.addBlames(items)
+ if err != nil {
+ return nil, err
+ }
+ if finished == true {
+ break
+ }
+ }
if err != nil {
return nil, err
}
- // Each node (line) holds the commit where it was introduced or
- // last modified. To achieve that we use the FORWARD algorithm
- // described in Zimmermann, et al. "Mining Version Archives for
- // Co-changed Lines", in proceedings of the Mining Software
- // Repositories workshop, Shanghai, May 22-23, 2006.
- lines, err := newLines(finalLines, b.sliceGraph(len(b.graph)-1))
+ b.lineToCommit = make([]*object.Commit, finalLength)
+ for i := range needsMap {
+ b.lineToCommit[i] = needsMap[i].Commit
+ }
+
+ lines, err := newLines(finalLines, b.lineToCommit)
if err != nil {
return nil, err
}
@@ -105,6 +126,8 @@ func Blame(c *object.Commit, path string) (*BlameResult, error) {
type Line struct {
// Author is the email address of the last author that modified the line.
Author string
+ // AuthorName is the name of the last author that modified the line.
+ AuthorName string
// Text is the original text of the line.
Text string
// Date is when the original text of the line was introduced
@@ -113,31 +136,21 @@ type Line struct {
Hash plumbing.Hash
}
-func newLine(author, text string, date time.Time, hash plumbing.Hash) *Line {
+func newLine(author, authorName, text string, date time.Time, hash plumbing.Hash) *Line {
return &Line{
- Author: author,
- Text: text,
- Hash: hash,
- Date: date,
+ Author: author,
+ AuthorName: authorName,
+ Text: text,
+ Hash: hash,
+ Date: date,
}
}
func newLines(contents []string, commits []*object.Commit) ([]*Line, error) {
- lcontents := len(contents)
- lcommits := len(commits)
-
- if lcontents != lcommits {
- if lcontents == lcommits-1 && contents[lcontents-1] != "\n" {
- contents = append(contents, "\n")
- } else {
- return nil, errors.New("contents and commits have different length")
- }
- }
-
- result := make([]*Line, 0, lcontents)
+ result := make([]*Line, 0, len(contents))
for i := range contents {
result = append(result, newLine(
- commits[i].Author.Email, contents[i],
+ commits[i].Author.Email, commits[i].Author.Name, contents[i],
commits[i].Author.When, commits[i].Hash,
))
}
@@ -152,151 +165,426 @@ type blame struct {
path string
// the commit of the final revision of the file to blame
fRev *object.Commit
- // the chain of revisions affecting the the file to blame
- revs []*object.Commit
- // the contents of the file across all its revisions
- data []string
- // the graph of the lines in the file across all the revisions
- graph [][]*object.Commit
+ // resolved lines
+ lineToCommit []*object.Commit
+ // queue of commits that need resolving
+ q *priorityQueue
}
-// calculate the history of a file "path", starting from commit "from", sorted by commit date.
-func (b *blame) fillRevs() error {
- var err error
-
- b.revs, err = references(b.fRev, b.path)
- return err
+type lineMap struct {
+ Orig, Cur int
+ Commit *object.Commit
+ FromParentNo int
}
-// build graph of a file from its revision history
-func (b *blame) fillGraphAndData() error {
- //TODO: not all commits are needed, only the current rev and the prev
- b.graph = make([][]*object.Commit, len(b.revs))
- b.data = make([]string, len(b.revs)) // file contents in all the revisions
- // for every revision of the file, starting with the first
- // one...
- for i, rev := range b.revs {
+func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
+ curItem := curItems[0]
+
+ // Simple optimisation to merge paths, there is potential to go a bit further here and check for any duplicates
+ // not only if they are all the same.
+ if len(curItems) == 1 {
+ curItems = nil
+ } else if curItem.IdenticalToChild {
+ allSame := true
+ lenCurItems := len(curItems)
+ lowestParentNo := curItem.ParentNo
+ for i := 1; i < lenCurItems; i++ {
+ if !curItems[i].IdenticalToChild || curItem.Child != curItems[i].Child {
+ allSame = false
+ break
+ }
+ lowestParentNo = min(lowestParentNo, curItems[i].ParentNo)
+ }
+ if allSame {
+ curItem.Child.numParentsNeedResolving = curItem.Child.numParentsNeedResolving - lenCurItems + 1
+ curItems = nil // free the memory
+ curItem.ParentNo = lowestParentNo
+
+ // Now check if we can remove the parent completely
+ for curItem.Child.IdenticalToChild && curItem.Child.MergedChildren == nil && curItem.Child.numParentsNeedResolving == 1 {
+ oldChild := curItem.Child
+ curItem.Child = oldChild.Child
+ curItem.ParentNo = oldChild.ParentNo
+ }
+ }
+ }
+
+ // if we have more than 1 item for this commit, create a single needsMap
+ if len(curItems) > 1 {
+ curItem.MergedChildren = make([]childToNeedsMap, len(curItems))
+ for i, c := range curItems {
+ curItem.MergedChildren[i] = childToNeedsMap{c.Child, c.NeedsMap, c.IdenticalToChild, c.ParentNo}
+ }
+ newNeedsMap := make([]lineMap, 0, len(curItem.NeedsMap))
+ newNeedsMap = append(newNeedsMap, curItems[0].NeedsMap...)
+
+ for i := 1; i < len(curItems); i++ {
+ cur := curItems[i].NeedsMap
+ n := 0 // position in newNeedsMap
+ c := 0 // position in current list
+ for c < len(cur) {
+ if n == len(newNeedsMap) {
+ newNeedsMap = append(newNeedsMap, cur[c:]...)
+ break
+ } else if newNeedsMap[n].Cur == cur[c].Cur {
+ n++
+ c++
+ } else if newNeedsMap[n].Cur < cur[c].Cur {
+ n++
+ } else {
+ newNeedsMap = append(newNeedsMap, cur[c])
+ newPos := len(newNeedsMap) - 1
+ for newPos > n {
+ newNeedsMap[newPos-1], newNeedsMap[newPos] = newNeedsMap[newPos], newNeedsMap[newPos-1]
+ newPos--
+ }
+ }
+ }
+ }
+ curItem.NeedsMap = newNeedsMap
+ curItem.IdenticalToChild = false
+ curItem.Child = nil
+ curItems = nil // free the memory
+ }
+
+ parents, err := parentsContainingPath(curItem.path, curItem.Commit)
+ if err != nil {
+ return false, err
+ }
+
+ anyPushed := false
+ for parnetNo, prev := range parents {
+ currentHash, err := blobHash(curItem.path, curItem.Commit)
+ if err != nil {
+ return false, err
+ }
+ prevHash, err := blobHash(prev.Path, prev.Commit)
+ if err != nil {
+ return false, err
+ }
+ if currentHash == prevHash {
+ if len(parents) == 1 && curItem.MergedChildren == nil && curItem.IdenticalToChild {
+ // commit that has 1 parent and 1 child and is the same as both, bypass it completely
+ b.q.Push(&queueItem{
+ Child: curItem.Child,
+ Commit: prev.Commit,
+ path: prev.Path,
+ Contents: curItem.Contents,
+ NeedsMap: curItem.NeedsMap, // reuse the NeedsMap as we are throwing away this item
+ IdenticalToChild: true,
+ ParentNo: curItem.ParentNo,
+ })
+ } else {
+ b.q.Push(&queueItem{
+ Child: curItem,
+ Commit: prev.Commit,
+ path: prev.Path,
+ Contents: curItem.Contents,
+ NeedsMap: append([]lineMap(nil), curItem.NeedsMap...), // create new slice and copy
+ IdenticalToChild: true,
+ ParentNo: parnetNo,
+ })
+ curItem.numParentsNeedResolving++
+ }
+ anyPushed = true
+ continue
+ }
+
// get the contents of the file
- file, err := rev.File(b.path)
+ file, err := prev.Commit.File(prev.Path)
if err != nil {
- return nil
+ return false, err
}
- b.data[i], err = file.Contents()
+ prevContents, err := file.Contents()
if err != nil {
- return err
+ return false, err
}
- nLines := countLines(b.data[i])
- // create a node for each line
- b.graph[i] = make([]*object.Commit, nLines)
- // assign a commit to each node
- // if this is the first revision, then the node is assigned to
- // this first commit.
- if i == 0 {
- for j := 0; j < nLines; j++ {
- b.graph[i][j] = b.revs[i]
+
+ hunks := diff.Do(prevContents, curItem.Contents)
+ prevl := -1
+ curl := -1
+ need := 0
+ getFromParent := make([]lineMap, 0)
+ out:
+ for h := range hunks {
+ hLines := countLines(hunks[h].Text)
+ for hl := 0; hl < hLines; hl++ {
+ switch {
+ case hunks[h].Type == diffmatchpatch.DiffEqual:
+ prevl++
+ curl++
+ if curl == curItem.NeedsMap[need].Cur {
+ // add to needs
+ getFromParent = append(getFromParent, lineMap{curl, prevl, nil, -1})
+ // move to next need
+ need++
+ if need >= len(curItem.NeedsMap) {
+ break out
+ }
+ }
+ case hunks[h].Type == diffmatchpatch.DiffInsert:
+ curl++
+ if curl == curItem.NeedsMap[need].Cur {
+ // the line we want is added, it may have been added here (or by another parent), skip it for now
+ need++
+ if need >= len(curItem.NeedsMap) {
+ break out
+ }
+ }
+ case hunks[h].Type == diffmatchpatch.DiffDelete:
+ prevl += hLines
+ continue out
+ default:
+ return false, errors.New("invalid state: invalid hunk Type")
+ }
}
- } else {
- // if this is not the first commit, then assign to the old
- // commit or to the new one, depending on what the diff
- // says.
- b.assignOrigin(i, i-1)
+ }
+
+ if len(getFromParent) > 0 {
+ b.q.Push(&queueItem{
+ curItem,
+ nil,
+ prev.Commit,
+ prev.Path,
+ prevContents,
+ getFromParent,
+ 0,
+ false,
+ parnetNo,
+ })
+ curItem.numParentsNeedResolving++
+ anyPushed = true
}
}
- return nil
-}
-// sliceGraph returns a slice of commits (one per line) for a particular
-// revision of a file (0=first revision).
-func (b *blame) sliceGraph(i int) []*object.Commit {
- fVs := b.graph[i]
- result := make([]*object.Commit, 0, len(fVs))
- for _, v := range fVs {
- c := *v
- result = append(result, &c)
+ curItem.Contents = "" // no longer need, free the memory
+
+ if !anyPushed {
+ return finishNeeds(curItem)
}
- return result
+
+ return false, nil
}
-// Assigns origin to vertexes in current (c) rev from data in its previous (p)
-// revision
-func (b *blame) assignOrigin(c, p int) {
- // assign origin based on diff info
- hunks := diff.Do(b.data[p], b.data[c])
- sl := -1 // source line
- dl := -1 // destination line
- for h := range hunks {
- hLines := countLines(hunks[h].Text)
- for hl := 0; hl < hLines; hl++ {
- switch {
- case hunks[h].Type == 0:
- sl++
- dl++
- b.graph[c][dl] = b.graph[p][sl]
- case hunks[h].Type == 1:
- dl++
- b.graph[c][dl] = b.revs[c]
- case hunks[h].Type == -1:
- sl++
- default:
- panic("unreachable")
+func finishNeeds(curItem *queueItem) (bool, error) {
+ // any needs left in the needsMap must have come from this revision
+ for i := range curItem.NeedsMap {
+ if curItem.NeedsMap[i].Commit == nil {
+ curItem.NeedsMap[i].Commit = curItem.Commit
+ curItem.NeedsMap[i].FromParentNo = -1
+ }
+ }
+
+ if curItem.Child == nil && curItem.MergedChildren == nil {
+ return true, nil
+ }
+
+ if curItem.MergedChildren == nil {
+ return applyNeeds(curItem.Child, curItem.NeedsMap, curItem.IdenticalToChild, curItem.ParentNo)
+ }
+
+ for _, ctn := range curItem.MergedChildren {
+ m := 0 // position in merged needs map
+ p := 0 // position in parent needs map
+ for p < len(ctn.NeedsMap) {
+ if ctn.NeedsMap[p].Cur == curItem.NeedsMap[m].Cur {
+ ctn.NeedsMap[p].Commit = curItem.NeedsMap[m].Commit
+ m++
+ p++
+ } else if ctn.NeedsMap[p].Cur < curItem.NeedsMap[m].Cur {
+ p++
+ } else {
+ m++
}
}
+ finished, err := applyNeeds(ctn.Child, ctn.NeedsMap, ctn.IdenticalToChild, ctn.ParentNo)
+ if finished || err != nil {
+ return finished, err
+ }
}
-}
-// GoString prints the results of a Blame using git-blame's style.
-func (b *blame) GoString() string {
- var buf bytes.Buffer
+ return false, nil
+}
- file, err := b.fRev.File(b.path)
- if err != nil {
- panic("PrettyPrint: internal error in repo.Data")
+func applyNeeds(child *queueItem, needsMap []lineMap, identicalToChild bool, parentNo int) (bool, error) {
+ if identicalToChild {
+ for i := range child.NeedsMap {
+ l := &child.NeedsMap[i]
+ if l.Cur != needsMap[i].Cur || l.Orig != needsMap[i].Orig {
+ return false, errors.New("needsMap isn't the same? Why not??")
+ }
+ if l.Commit == nil || parentNo < l.FromParentNo {
+ l.Commit = needsMap[i].Commit
+ l.FromParentNo = parentNo
+ }
+ }
+ } else {
+ i := 0
+ out:
+ for j := range child.NeedsMap {
+ l := &child.NeedsMap[j]
+ for needsMap[i].Orig < l.Cur {
+ i++
+ if i == len(needsMap) {
+ break out
+ }
+ }
+ if l.Cur == needsMap[i].Orig {
+ if l.Commit == nil || parentNo < l.FromParentNo {
+ l.Commit = needsMap[i].Commit
+ l.FromParentNo = parentNo
+ }
+ }
+ }
}
- contents, err := file.Contents()
- if err != nil {
- panic("PrettyPrint: internal error in repo.Data")
+ child.numParentsNeedResolving--
+ if child.numParentsNeedResolving == 0 {
+ finished, err := finishNeeds(child)
+ if finished || err != nil {
+ return finished, err
+ }
}
- lines := strings.Split(contents, "\n")
+ return false, nil
+}
+
+// String prints the results of a Blame using git-blame's style.
+func (b BlameResult) String() string {
+ var buf bytes.Buffer
+
// max line number length
- mlnl := len(strconv.Itoa(len(lines)))
+ mlnl := len(strconv.Itoa(len(b.Lines)))
// max author length
mal := b.maxAuthorLength()
- format := fmt.Sprintf("%%s (%%-%ds %%%dd) %%s\n",
- mal, mlnl)
+ format := fmt.Sprintf("%%s (%%-%ds %%s %%%dd) %%s\n", mal, mlnl)
- fVs := b.graph[len(b.graph)-1]
- for ln, v := range fVs {
- fmt.Fprintf(&buf, format, v.Hash.String()[:8],
- prettyPrintAuthor(fVs[ln]), ln+1, lines[ln])
+ for ln := range b.Lines {
+ _, _ = fmt.Fprintf(&buf, format, b.Lines[ln].Hash.String()[:8],
+ b.Lines[ln].AuthorName, b.Lines[ln].Date.Format("2006-01-02 15:04:05 -0700"), ln+1, b.Lines[ln].Text)
}
return buf.String()
}
-// utility function to pretty print the author.
-func prettyPrintAuthor(c *object.Commit) string {
- return fmt.Sprintf("%s %s", c.Author.Name, c.Author.When.Format("2006-01-02"))
-}
-
// utility function to calculate the number of runes needed
// to print the longest author name in the blame of a file.
-func (b *blame) maxAuthorLength() int {
- memo := make(map[plumbing.Hash]struct{}, len(b.graph)-1)
- fVs := b.graph[len(b.graph)-1]
+func (b BlameResult) maxAuthorLength() int {
m := 0
- for ln := range fVs {
- if _, ok := memo[fVs[ln].Hash]; ok {
- continue
- }
- memo[fVs[ln].Hash] = struct{}{}
- m = max(m, utf8.RuneCountInString(prettyPrintAuthor(fVs[ln])))
+ for ln := range b.Lines {
+ m = max(m, utf8.RuneCountInString(b.Lines[ln].AuthorName))
}
return m
}
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
func max(a, b int) int {
if a > b {
return a
}
return b
}
+
+type childToNeedsMap struct {
+ Child *queueItem
+ NeedsMap []lineMap
+ IdenticalToChild bool
+ ParentNo int
+}
+
+type queueItem struct {
+ Child *queueItem
+ MergedChildren []childToNeedsMap
+ Commit *object.Commit
+ path string
+ Contents string
+ NeedsMap []lineMap
+ numParentsNeedResolving int
+ IdenticalToChild bool
+ ParentNo int
+}
+
+type priorityQueueImp []*queueItem
+
+func (pq *priorityQueueImp) Len() int { return len(*pq) }
+func (pq *priorityQueueImp) Less(i, j int) bool {
+ return !(*pq)[i].Commit.Less((*pq)[j].Commit)
+}
+func (pq *priorityQueueImp) Swap(i, j int) { (*pq)[i], (*pq)[j] = (*pq)[j], (*pq)[i] }
+func (pq *priorityQueueImp) Push(x any) { *pq = append(*pq, x.(*queueItem)) }
+func (pq *priorityQueueImp) Pop() any {
+ n := len(*pq)
+ ret := (*pq)[n-1]
+ (*pq)[n-1] = nil // ovoid memory leak
+ *pq = (*pq)[0 : n-1]
+
+ return ret
+}
+func (pq *priorityQueueImp) Peek() *object.Commit {
+ if len(*pq) == 0 {
+ return nil
+ }
+ return (*pq)[0].Commit
+}
+
+type priorityQueue priorityQueueImp
+
+func (pq *priorityQueue) Init() { heap.Init((*priorityQueueImp)(pq)) }
+func (pq *priorityQueue) Len() int { return (*priorityQueueImp)(pq).Len() }
+func (pq *priorityQueue) Push(c *queueItem) {
+ heap.Push((*priorityQueueImp)(pq), c)
+}
+func (pq *priorityQueue) Pop() *queueItem {
+ return heap.Pop((*priorityQueueImp)(pq)).(*queueItem)
+}
+func (pq *priorityQueue) Peek() *object.Commit { return (*priorityQueueImp)(pq).Peek() }
+
+type parentCommit struct {
+ Commit *object.Commit
+ Path string
+}
+
+func parentsContainingPath(path string, c *object.Commit) ([]parentCommit, error) {
+ // TODO: benchmark this method making git.object.Commit.parent public instead of using
+ // an iterator
+ var result []parentCommit
+ iter := c.Parents()
+ for {
+ parent, err := iter.Next()
+ if err == io.EOF {
+ return result, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ if _, err := parent.File(path); err == nil {
+ result = append(result, parentCommit{parent, path})
+ } else {
+ // look for renames
+ patch, err := parent.Patch(c)
+ if err != nil {
+ return nil, err
+ } else if patch != nil {
+ for _, fp := range patch.FilePatches() {
+ from, to := fp.Files()
+ if from != nil && to != nil && to.Path() == path {
+ result = append(result, parentCommit{parent, from.Path()})
+ break
+ }
+ }
+ }
+ }
+ }
+}
+
+func blobHash(path string, commit *object.Commit) (plumbing.Hash, error) {
+ file, err := commit.File(path)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+ return file.Hash, nil
+}
diff --git a/blame_test.go b/blame_test.go
index 7895b66..1c5db26 100644
--- a/blame_test.go
+++ b/blame_test.go
@@ -28,7 +28,7 @@ func (s *BlameSuite) TestNewLines(c *C) {
}
func (s *BlameSuite) TestNewLinesWithNewLine(c *C) {
- lines, err := newLines([]string{"foo"}, []*object.Commit{
+ lines, err := newLines([]string{"foo", ""}, []*object.Commit{
{Message: "foo"},
{Message: "bar"},
})
@@ -36,7 +36,7 @@ func (s *BlameSuite) TestNewLinesWithNewLine(c *C) {
c.Assert(err, IsNil)
c.Assert(lines, HasLen, 2)
c.Assert(lines[0].Text, Equals, "foo")
- c.Assert(lines[1].Text, Equals, "\n")
+ c.Assert(lines[1].Text, Equals, "")
}
type blameTest struct {
@@ -81,10 +81,11 @@ func (s *BlameSuite) mockBlame(c *C, t blameTest, r *Repository) (blame *BlameRe
commit, err := r.CommitObject(plumbing.NewHash(t.blames[i]))
c.Assert(err, IsNil)
l := &Line{
- Author: commit.Author.Email,
- Text: lines[i],
- Date: commit.Author.When,
- Hash: commit.Hash,
+ Author: commit.Author.Email,
+ AuthorName: commit.Author.Name,
+ Text: lines[i],
+ Date: commit.Author.When,
+ Hash: commit.Hash,
}
blamedLines = append(blamedLines, l)
}
@@ -146,7 +147,11 @@ var blameTests = [...]blameTest{
repeat("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", 7),
)},
/*
- // Failed
+ // This fails due to the different diff tool being used to create the patches.
+ // For example in commit d4b48a39aba7d3bd3e8abef2274a95b112d1ae73 when "function echo_status()" is added:
+ // - 'git diff' adds the new "}\n\n" to the end of function and keeps the "}\n\n" beforehand blamed to the previous commit
+ // - our diff adds the new "}\n\n" before the function and reuses the existing "}\n\n" to close the new function
+ // the resultant file is the same, but it causes blame not to match.
{"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "InstallSpinnaker.sh", concat(
repeat("ce9f123d790717599aaeb76bc62510de437761be", 2),
repeat("a47d0aaeda421f06df248ad65bd58230766bf118", 1),
@@ -341,7 +346,9 @@ var blameTests = [...]blameTest{
repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 185),
)},
/*
- // Fail by 3
+ // This fails due to the different diff tool being used to create the patches.
+ // For commit c89dab0d42f1856d157357e9010f8cc6a12f5b1f our diff tool keeps an existing newline as moved in the file, whereas
+ // 'git diff' says the existing newline was deleted and a new one created.
{"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "pylib/spinnaker/configurator.py", concat(
repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 53),
repeat("c89dab0d42f1856d157357e9010f8cc6a12f5b1f", 1),
@@ -423,65 +430,63 @@ var blameTests = [...]blameTest{
repeat("637ba49300f701cfbd859c1ccf13c4f39a9ba1c8", 1),
repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 13),
)},
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "config/default-spinnaker-local.yml", concat(
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 9),
+ repeat("5e09821cbd7d710405b61cab0a795c2982a71b9c", 2),
+ repeat("99534ecc895fe17a1d562bb3049d4168a04d0865", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 2),
+ repeat("a596972a661d9a7deca8abd18b52ce1a39516e89", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 5),
+ repeat("5e09821cbd7d710405b61cab0a795c2982a71b9c", 2),
+ repeat("a596972a661d9a7deca8abd18b52ce1a39516e89", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 5),
+ repeat("5e09821cbd7d710405b61cab0a795c2982a71b9c", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 25),
+ repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
+ repeat("eaf7614cad81e8ab5c813dd4821129d0c04ea449", 1),
+ repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 24),
+ repeat("974b775a8978b120ff710cac93a21c7387b914c9", 2),
+ repeat("3ce7b902a51bac2f10994f7d1f251b616c975e54", 1),
+ repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 6),
+ repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 14),
+ repeat("7c8d9a6081d9cb7a56c479bfe64d70540ea32795", 5),
+ repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 2),
+ )},
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "config/spinnaker.yml", concat(
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 32),
+ repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 2),
+ repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 1),
+ repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 6),
+ repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 2),
+ repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 2),
+ repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 2),
+ repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 3),
+ repeat("7c8d9a6081d9cb7a56c479bfe64d70540ea32795", 3),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 50),
+ repeat("974b775a8978b120ff710cac93a21c7387b914c9", 2),
+ repeat("d4553dac205023fa77652308af1a2d1cf52138fb", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 9),
+ repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
+ repeat("eaf7614cad81e8ab5c813dd4821129d0c04ea449", 1),
+ repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 39),
+ repeat("079e42e7c979541b6fab7343838f7b9fd4a360cd", 6),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 15),
+ )},
/*
- // fail a few lines
- {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "config/default-spinnaker-local.yml", concat(
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 9),
- repeat("5e09821cbd7d710405b61cab0a795c2982a71b9c", 2),
- repeat("99534ecc895fe17a1d562bb3049d4168a04d0865", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 2),
- repeat("a596972a661d9a7deca8abd18b52ce1a39516e89", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 5),
- repeat("5e09821cbd7d710405b61cab0a795c2982a71b9c", 2),
- repeat("a596972a661d9a7deca8abd18b52ce1a39516e89", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 5),
- repeat("5e09821cbd7d710405b61cab0a795c2982a71b9c", 1),
- repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 25),
- repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
- repeat("eaf7614cad81e8ab5c813dd4821129d0c04ea449", 1),
- repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 24),
- repeat("974b775a8978b120ff710cac93a21c7387b914c9", 2),
- repeat("3ce7b902a51bac2f10994f7d1f251b616c975e54", 1),
- repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 6),
- repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 14),
- repeat("7c8d9a6081d9cb7a56c479bfe64d70540ea32795", 5),
- repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 2),
- )},
- */
- /*
- // fail one line
- {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "config/spinnaker.yml", concat(
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 32),
- repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 2),
- repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 1),
- repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 6),
- repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 2),
- repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 2),
- repeat("5a2a845bc08974a36d599a4a4b7e25be833823b0", 2),
- repeat("41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c", 3),
- repeat("7c8d9a6081d9cb7a56c479bfe64d70540ea32795", 3),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 50),
- repeat("974b775a8978b120ff710cac93a21c7387b914c9", 2),
- repeat("d4553dac205023fa77652308af1a2d1cf52138fb", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 9),
- repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
- repeat("eaf7614cad81e8ab5c813dd4821129d0c04ea449", 1),
- repeat("caf6d62e8285d4681514dd8027356fb019bc97ff", 1),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 39),
- repeat("079e42e7c979541b6fab7343838f7b9fd4a360cd", 6),
- repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 15),
- )},
- */
- /*
+ // This fails due to the different diff tool being used to create the patches
+ // For commit d1ff4e13e9e0b500821aa558373878f93487e34b our diff tool keeps an existing newline as moved in the file, whereas
+ // 'git diff' says the existing newline was deleted and a new one created
{"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/install_development.sh", concat(
repeat("99534ecc895fe17a1d562bb3049d4168a04d0865", 1),
repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 71),
)},
*/
/*
- // FAIL two lines interchanged
+ // This fails due to the different diff tool being used to create the patches
+ // For commit 838aed816872c52ed435e4876a7b64dba0bed500 the diff tools assign the "fi\n" to different line numbers
{"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/bootstrap_dev.sh", concat(
repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 95),
repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
@@ -542,10 +547,7 @@ var blameTests = [...]blameTest{
repeat("838aed816872c52ed435e4876a7b64dba0bed500", 8),
)},
*/
- /*
- // FAIL move?
- {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/create_google_dev_vm.sh", concat(
- repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 20),
- )},
- */
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/create_google_dev_vm.sh", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 20),
+ )},
}
diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go
index d2f7184..8a0f35c 100644
--- a/plumbing/object/commit.go
+++ b/plumbing/object/commit.go
@@ -376,6 +376,17 @@ func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
return openpgp.CheckArmoredDetachedSignature(keyring, er, signature, nil)
}
+// Less defines a compare function to determine which commit is 'earlier' by:
+// - First use Committer.When
+// - If Committer.When are equal then use Author.When
+// - If Author.When also equal then compare the string value of the hash
+func (c *Commit) Less(rhs *Commit) bool {
+ return c.Committer.When.Before(rhs.Committer.When) ||
+ (c.Committer.When.Equal(rhs.Committer.When) &&
+ (c.Author.When.Before(rhs.Author.When) ||
+ (c.Author.When.Equal(rhs.Author.When) && bytes.Compare(c.Hash[:], rhs.Hash[:]) < 0)))
+}
+
func indent(t string) string {
var output []string
for _, line := range strings.Split(t, "\n") {
diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go
index a939bc6..4b0f6b4 100644
--- a/plumbing/object/commit_test.go
+++ b/plumbing/object/commit_test.go
@@ -503,3 +503,73 @@ func (s *SuiteCommit) TestEncodeWithoutSignature(c *C) {
"\n"+
"Merge branch 'master' of github.com:tyba/git-fixture\n")
}
+
+func (s *SuiteCommit) TestLess(c *C) {
+ when1 := time.Now()
+ when2 := when1.Add(time.Hour)
+
+ hash1 := plumbing.NewHash("1669dce138d9b841a518c64b10914d88f5e488ea")
+ hash2 := plumbing.NewHash("2669dce138d9b841a518c64b10914d88f5e488ea")
+
+ commitLessTests := []struct {
+ Committer1When, Committer2When time.Time
+ Author1When, Author2When time.Time
+ Hash1, Hash2 plumbing.Hash
+ Exp bool
+ }{
+ {when1, when1, when1, when1, hash1, hash2, true},
+ {when1, when1, when1, when1, hash2, hash1, false},
+ {when1, when1, when1, when2, hash1, hash2, true},
+ {when1, when1, when1, when2, hash2, hash1, true},
+ {when1, when1, when2, when1, hash1, hash2, false},
+ {when1, when1, when2, when1, hash2, hash1, false},
+ {when1, when1, when2, when2, hash1, hash2, true},
+ {when1, when1, when2, when2, hash2, hash1, false},
+ {when1, when2, when1, when1, hash1, hash2, true},
+ {when1, when2, when1, when1, hash2, hash1, true},
+ {when1, when2, when1, when2, hash1, hash2, true},
+ {when1, when2, when1, when2, hash2, hash1, true},
+ {when1, when2, when2, when1, hash1, hash2, true},
+ {when1, when2, when2, when1, hash2, hash1, true},
+ {when1, when2, when2, when2, hash1, hash2, true},
+ {when1, when2, when2, when2, hash2, hash1, true},
+ {when2, when1, when1, when1, hash1, hash2, false},
+ {when2, when1, when1, when1, hash2, hash1, false},
+ {when2, when1, when1, when2, hash1, hash2, false},
+ {when2, when1, when1, when2, hash2, hash1, false},
+ {when2, when1, when2, when1, hash1, hash2, false},
+ {when2, when1, when2, when1, hash2, hash1, false},
+ {when2, when1, when2, when2, hash1, hash2, false},
+ {when2, when1, when2, when2, hash2, hash1, false},
+ {when2, when2, when1, when1, hash1, hash2, true},
+ {when2, when2, when1, when1, hash2, hash1, false},
+ {when2, when2, when1, when2, hash1, hash2, true},
+ {when2, when2, when1, when2, hash2, hash1, true},
+ {when2, when2, when2, when1, hash1, hash2, false},
+ {when2, when2, when2, when1, hash2, hash1, false},
+ {when2, when2, when2, when2, hash1, hash2, true},
+ {when2, when2, when2, when2, hash2, hash1, false},
+ }
+
+ for _, t := range commitLessTests {
+ commit1 := &Commit{
+ Hash: t.Hash1,
+ Author: Signature{
+ When: t.Author1When,
+ },
+ Committer: Signature{
+ When: t.Committer1When,
+ },
+ }
+ commit2 := &Commit{
+ Hash: t.Hash2,
+ Author: Signature{
+ When: t.Author2When,
+ },
+ Committer: Signature{
+ When: t.Committer2When,
+ },
+ }
+ c.Assert(commit1.Less(commit2), Equals, t.Exp)
+ }
+}
diff --git a/references.go b/references.go
deleted file mode 100644
index 6d96035..0000000
--- a/references.go
+++ /dev/null
@@ -1,264 +0,0 @@
-package git
-
-import (
- "io"
- "sort"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/go-git/go-git/v5/utils/diff"
-
- "github.com/sergi/go-diff/diffmatchpatch"
-)
-
-// References returns a slice of Commits for the file at "path", starting from
-// the commit provided that contains the file from the provided path. The last
-// commit into the returned slice is the commit where the file was created.
-// If the provided commit does not contains the specified path, a nil slice is
-// returned. The commits are sorted in commit order, newer to older.
-//
-// Caveats:
-//
-// - Moves and copies are not currently supported.
-//
-// - Cherry-picks are not detected unless there are no commits between them and
-// therefore can appear repeated in the list. (see git path-id for hints on how
-// to fix this).
-func references(c *object.Commit, path string) ([]*object.Commit, error) {
- var result []*object.Commit
- seen := make(map[plumbing.Hash]struct{})
- if err := walkGraph(&result, &seen, c, path); err != nil {
- return nil, err
- }
-
- // TODO result should be returned without ordering
- sortCommits(result)
-
- // for merges of identical cherry-picks
- return removeComp(path, result, equivalent)
-}
-
-type commitSorterer struct {
- l []*object.Commit
-}
-
-func (s commitSorterer) Len() int {
- return len(s.l)
-}
-
-func (s commitSorterer) Less(i, j int) bool {
- return s.l[i].Committer.When.Before(s.l[j].Committer.When) ||
- s.l[i].Committer.When.Equal(s.l[j].Committer.When) &&
- s.l[i].Author.When.Before(s.l[j].Author.When)
-}
-
-func (s commitSorterer) Swap(i, j int) {
- s.l[i], s.l[j] = s.l[j], s.l[i]
-}
-
-// SortCommits sorts a commit list by commit date, from older to newer.
-func sortCommits(l []*object.Commit) {
- s := &commitSorterer{l}
- sort.Sort(s)
-}
-
-// Recursive traversal of the commit graph, generating a linear history of the
-// path.
-func walkGraph(result *[]*object.Commit, seen *map[plumbing.Hash]struct{}, current *object.Commit, path string) error {
- // check and update seen
- if _, ok := (*seen)[current.Hash]; ok {
- return nil
- }
- (*seen)[current.Hash] = struct{}{}
-
- // if the path is not in the current commit, stop searching.
- if _, err := current.File(path); err != nil {
- return nil
- }
-
- // optimization: don't traverse branches that does not
- // contain the path.
- parents, err := parentsContainingPath(path, current)
- if err != nil {
- return err
- }
- switch len(parents) {
- // if the path is not found in any of its parents, the path was
- // created by this commit; we must add it to the revisions list and
- // stop searching. This includes the case when current is the
- // initial commit.
- case 0:
- *result = append(*result, current)
- return nil
- case 1: // only one parent contains the path
- // if the file contents has change, add the current commit
- different, err := differentContents(path, current, parents)
- if err != nil {
- return err
- }
- if len(different) == 1 {
- *result = append(*result, current)
- }
- // in any case, walk the parent
- return walkGraph(result, seen, parents[0], path)
- default: // more than one parent contains the path
- // TODO: detect merges that had a conflict, because they must be
- // included in the result here.
- for _, p := range parents {
- err := walkGraph(result, seen, p, path)
- if err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func parentsContainingPath(path string, c *object.Commit) ([]*object.Commit, error) {
- // TODO: benchmark this method making git.object.Commit.parent public instead of using
- // an iterator
- var result []*object.Commit
- iter := c.Parents()
- for {
- parent, err := iter.Next()
- if err == io.EOF {
- return result, nil
- }
- if err != nil {
- return nil, err
- }
- if _, err := parent.File(path); err == nil {
- result = append(result, parent)
- }
- }
-}
-
-// Returns an slice of the commits in "cs" that has the file "path", but with different
-// contents than what can be found in "c".
-func differentContents(path string, c *object.Commit, cs []*object.Commit) ([]*object.Commit, error) {
- result := make([]*object.Commit, 0, len(cs))
- h, found := blobHash(path, c)
- if !found {
- return nil, object.ErrFileNotFound
- }
- for _, cx := range cs {
- if hx, found := blobHash(path, cx); found && h != hx {
- result = append(result, cx)
- }
- }
- return result, nil
-}
-
-// blobHash returns the hash of a path in a commit
-func blobHash(path string, commit *object.Commit) (hash plumbing.Hash, found bool) {
- file, err := commit.File(path)
- if err != nil {
- var empty plumbing.Hash
- return empty, found
- }
- return file.Hash, true
-}
-
-type contentsComparatorFn func(path string, a, b *object.Commit) (bool, error)
-
-// Returns a new slice of commits, with duplicates removed. Expects a
-// sorted commit list. Duplication is defined according to "comp". It
-// will always keep the first commit of a series of duplicated commits.
-func removeComp(path string, cs []*object.Commit, comp contentsComparatorFn) ([]*object.Commit, error) {
- result := make([]*object.Commit, 0, len(cs))
- if len(cs) == 0 {
- return result, nil
- }
- result = append(result, cs[0])
- for i := 1; i < len(cs); i++ {
- equals, err := comp(path, cs[i], cs[i-1])
- if err != nil {
- return nil, err
- }
- if !equals {
- result = append(result, cs[i])
- }
- }
- return result, nil
-}
-
-// Equivalent commits are commits whose patch is the same.
-func equivalent(path string, a, b *object.Commit) (bool, error) {
- numParentsA := a.NumParents()
- numParentsB := b.NumParents()
-
- // the first commit is not equivalent to anyone
- // and "I think" merges can not be equivalent to anything
- if numParentsA != 1 || numParentsB != 1 {
- return false, nil
- }
-
- diffsA, err := patch(a, path)
- if err != nil {
- return false, err
- }
- diffsB, err := patch(b, path)
- if err != nil {
- return false, err
- }
-
- return sameDiffs(diffsA, diffsB), nil
-}
-
-func patch(c *object.Commit, path string) ([]diffmatchpatch.Diff, error) {
- // get contents of the file in the commit
- file, err := c.File(path)
- if err != nil {
- return nil, err
- }
- content, err := file.Contents()
- if err != nil {
- return nil, err
- }
-
- // get contents of the file in the first parent of the commit
- var contentParent string
- iter := c.Parents()
- parent, err := iter.Next()
- if err != nil {
- return nil, err
- }
- file, err = parent.File(path)
- if err != nil {
- contentParent = ""
- } else {
- contentParent, err = file.Contents()
- if err != nil {
- return nil, err
- }
- }
-
- // compare the contents of parent and child
- return diff.Do(content, contentParent), nil
-}
-
-func sameDiffs(a, b []diffmatchpatch.Diff) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if !sameDiff(a[i], b[i]) {
- return false
- }
- }
- return true
-}
-
-func sameDiff(a, b diffmatchpatch.Diff) bool {
- if a.Type != b.Type {
- return false
- }
- switch a.Type {
- case 0:
- return countLines(a.Text) == countLines(b.Text)
- case 1, -1:
- return a.Text == b.Text
- default:
- panic("unreachable")
- }
-}
diff --git a/references_test.go b/references_test.go
deleted file mode 100644
index 28d1bb9..0000000
--- a/references_test.go
+++ /dev/null
@@ -1,401 +0,0 @@
-package git
-
-import (
- "bytes"
- "fmt"
-
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/go-git/go-git/v5/storage/memory"
-
- fixtures "github.com/go-git/go-git-fixtures/v4"
- . "gopkg.in/check.v1"
-)
-
-type ReferencesSuite struct {
- BaseSuite
-}
-
-var _ = Suite(&ReferencesSuite{})
-
-var referencesTests = [...]struct {
- // input data to revlist
- repo string
- commit string
- path string
- // expected output data form the revlist
- revs []string
-}{
- // Tyba git-fixture
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "binary.jpg", []string{
- "35e85108805c84807bc66a02d91535e1e24b38b9",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "CHANGELOG", []string{
- "b8e471f58bcbca63b07bda20e428190409c2db47",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "go/example.go", []string{
- "918c48b83bd081e863dbe1b80f8998f058cd8294",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "json/long.json", []string{
- "af2d6a6954d532f8ffb47615169c8fdf9d383a1a",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "json/short.json", []string{
- "af2d6a6954d532f8ffb47615169c8fdf9d383a1a",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "LICENSE", []string{
- "b029517f6300c2da0f4b651b8642506cd6aaf45d",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "php/crappy.php", []string{
- "918c48b83bd081e863dbe1b80f8998f058cd8294",
- }},
- {"https://github.com/git-fixtures/basic.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "vendor/foo.go", []string{
- "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
- }},
- {"https://github.com/jamesob/desk.git", "d4edaf0e8101fcea437ebd982d899fe2cc0f9f7b", "LICENSE", []string{
- "ffcda27c2de6768ee83f3f4a027fa4ab57d50f09",
- }},
- {"https://github.com/jamesob/desk.git", "d4edaf0e8101fcea437ebd982d899fe2cc0f9f7b", "README.md", []string{
- "ffcda27c2de6768ee83f3f4a027fa4ab57d50f09",
- "2e87a2dcc63a115f9a61bd969d1e85fb132a431b",
- "215b0ac06225b0671bc3460d10da88c3406f796f",
- "0260eb7a2623dd2309ab439f74e8681fccdc4285",
- "d46b48933e94f30992486374fa9a6becfd28ea17",
- "9cb4df2a88efee8836f9b8ad27ca2717f624164e",
- "8c49acdec2ed441706d8799f8b17878aae4c1ffe",
- "ebaca0c6f54c23193ee8175c3530e370cb2dabe3",
- "77675f82039551a19de4fbccbe69366fe63680df",
- "b9741594fb8ab7374f9be07d6a09a3bf96719816",
- "04db6acd94de714ca48128c606b17ee1149a630e",
- "ff737bd8a962a714a446d7592fae423a56e61e12",
- "eadd03f7a1cc54810bd10eef6747ad9562ad246d",
- "b5072ab5c1cf89191d71f1244eecc5d1f369ef7e",
- "bfa6ebc9948f1939402b063c0a2a24bf2b1c1cc3",
- "d9aef39828c670dfdb172502021a2ebcda8cf2fb",
- "1a6b6e45c91e1831494eb139ee3f8e21649c7fb0",
- "09fdbe4612066cf63ea46aee43c7cfaaff02ecfb",
- "236f6526b1150cc1f1723566b4738f443fc70777",
- "7862953f470b62397d22f6782a884f5bea6d760d",
- "b0b0152d08c2333680266977a5bc9c4e50e1e968",
- "13ce6c1c77c831f381974aa1c62008a414bd2b37",
- "d3f3c8faca048d11709969fbfc0cdf2901b87578",
- "8777dde1abe18c805d021366643218d3f3356dd9",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "pylib/spinnaker/reconfigure_spinnaker.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "pylib/spinnaker/validate_configuration.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- "1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9",
- "1e3d328a2cabda5d0aaddc5dec65271343e0dc37",
- "b5d999e2986e190d81767cd3cfeda0260f9f6fb8",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "pylib/spinnaker/fetch.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "pylib/spinnaker/yaml_util.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- "1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9",
- "b5d999e2986e190d81767cd3cfeda0260f9f6fb8",
- "023d4fb17b76e0fe0764971df8b8538b735a1d67",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "dev/build_release.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- "1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9",
- "f42771ba298b93a7c4f5b16c5b30ab96c15305a8",
- "dd52703a50e71891f63fcf05df1f69836f4e7056",
- "0d9c9cef53af38cefcb6801bb492aaed3f2c9a42",
- "d375f1994ff4d0bdc32d614e698f1b50e1093f14",
- "abad497f11a366548aa95303c8c2f165fe7ae918",
- "6986d885626792dee4ef6b7474dfc9230c5bda54",
- "5422a86a10a8c5a1ef6728f5fc8894d9a4c54cb9",
- "09a4ea729b25714b6368959eea5113c99938f7b6",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "pkg_scripts/postUninstall.sh", []string{
- "ce9f123d790717599aaeb76bc62510de437761be",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "install/first_google_boot.sh", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- "de25f576b888569192e6442b0202d30ca7b2d8ec",
- "a596972a661d9a7deca8abd18b52ce1a39516e89",
- "9467ec579708b3c71dd9e5b3906772841c144a30",
- "c4a9091e4076cb740fa46e790dd5b658e19012ad",
- "6eb5d9c5225224bfe59c401182a2939d6c27fc00",
- "495c7118e7cf757aa04eab410b64bfb5b5149ad2",
- "dd2d03c19658ff96d371aef00e75e2e54702da0e",
- "2a3b1d3b134e937c7bafdab6cc2950e264bf5dee",
- "a57b08a9072f6a865f760551be2a4944f72f804a",
- "0777fadf4ca6f458d7071de414f9bd5417911037",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "install/install_spinnaker.sh", []string{
- "0d9c9cef53af38cefcb6801bb492aaed3f2c9a42",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "install/install_fake_openjdk8.sh", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "install/install_spinnaker.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- "37f94770d81232b1895fca447878f68d65aac652",
- "46c9dcbb55ca3f4735e82ad006e8cae2fdd050d9",
- "124a88cfda413cb7182ca9c739a284a9e50042a1",
- "eb4faf67a8b775d7985d07a708e3ffeac4273580",
- "0d9c9cef53af38cefcb6801bb492aaed3f2c9a42",
- "01171a8a2e843bef3a574ba73b258ac29e5d5405",
- "739d8c6fe16edcb6ef9185dc74197de561b84315",
- "d33c2d1e350b03fb989eefc612e8c9d5fa7cadc2",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "install/__init__.py", []string{
- "a24001f6938d425d0e7504bdf5d27fc866a85c3d",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "experimental/docker-compose/docker-compose.yml", []string{
- "fda357835d889595dc39dfebc6181d863cce7d4f",
- "57c59e7144354a76e1beba69ae2f85db6b1727af",
- "7682dff881029c722d893a112a64fea6849a0428",
- "66f1c938c380a4096674b27540086656076a597f",
- "56dc238f6f397e93f1d1aad702976889c830e8bf",
- "b95e442c064935709e789fa02126f17ddceef10b",
- "f98965a8f42037bd038b86c3401da7e6dfbf4f2e",
- "5344429749e8b68b168d2707b7903692436cc2ea",
- "6a31f5d219766b0cec4ea4fbbbfe47bdcdb0ab8e",
- "ddaae195b628150233b0a48f50a1674fd9d1a924",
- "7119ad9cf7d4e4d8b059e5337374baae4adc7458",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "unittest/validate_configuration_test.py", []string{
- "1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9",
- "1e3d328a2cabda5d0aaddc5dec65271343e0dc37",
- }},
- {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "README.adoc", []string{
- "638f61b3331695f46f1a88095e26dea0f09f176b",
- "bd42370d3fe8d410e78acb96f81cb3d838ad1c21",
- "d6905eab6fec1841c7cf8e4484499f5c8d7d423e",
- "c0a70a0f5aa494f0ae01c55ba191f2325556489a",
- "811795c8a185e88f5d269195cb68b29c8d0fe170",
- "d6e6fe0194447cc280f942d6a2e0521b68ea7796",
- "174bdbf9edfb0ca88415dd4a673852d5b22e7036",
- "9944d6cf72b8f82d622d85dad7434472bc8f397d",
- "e805183c72f0426fb073728c01901c2fd2db1da6",
- "8ef83dd443a05e9122681950399edaa58a38d466",
- "d73f9cee49a5ad27a42a6e18af7c49a8f28ad8a8",
- }},
- // FAILS
- /*
- // this contains an empty move
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "google/dev/build_google_tarball.py", []string{
- "88e60ac93f832efc2616b3c165e99a8f2ffc3e0c",
- "9e49443da49b8c862cc140b660744f84eebcfa51",
- }},
- */
- /*
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "unittest/yaml_util_test.py", []string{
- "edf909edb9319c5e615e4ce73da47bbdca388ebe",
- "023d4fb17b76e0fe0764971df8b8538b735a1d67",
- }},
- */
- /*
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "unittest/configurator_test.py", []string{
- "1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9",
- "edf909edb9319c5e615e4ce73da47bbdca388ebe",
- "d14f793a6cd7169ef708a4fc276ad876bd3edd4e",
- "023d4fb17b76e0fe0764971df8b8538b735a1d67",
- }},
- */
- /*
- // this contains a cherry-pick at 094d0e7d5d691 (with 3f34438d)
- {"https://github.com/jamesob/desk.git", "d4edaf0e8101fcea437ebd982d899fe2cc0f9f7b", "desk", []string{
- "ffcda27c2de6768ee83f3f4a027fa4ab57d50f09",
- "a0c1e853158ccbaf95574220bbf3b54509034a9f",
- "decfc524570c407d6bba0f217e534c8b47dbdbee",
- "1413872d5b3af7cd674bbe0e1f23387cd5d940e6",
- "40cd5a91d916e7b2f331e4e85fdc52636fd7cff7",
- "8e07d73aa0e3780f8c7cf8ad1a6b263df26a0a52",
- "19c56f95720ac3630efe9f29b1a252581d6cbc0c",
- "9ea46ccc6d253cffb4b7b66e936987d87de136e4",
- "094d0e7d5d69141c98a606910ba64786c5565da0",
- "801e62706a9e4fef75fcaca9c78744de0bc36e6a",
- "eddf335f31c73624ed3f40dc5fcad50136074b2b",
- "c659093f06eb2bd68c6252caeab605e5cd8aa49e",
- "d94b3fe8ce0e3a474874d742992d432cd040582f",
- "93cddf036df2d8509f910063696acd556ca7600f",
- "b3d4cb0c826b16b301f088581d681654d8de6c07",
- "52d90f9b513dd3c5330663cba39396e6b8a3ba4e",
- "15919e99ded03c6ceea9ff98558e77a322a4dadb",
- "803bf37847633e2f685a46a27b11facf22efebec",
- "c07ad524ee1e616c70bf2ea7a0ee4f4a01195d78",
- "b91aff30f318fda461d009c308490613b394f3e2",
- "67cec1e8a3f21c6eb11678e3f31ffd228b55b783",
- "bbe404c78af7525fabc57b9e7aa7c100b0d39f7a",
- "5dd078848786c2babc2511e9502fa98518cf3535",
- "7970ae7cc165c5205945dfb704d67d53031f550a",
- "33091ac904747747ff30f107d4d0f22fa872eccf",
- "069f81cab12d185ba1b509be946c47897cd4fb1f",
- "13ce6c1c77c831f381974aa1c62008a414bd2b37",
- }},
- */
- /*
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "InstallSpinnaker.sh", []string{
- "ce9f123d790717599aaeb76bc62510de437761be",
- "23673af3ad70b50bba7fdafadc2323302f5ba520",
- "b7015a5d36990d69a054482556127b9c7404a24a",
- "582da9622e3a72a19cd261a017276d72b5b0051a",
- "0c5bb1e4392e751f884f3c57de5d4aee72c40031",
- "c9c2a0ec03968ab17e8b16fdec9661eb1dbea173",
- "a3cdf880826b4d9af42b93f4a2df29a91ab31d35",
- "18526c447f5174d33c96aac6d6433318b0e2021c",
- "2a6288be1c8ea160c443ca3cd0fe826ff2387d37",
- "9e74d009894d73dd07773ea6b3bdd8323db980f7",
- "d2f6214b625db706384b378a29cc4c22237db97a",
- "202a9c720b3ba8106e022a0ad027ebe279040c78",
- "791bcd1592828d9d5d16e83f3a825fb08b0ba22d",
- "01e65d67eed8afcb67a6bdf1c962541f62b299c9",
- "6328ee836affafc1b52127147b5ca07300ac78e6",
- "3de4f77c105f700f50d9549d32b9a05a01b46c4b",
- "8980daf661408a3faa1f22c225702a5c1d11d5c9",
- "8eb116de9128c314ac8a6f5310ca500b8c74f5db",
- "88e841aad37b71b78a8fb88bc75fe69499d527c7",
- "370d61cdbc1f3c90db6759f1599ccbabd40ad6c1",
- "505577dc87d300cf562dc4702a05a5615d90d855",
- "b5c6053a46993b20d1b91e7b7206bffa54669ad7",
- "ba486de7c025457963701114c683dcd4708e1dee",
- "b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941",
- "a47d0aaeda421f06df248ad65bd58230766bf118",
- "495c7118e7cf757aa04eab410b64bfb5b5149ad2",
- "46670eb6477c353d837dbaba3cf36c5f8b86f037",
- "dd2d03c19658ff96d371aef00e75e2e54702da0e",
- "4bbcad219ec55a465fb48ce236cb10ca52d43b1f",
- "50d0556563599366f29cb286525780004fa5a317",
- "9a06d3f20eabb254d0a1e2ff7735ef007ccd595e",
- "d4b48a39aba7d3bd3e8abef2274a95b112d1ae73",
- }},
- */
- /*
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "config/default-spinnaker-local.yml", []string{
- "ae904e8d60228c21c47368f6a10f1cc9ca3aeebf",
- "99534ecc895fe17a1d562bb3049d4168a04d0865",
- "caf6d62e8285d4681514dd8027356fb019bc97ff",
- "eaf7614cad81e8ab5c813dd4821129d0c04ea449",
- "5a2a845bc08974a36d599a4a4b7e25be833823b0",
- "41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c",
- "974b775a8978b120ff710cac93a21c7387b914c9",
- "87e459a9a044b3109dfeb943cc82c627b61d84a6",
- "5e09821cbd7d710405b61cab0a795c2982a71b9c",
- "8cc2d4bdb0a15aafc7fe02cdcb03ab90c974cafa",
- "3ce7b902a51bac2f10994f7d1f251b616c975e54",
- "a596972a661d9a7deca8abd18b52ce1a39516e89",
- "8980daf661408a3faa1f22c225702a5c1d11d5c9",
- }},
- */
- /*
- {"https://github.com/spinnaker/spinnaker.git", "b32b2aecae2cfca4840dd480f8082da206a538da", "config/spinnaker.yml", []string{
- "ae904e8d60228c21c47368f6a10f1cc9ca3aeebf",
- "caf6d62e8285d4681514dd8027356fb019bc97ff",
- "eaf7614cad81e8ab5c813dd4821129d0c04ea449",
- "5a2a845bc08974a36d599a4a4b7e25be833823b0",
- "41e96c54a478e5d09dd07ed7feb2d8d08d8c7e3c",
- "974b775a8978b120ff710cac93a21c7387b914c9",
- "ed887f6547d7cd2b2d741184a06f97a0a704152b",
- "d4553dac205023fa77652308af1a2d1cf52138fb",
- "a596972a661d9a7deca8abd18b52ce1a39516e89",
- "66ac94f0b4442707fb6f695fbed91d62b3bd9d4a",
- "079e42e7c979541b6fab7343838f7b9fd4a360cd",
- }},
- */
-}
-
-func (s *ReferencesSuite) TestObjectNotFoundError(c *C) {
- h1 := plumbing.NewHash("af2d6a6954d532f8ffb47615169c8fdf9d383a1a")
- hParent := plumbing.NewHash("1669dce138d9b841a518c64b10914d88f5e488ea")
-
- url := fixtures.ByURL("https://github.com/git-fixtures/basic.git").One().DotGit().Root()
- storer := memory.NewStorage()
- r, err := Clone(storer, nil, &CloneOptions{
- URL: url,
- })
- c.Assert(err, IsNil)
-
- delete(storer.Objects, hParent)
-
- commit, err := r.CommitObject(h1)
- c.Assert(err, IsNil)
-
- _, err = references(commit, "LICENSE")
- c.Assert(err, Equals, plumbing.ErrObjectNotFound)
-}
-
-func (s *ReferencesSuite) TestRevList(c *C) {
- for _, t := range referencesTests {
- r := s.NewRepositoryFromPackfile(fixtures.ByURL(t.repo).One())
-
- commit, err := r.CommitObject(plumbing.NewHash(t.commit))
- c.Assert(err, IsNil)
-
- revs, err := references(commit, t.path)
- c.Assert(err, IsNil)
- c.Assert(len(revs), Equals, len(t.revs))
-
- for i := range revs {
- if revs[i].Hash.String() != t.revs[i] {
- commit, err := s.Repository.CommitObject(plumbing.NewHash(t.revs[i]))
- c.Assert(err, IsNil)
- equiv, err := equivalent(t.path, revs[i], commit)
- c.Assert(err, IsNil)
- if equiv {
- fmt.Printf("cherry-pick detected: %s %s\n", revs[i].Hash.String(), t.revs[i])
- } else {
- c.Fatalf("\nrepo=%s, commit=%s, path=%s, \n%s",
- t.repo, t.commit, t.path, compareSideBySide(t.revs, revs))
- }
- }
- }
- }
-}
-
-// same length is assumed
-func compareSideBySide(a []string, b []*object.Commit) string {
- var buf bytes.Buffer
- buf.WriteString("\t EXPECTED OBTAINED ")
- var sep string
- var obt string
- for i := range a {
- obt = b[i].Hash.String()
- if a[i] != obt {
- sep = "------"
- } else {
- sep = " "
- }
- buf.WriteString(fmt.Sprintf("\n%d", i+1))
- buf.WriteString(sep)
- buf.WriteString(a[i])
- buf.WriteString(sep)
- buf.WriteString(obt)
- }
- return buf.String()
-}
-
-var cherryPicks = [...][]string{
- // repo, path, commit a, commit b
- {"https://github.com/jamesob/desk.git", "desk", "094d0e7d5d69141c98a606910ba64786c5565da0", "3f34438d54f4a1ca86db8c0f03ed8eb38f20e22c"},
-}
-
-// should detect cherry picks
-func (s *ReferencesSuite) TestEquivalent(c *C) {
- for _, t := range cherryPicks {
- cs := s.commits(c, t[0], t[2], t[3])
- equiv, err := equivalent(t[1], cs[0], cs[1])
- c.Assert(err, IsNil)
- c.Assert(equiv, Equals, true, Commentf("repo=%s, file=%s, a=%s b=%s", t[0], t[1], t[2], t[3]))
- }
-}
-
-// returns the commits from a slice of hashes
-func (s *ReferencesSuite) commits(c *C, repo string, hs ...string) []*object.Commit {
- r := s.NewRepositoryFromPackfile(fixtures.ByURL(repo).One())
-
- result := make([]*object.Commit, 0, len(hs))
- for _, h := range hs {
- commit, err := r.CommitObject(plumbing.NewHash(h))
- c.Assert(err, IsNil)
-
- result = append(result, commit)
- }
-
- return result
-}