aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2015-12-11 23:58:00 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2015-12-11 23:58:00 +0100
commitc22c181f70e0afb294513315e9975b9f3f4c1d39 (patch)
tree8377ae4645c58454d0f2a61631a4a81d2d7d5faf
parent5c8fff7e9e614d9f463d964699539fe71509ba39 (diff)
parentc347e978b52212b5aa968a14d81c8fff47ab24d7 (diff)
downloadgo-git-c22c181f70e0afb294513315e9975b9f3f4c1d39.tar.gz
Merge pull request #7 from alcortesm/blame
Blame
-rw-r--r--blame/blame.go283
-rwxr-xr-xblame/blame2humantest.bash47
-rw-r--r--blame/blame_test.go572
-rw-r--r--commit.go21
-rw-r--r--commit_test.go96
-rw-r--r--common.go18
-rw-r--r--common_test.go29
-rw-r--r--diff/diff.go45
-rw-r--r--diff/diff_ext_test.go109
-rw-r--r--file.go35
-rw-r--r--file_test.go133
-rw-r--r--repository.go2
-rw-r--r--repository_test.go4
-rw-r--r--revlist/revlist.go274
-rwxr-xr-xrevlist/revlist2humantest.bash36
-rw-r--r--revlist/revlist_test.go402
-rw-r--r--tree.go7
17 files changed, 2101 insertions, 12 deletions
diff --git a/blame/blame.go b/blame/blame.go
new file mode 100644
index 0000000..7256a7b
--- /dev/null
+++ b/blame/blame.go
@@ -0,0 +1,283 @@
+// Package blame contains blaming functionality for files in the repo.
+//
+// Blaming a file is finding what commit was the last to modify each of
+// the lines in the file, therefore the output of a blaming operation is
+// usualy a slice of commits, one commit per line in the file.
+//
+// This package also provides a pretty print function to output the
+// results of a blame in a similar format to the git-blame command.
+package blame
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+
+ "gopkg.in/src-d/go-git.v2"
+ "gopkg.in/src-d/go-git.v2/core"
+ "gopkg.in/src-d/go-git.v2/diff"
+ "gopkg.in/src-d/go-git.v2/revlist"
+)
+
+// Blame returns the last commit that modified each line of a file in
+// a repository.
+//
+// The file to blame is identified by the input arguments: repo, commit and path.
+// The output is a slice of commits, one for each line in the file.
+//
+// Blaming a file is a two step process:
+//
+// 1. Create a linear history of the commits affecting a file. We use
+// revlist.New for that.
+//
+// 2. Then build a graph with a node for every line in every file in
+// the history of the file.
+//
+// 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.
+//
+// Each node is asigned 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.
+
+type Blame struct {
+ Repo string
+ Path string
+ Rev string
+ Lines []*line
+}
+
+func New(repo *git.Repository, path string, commit *git.Commit) (*Blame, error) {
+ // init the internal blame struct
+ b := new(blame)
+ b.repo = repo
+ b.fRev = commit
+ b.path = path
+
+ // get all the file revisions
+ if err := b.fillRevs(); err != nil {
+ return nil, err
+ }
+
+ // calculate the line tracking graph and fill in
+ // file contents in data.
+ if err := b.fillGraphAndData(); err != nil {
+ return nil, err
+ }
+
+ file, err := b.fRev.File(b.path)
+ if err != nil {
+ return nil, err
+ }
+ finalLines := file.Lines()
+
+ lines, err := newLines(finalLines, b.sliceGraph(len(b.graph)-1))
+ if err != nil {
+ return nil, err
+ }
+
+ return &Blame{
+ Repo: repo.URL,
+ Path: path,
+ Rev: commit.Hash.String(),
+ Lines: lines,
+ }, nil
+}
+
+type line struct {
+ author string
+ text string
+}
+
+func newLine(author, text string) *line {
+ return &line{
+ author: author,
+ text: text,
+ }
+}
+
+func newLines(contents []string, commits []*git.Commit) ([]*line, error) {
+ if len(contents) != len(commits) {
+ return nil, errors.New("contents and commits have different length")
+ }
+ result := make([]*line, 0, len(contents))
+ for i := range contents {
+ l := newLine(commits[i].Author.Email, contents[i])
+ result = append(result, l)
+ }
+ return result, nil
+}
+
+// this struct is internally used by the blame function to hold its
+// inputs, outputs and state.
+type blame struct {
+ repo *git.Repository // the repo holding the history of the file to blame
+ path string // the path of the file to blame
+ fRev *git.Commit // the commit of the final revision of the file to blame
+ revs revlist.Revs // the chain of revisions affecting the the file to blame
+ data []string // the contents of the file across all its revisions
+ graph [][]*git.Commit // the graph of the lines in the file across all the revisions TODO: not all commits are needed, only the current rev and the prev
+}
+
+// calculte 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 = revlist.NewRevs(b.repo, b.fRev, b.path)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// build graph of a file from its revision history
+func (b *blame) fillGraphAndData() error {
+ b.graph = make([][]*git.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 {
+ // get the contents of the file
+ file, err := rev.File(b.path)
+ if err != nil {
+ return nil
+ }
+ b.data[i] = file.Contents()
+ nLines := git.CountLines(b.data[i])
+ // create a node for each line
+ b.graph[i] = make([]*git.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] = (*git.Commit)(b.revs[i])
+ }
+ } 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)
+ }
+ }
+ 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) []*git.Commit {
+ fVs := b.graph[i]
+ result := make([]*git.Commit, 0, len(fVs))
+ for _, v := range fVs {
+ c := git.Commit(*v)
+ result = append(result, &c)
+ }
+ return result
+}
+
+// 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 := git.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] = (*git.Commit)(b.revs[c])
+ case hunks[h].Type == -1:
+ sl++
+ default:
+ panic("unreachable")
+ }
+ }
+ }
+}
+
+// GoString prints the results of a Blame using git-blame's style.
+func (b *blame) GoString() string {
+ var buf bytes.Buffer
+
+ file, err := b.fRev.File(b.path)
+ if err != nil {
+ panic("PrettyPrint: internal error in repo.Data")
+ }
+ contents := file.Contents()
+
+ lines := strings.Split(contents, "\n")
+ // max line number length
+ mlnl := len(fmt.Sprintf("%s", strconv.Itoa(len(lines))))
+ // max author length
+ mal := b.maxAuthorLength()
+ format := fmt.Sprintf("%%s (%%-%ds %%%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])
+ }
+ return buf.String()
+}
+
+// utility function to pretty print the author.
+func prettyPrintAuthor(c *git.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[core.Hash]struct{}, len(b.graph)-1)
+ fVs := b.graph[len(b.graph)-1]
+ 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])))
+ }
+ return m
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/blame/blame2humantest.bash b/blame/blame2humantest.bash
new file mode 100755
index 0000000..259988f
--- /dev/null
+++ b/blame/blame2humantest.bash
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+set -e
+
+repo=`git remote show origin | grep Fetch | cut -d' ' -f5`
+branch="master"
+if [ "$#" -eq 1 ] ; then
+ commit=`git log | head -1 | cut -d' ' -f2`
+ path=$1
+elif [ "$#" -eq 2 ] ; then
+ commit=$1
+ path=$2
+else
+ echo "bad number of parameters" > /dev/stderr
+ echo > /dev/stderr
+ echo " try with: [commit] path" > /dev/stderr
+ exit
+fi
+
+blames=`git blame --root $path | cut -d' ' -f1`
+declare -a blame
+i=0
+for shortBlame in $blames ; do
+ blame[$i]=`git show $shortBlame | head -1 | cut -d' ' -f2`
+ i=`expr $i + 1`
+done
+
+# some remotes have the .git, other don't,
+# repoDot makes sure all have
+repoDot="${repo%.git}.git"
+
+echo -e "\t{\"${repoDot}\", \"${branch}\", \"${commit}\", \"${path}\", concat(&[]string{},"
+prev=""
+count=1
+for i in ${blame[@]} ; do
+ if [ "${prev}" == "" ] ; then
+ prev=$i
+ elif [ "$prev" == "$i" ] ; then
+ count=`expr $count + 1`
+ else
+ echo -e "\t\trepeat(\"${prev}\", $count),"
+ count=1
+ prev=$i
+ fi
+done
+echo -e "\t\trepeat(\"${prev}\", $count),"
+echo -e "\t)},"
diff --git a/blame/blame_test.go b/blame/blame_test.go
new file mode 100644
index 0000000..3c24852
--- /dev/null
+++ b/blame/blame_test.go
@@ -0,0 +1,572 @@
+package blame
+
+import (
+ "bytes"
+ "os"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v2"
+ "gopkg.in/src-d/go-git.v2/core"
+ "gopkg.in/src-d/go-git.v2/formats/packfile"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct {
+ repos map[string]*git.Repository
+}
+
+var _ = Suite(&SuiteCommon{})
+
+var fixtureRepos = [...]struct {
+ url string
+ packfile string
+}{
+ {"https://github.com/tyba/git-fixture.git", "../formats/packfile/fixtures/git-fixture.ofs-delta"},
+ {"https://github.com/spinnaker/spinnaker.git", "../formats/packfile/fixtures/spinnaker-spinnaker.pack"},
+}
+
+// create the repositories of the fixtures
+func (s *SuiteCommon) SetUpSuite(c *C) {
+ s.repos = make(map[string]*git.Repository, 0)
+ for _, fixRepo := range fixtureRepos {
+ repo := git.NewPlainRepository()
+ repo.URL = fixRepo.url
+
+ d, err := os.Open(fixRepo.packfile)
+ c.Assert(err, IsNil)
+
+ r := packfile.NewReader(d)
+ // TODO: how to know the format of a pack file ahead of time?
+ // Some info at:
+ // https://codewords.recurse.com/issues/three/unpacking-git-packfiles
+ r.Format = packfile.OFSDeltaFormat
+
+ _, err = r.Read(repo.Storage)
+ c.Assert(err, IsNil)
+
+ c.Assert(d.Close(), IsNil)
+
+ s.repos[fixRepo.url] = repo
+ }
+}
+
+type blameTest struct {
+ repo string
+ rev string
+ path string
+ blames []string // the commits blamed for each line
+}
+
+func (s *SuiteCommon) mockBlame(t blameTest, c *C) (blame *Blame) {
+ repo, ok := s.repos[t.repo]
+ c.Assert(ok, Equals, true)
+
+ commit, err := repo.Commit(core.NewHash(t.rev))
+ c.Assert(err, IsNil, Commentf("%v: repo=%s, rev=%s", err, repo, t.rev))
+
+ file, err := commit.File(t.path)
+ c.Assert(err, IsNil)
+ lines := file.Lines()
+ c.Assert(len(t.blames), Equals, len(lines), Commentf(
+ "repo=%s, path=%s, rev=%s: the number of lines in the file and the number of expected blames differ (len(blames)=%d, len(lines)=%d)\nblames=%#q\nlines=%#q", t.repo, t.path, t.rev, len(t.blames), len(lines), t.blames, lines))
+
+ blamedLines := make([]*line, 0, len(t.blames))
+ for i := range t.blames {
+ commit, err := repo.Commit(core.NewHash(t.blames[i]))
+ c.Assert(err, IsNil)
+ l := &line{
+ author: commit.Author.Email,
+ text: lines[i],
+ }
+ blamedLines = append(blamedLines, l)
+ }
+
+ return &Blame{
+ Repo: t.repo,
+ Path: t.path,
+ Rev: t.rev,
+ Lines: blamedLines,
+ }
+}
+
+// run a blame on all the suite's tests
+func (s *SuiteCommon) TestBlame(c *C) {
+ for i, t := range blameTests {
+ expected := s.mockBlame(t, c)
+
+ repo, ok := s.repos[t.repo]
+ c.Assert(ok, Equals, true)
+
+ commit, err := repo.Commit(core.NewHash(t.rev))
+ c.Assert(err, IsNil)
+
+ obtained, err := New(repo, t.path, commit)
+ c.Assert(err, IsNil, Commentf("subtest %d", i))
+
+ c.Assert(obtained, DeepEquals, expected, Commentf("subtest %d: %s",
+ i, sideBySide(obtained, expected)))
+ }
+}
+
+func sideBySide(output, expected *Blame) string {
+ var buf bytes.Buffer
+ buf.WriteString(output.Repo)
+ buf.WriteString(" ")
+ buf.WriteString(expected.Repo)
+ return buf.String()
+}
+
+// utility function to avoid writing so many repeated commits
+func repeat(s string, n int) []string {
+ if n < 0 {
+ panic("repeat: n < 0")
+ }
+ r := make([]string, 0, n)
+ for i := 0; i < n; i++ {
+ r = append(r, s)
+ }
+ return r
+}
+
+// utility function to concat slices
+func concat(vargs ...[]string) []string {
+ var result []string
+ for _, ss := range vargs {
+ result = append(result, ss...)
+ }
+ return result
+}
+
+var blameTests = [...]blameTest{
+ // use the blame2humantest.bash script to easily add more tests.
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "binary.jpg", concat(
+ repeat("35e85108805c84807bc66a02d91535e1e24b38b9", 285),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "CHANGELOG", concat(
+ repeat("b8e471f58bcbca63b07bda20e428190409c2db47", 1),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "go/example.go", concat(
+ repeat("918c48b83bd081e863dbe1b80f8998f058cd8294", 142),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "json/long.json", concat(
+ repeat("af2d6a6954d532f8ffb47615169c8fdf9d383a1a", 6492),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "json/short.json", concat(
+ repeat("af2d6a6954d532f8ffb47615169c8fdf9d383a1a", 22),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "LICENSE", concat(
+ repeat("b029517f6300c2da0f4b651b8642506cd6aaf45d", 22),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "php/crappy.php", concat(
+ repeat("918c48b83bd081e863dbe1b80f8998f058cd8294", 259),
+ )},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "vendor/foo.go", concat(
+ repeat("6ecf0ef2c2dffb796033e5a02219af86ec6584e5", 7),
+ )},
+ /*
+ // Failed
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "InstallSpinnaker.sh", concat(
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 2),
+ repeat("a47d0aaeda421f06df248ad65bd58230766bf118", 1),
+ repeat("23673af3ad70b50bba7fdafadc2323302f5ba520", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 29),
+ repeat("9a06d3f20eabb254d0a1e2ff7735ef007ccd595e", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 4),
+ repeat("a47d0aaeda421f06df248ad65bd58230766bf118", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 5),
+ repeat("0c5bb1e4392e751f884f3c57de5d4aee72c40031", 2),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 3),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 7),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 2),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 5),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 7),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 3),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 6),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 10),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 4),
+ repeat("0c5bb1e4392e751f884f3c57de5d4aee72c40031", 2),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 2),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 4),
+ repeat("23673af3ad70b50bba7fdafadc2323302f5ba520", 4),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 4),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("0c5bb1e4392e751f884f3c57de5d4aee72c40031", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 13),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 2),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 6),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 2),
+ repeat("0c5bb1e4392e751f884f3c57de5d4aee72c40031", 1),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 4),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 3),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 2),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 4),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 3),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 15),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 1),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 1),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 8),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 2),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 12),
+ repeat("505577dc87d300cf562dc4702a05a5615d90d855", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 5),
+ repeat("370d61cdbc1f3c90db6759f1599ccbabd40ad6c1", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 4),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 1),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 5),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 3),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 2),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 2),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 9),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 1),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 3),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 4),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("8eb116de9128c314ac8a6f5310ca500b8c74f5db", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 6),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 6),
+ repeat("d2f6214b625db706384b378a29cc4c22237db97a", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 3),
+ repeat("d2f6214b625db706384b378a29cc4c22237db97a", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 4),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("c9c2a0ec03968ab17e8b16fdec9661eb1dbea173", 1),
+ repeat("d2f6214b625db706384b378a29cc4c22237db97a", 2),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 12),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 5),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 3),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 5),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 3),
+ repeat("a47d0aaeda421f06df248ad65bd58230766bf118", 5),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 5),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 2),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 1),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("b2c7142082d52b09ca20228606c31c7479c0833e", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("495c7118e7cf757aa04eab410b64bfb5b5149ad2", 1),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 1),
+ repeat("495c7118e7cf757aa04eab410b64bfb5b5149ad2", 3),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 1),
+ repeat("495c7118e7cf757aa04eab410b64bfb5b5149ad2", 1),
+ repeat("50d0556563599366f29cb286525780004fa5a317", 1),
+ repeat("dd2d03c19658ff96d371aef00e75e2e54702da0e", 1),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 1),
+ repeat("dd2d03c19658ff96d371aef00e75e2e54702da0e", 2),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 2),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 1),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("b5c6053a46993b20d1b91e7b7206bffa54669ad7", 1),
+ repeat("9e74d009894d73dd07773ea6b3bdd8323db980f7", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("d4b48a39aba7d3bd3e8abef2274a95b112d1ae73", 4),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 1),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 1),
+ repeat("d2f6214b625db706384b378a29cc4c22237db97a", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 3),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 2),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 2),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 4),
+ repeat("d2f6214b625db706384b378a29cc4c22237db97a", 1),
+ repeat("b7015a5d36990d69a054482556127b9c7404a24a", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 5),
+ repeat("b41d7c0e5b20bbe7c8eb6606731a3ff68f4e3941", 2),
+ repeat("d2f6214b625db706384b378a29cc4c22237db97a", 1),
+ repeat("ce9f123d790717599aaeb76bc62510de437761be", 5),
+ repeat("ba486de7c025457963701114c683dcd4708e1dee", 4),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 1),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 3),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 1),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 3),
+ repeat("6328ee836affafc1b52127147b5ca07300ac78e6", 2),
+ repeat("01e65d67eed8afcb67a6bdf1c962541f62b299c9", 3),
+ repeat("3de4f77c105f700f50d9549d32b9a05a01b46c4b", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 2),
+ repeat("370d61cdbc1f3c90db6759f1599ccbabd40ad6c1", 6),
+ repeat("dd7e66c862209e8b912694a582a09c0db3227f0d", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 2),
+ repeat("dd7e66c862209e8b912694a582a09c0db3227f0d", 3),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 1),
+ repeat("dd7e66c862209e8b912694a582a09c0db3227f0d", 1),
+ repeat("8980daf661408a3faa1f22c225702a5c1d11d5c9", 3),
+ )},
+ */
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "pylib/spinnaker/reconfigure_spinnaker.py", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 22),
+ repeat("c89dab0d42f1856d157357e9010f8cc6a12f5b1f", 7),
+ )},
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "pylib/spinnaker/validate_configuration.py", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 29),
+ repeat("1e3d328a2cabda5d0aaddc5dec65271343e0dc37", 19),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 15),
+ repeat("b5d999e2986e190d81767cd3cfeda0260f9f6fb8", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 12),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ repeat("b5d999e2986e190d81767cd3cfeda0260f9f6fb8", 8),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("b5d999e2986e190d81767cd3cfeda0260f9f6fb8", 4),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 46),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ repeat("1e3d328a2cabda5d0aaddc5dec65271343e0dc37", 42),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("1e3d328a2cabda5d0aaddc5dec65271343e0dc37", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 3),
+ repeat("1e3d328a2cabda5d0aaddc5dec65271343e0dc37", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 8),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 2),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 3),
+ repeat("1e3d328a2cabda5d0aaddc5dec65271343e0dc37", 3),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 12),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 10),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 69),
+ repeat("b5d999e2986e190d81767cd3cfeda0260f9f6fb8", 7),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ )},
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "pylib/spinnaker/run.py", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 185),
+ )},
+ /*
+ // Fail by 3
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "pylib/spinnaker/configurator.py", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 53),
+ repeat("c89dab0d42f1856d157357e9010f8cc6a12f5b1f", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ repeat("e805183c72f0426fb073728c01901c2fd2db1da6", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 6),
+ repeat("023d4fb17b76e0fe0764971df8b8538b735a1d67", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 36),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 3),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 3),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ repeat("c89dab0d42f1856d157357e9010f8cc6a12f5b1f", 13),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("c89dab0d42f1856d157357e9010f8cc6a12f5b1f", 18),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("1e14f94bcf82694fdc7e2dcbbfdbbed58db0f4d9", 1),
+ repeat("023d4fb17b76e0fe0764971df8b8538b735a1d67", 17),
+ repeat("c89dab0d42f1856d157357e9010f8cc6a12f5b1f", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 43),
+ )},
+ */
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "pylib/spinnaker/__init__.py", []string{}},
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "gradle/wrapper/gradle-wrapper.jar", concat(
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 1),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 7),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 2),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 2),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 3),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 1),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 10),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 11),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 29),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 7),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 58),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 1),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 1),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 2),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 2),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 13),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 4),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 3),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 13),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 2),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 9),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 3),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 1),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 17),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 3),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 6),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 6),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 3),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 5),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 4),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 3),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 2),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 1),
+ repeat("11d6c1020b1765e236ca65b2709d37b5bfdba0f4", 6),
+ repeat("bc02440df2ff95a014a7b3cb11b98c3a2bded777", 55),
+ )},
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "config/settings.js", concat(
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 17),
+ repeat("99534ecc895fe17a1d562bb3049d4168a04d0865", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 43),
+ repeat("d2838db9f6ef9628645e7d04cd9658a83e8708ea", 1),
+ repeat("637ba49300f701cfbd859c1ccf13c4f39a9ba1c8", 1),
+ repeat("ae904e8d60228c21c47368f6a10f1cc9ca3aeebf", 13),
+ )},
+ /*
+ // 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),
+ )},
+ */
+ /*
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/install_development.sh", concat(
+ repeat("99534ecc895fe17a1d562bb3049d4168a04d0865", 1),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 71),
+ )},
+ */
+ /*
+ // FAIL two lines interchanged
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/bootstrap_dev.sh", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 95),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 10),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 7),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 3),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 12),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 2),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 3),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 6),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 4),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("376599177551c3f04ccc94d71bbb4d037dec0c3f", 2),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 17),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 2),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 2),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 3),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 3),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 5),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 5),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 8),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 4),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 1),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 6),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 4),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 10),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 2),
+ repeat("fc28a378558cdb5bbc08b6dcb96ee77c5b716760", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 1),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 8),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 1),
+ repeat("fc28a378558cdb5bbc08b6dcb96ee77c5b716760", 1),
+ repeat("d1ff4e13e9e0b500821aa558373878f93487e34b", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 4),
+ repeat("24551a5d486969a2972ee05e87f16444890f9555", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 2),
+ repeat("24551a5d486969a2972ee05e87f16444890f9555", 1),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 8),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 13),
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 5),
+ repeat("24551a5d486969a2972ee05e87f16444890f9555", 1),
+ repeat("838aed816872c52ed435e4876a7b64dba0bed500", 8),
+ )},
+ */
+ /*
+ // FAIL move?
+ {"https://github.com/spinnaker/spinnaker.git", "f39d86f59a0781f130e8de6b2115329c1fbe9545", "dev/create_google_dev_vm.sh", concat(
+ repeat("a24001f6938d425d0e7504bdf5d27fc866a85c3d", 20),
+ )},
+ */
+}
diff --git a/commit.go b/commit.go
index 1440403..99dbf37 100644
--- a/commit.go
+++ b/commit.go
@@ -3,12 +3,16 @@ package git
import (
"bufio"
"bytes"
+ "errors"
"fmt"
"io"
"gopkg.in/src-d/go-git.v2/core"
)
+// New errors defined by this package.
+var ErrFileNotFound = errors.New("file not found")
+
type Hash core.Hash
// Commit points to a single tree, marking it as what the project looked like
@@ -45,6 +49,23 @@ func (c *Commit) Parents() *CommitIter {
return i
}
+// NumParents returns the number of parents in a commit.
+func (c *Commit) NumParents() int {
+ return len(c.parents)
+}
+
+// File returns the file with the specified "path" in the commit and a
+// nil error if the file exists. If the file does not exists, it returns
+// a nil file and the ErrFileNotFound error.
+func (c *Commit) File(path string) (file *File, err error) {
+ for file := range c.Tree().Files() {
+ if file.Name == path {
+ return file, nil
+ }
+ }
+ return nil, ErrFileNotFound
+}
+
// Decode transform an core.Object into a Blob struct
func (c *Commit) Decode(o core.Object) error {
c.Hash = o.Hash()
diff --git a/commit_test.go b/commit_test.go
index 14c2e74..67b9e77 100644
--- a/commit_test.go
+++ b/commit_test.go
@@ -1,16 +1,104 @@
package git
import (
- . "gopkg.in/check.v1"
+ "os"
+
"gopkg.in/src-d/go-git.v2/core"
+ "gopkg.in/src-d/go-git.v2/formats/packfile"
+
+ . "gopkg.in/check.v1"
)
-type CommitCommon struct{}
+type SuiteCommit struct {
+ repos map[string]*Repository
+}
+
+var _ = Suite(&SuiteCommit{})
+
+// create the repositories of the fixtures
+func (s *SuiteCommit) SetUpSuite(c *C) {
+ fixtureRepos := [...]struct {
+ url string
+ packfile string
+ }{
+ {"https://github.com/tyba/git-fixture.git", "formats/packfile/fixtures/git-fixture.ofs-delta"},
+ }
+ s.repos = make(map[string]*Repository, 0)
+ for _, fixRepo := range fixtureRepos {
+ s.repos[fixRepo.url] = NewPlainRepository()
+
+ d, err := os.Open(fixRepo.packfile)
+ c.Assert(err, IsNil)
-var _ = Suite(&CommitCommon{})
+ r := packfile.NewReader(d)
+ r.Format = packfile.OFSDeltaFormat // TODO: how to know the format of a pack file ahead of time?
-func (s *CommitCommon) TestIterClose(c *C) {
+ _, err = r.Read(s.repos[fixRepo.url].Storage)
+ c.Assert(err, IsNil)
+
+ c.Assert(d.Close(), IsNil)
+ }
+}
+
+func (s *SuiteCommit) TestIterClose(c *C) {
i := &iter{ch: make(chan core.Object, 1)}
i.Close()
i.Close()
}
+
+var fileTests = []struct {
+ repo string // the repo name as in localRepos
+ commit string // the commit to search for the file
+ path string // the path of the file to find
+ blobHash string // expected hash of the returned file
+ found bool // expected found value
+}{
+ // use git ls-tree commit to get the hash of the blobs
+ {"https://github.com/tyba/git-fixture.git", "b029517f6300c2da0f4b651b8642506cd6aaf45d", "not-found",
+ "", false},
+ {"https://github.com/tyba/git-fixture.git", "b029517f6300c2da0f4b651b8642506cd6aaf45d", ".gitignore",
+ "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88", true},
+ {"https://github.com/tyba/git-fixture.git", "b029517f6300c2da0f4b651b8642506cd6aaf45d", "LICENSE",
+ "c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", true},
+
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "not-found",
+ "", false},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", ".gitignore",
+ "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88", true},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "binary.jpg",
+ "d5c0f4ab811897cadf03aec358ae60d21f91c50d", true},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "LICENSE",
+ "c192bd6a24ea1ab01d78686e417c8bdc7c3d197f", true},
+
+ {"https://github.com/tyba/git-fixture.git", "35e85108805c84807bc66a02d91535e1e24b38b9", "binary.jpg",
+ "d5c0f4ab811897cadf03aec358ae60d21f91c50d", true},
+ {"https://github.com/tyba/git-fixture.git", "b029517f6300c2da0f4b651b8642506cd6aaf45d", "binary.jpg",
+ "", false},
+
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "CHANGELOG",
+ "d3ff53e0564a9f87d8e84b6e28e5060e517008aa", true},
+ {"https://github.com/tyba/git-fixture.git", "1669dce138d9b841a518c64b10914d88f5e488ea", "CHANGELOG",
+ "d3ff53e0564a9f87d8e84b6e28e5060e517008aa", true},
+ {"https://github.com/tyba/git-fixture.git", "a5b8b09e2f8fcb0bb99d3ccb0958157b40890d69", "CHANGELOG",
+ "d3ff53e0564a9f87d8e84b6e28e5060e517008aa", true},
+ {"https://github.com/tyba/git-fixture.git", "35e85108805c84807bc66a02d91535e1e24b38b9", "CHANGELOG",
+ "d3ff53e0564a9f87d8e84b6e28e5060e517008aa", false},
+ {"https://github.com/tyba/git-fixture.git", "b8e471f58bcbca63b07bda20e428190409c2db47", "CHANGELOG",
+ "d3ff53e0564a9f87d8e84b6e28e5060e517008aa", true},
+ {"https://github.com/tyba/git-fixture.git", "b029517f6300c2da0f4b651b8642506cd6aaf45d", "CHANGELOG",
+ "d3ff53e0564a9f87d8e84b6e28e5060e517008aa", false},
+}
+
+func (s *SuiteCommit) TestFile(c *C) {
+ for i, t := range fileTests {
+ commit, err := s.repos[t.repo].Commit(core.NewHash(t.commit))
+ c.Assert(err, IsNil, Commentf("subtest %d: %v (%s)", i, err, t.commit))
+
+ file, err := commit.File(t.path)
+ found := err == nil
+ c.Assert(found, Equals, t.found, Commentf("subtest %d, path=%s, commit=%s", i, t.path, t.commit))
+ if found {
+ c.Assert(file.Hash.String(), Equals, t.blobHash, Commentf("subtest %d, commit=%s, path=%s", i, t.commit, t.path))
+ }
+ }
+}
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..51486b8
--- /dev/null
+++ b/common.go
@@ -0,0 +1,18 @@
+package git
+
+import "strings"
+
+// CountLines returns the number of lines in a string à la git, this is
+// The newline character is assumed to be '\n'. The empty string
+// contains 0 lines. If the last line of the string doesn't end with a
+// newline, it will still be considered a line.
+func CountLines(s string) int {
+ if s == "" {
+ return 0
+ }
+ nEol := strings.Count(s, "\n")
+ if strings.HasSuffix(s, "\n") {
+ return nEol
+ }
+ return nEol + 1
+}
diff --git a/common_test.go b/common_test.go
index cf4fc29..4c48419 100644
--- a/common_test.go
+++ b/common_test.go
@@ -5,9 +5,10 @@ import (
"os"
"testing"
- . "gopkg.in/check.v1"
"gopkg.in/src-d/go-git.v2/clients/common"
"gopkg.in/src-d/go-git.v2/core"
+
+ . "gopkg.in/check.v1"
)
func Test(t *testing.T) { TestingT(t) }
@@ -42,3 +43,29 @@ func (s *MockGitUploadPackService) Fetch(*common.GitUploadPackRequest) (io.ReadC
r, _ := os.Open("formats/packfile/fixtures/git-fixture.ref-delta")
return r, nil
}
+
+type SuiteCommon struct{}
+
+var _ = Suite(&SuiteCommon{})
+
+var countLinesTests = [...]struct {
+ i string // the string we want to count lines from
+ e int // the expected number of lines in i
+}{
+ {"", 0},
+ {"a", 1},
+ {"a\n", 1},
+ {"a\nb", 2},
+ {"a\nb\n", 2},
+ {"a\nb\nc", 3},
+ {"a\nb\nc\n", 3},
+ {"a\n\n\nb\n", 4},
+ {"first line\n\tsecond line\nthird line\n", 3},
+}
+
+func (s *SuiteCommon) TestCountLines(c *C) {
+ for i, t := range countLinesTests {
+ o := CountLines(t.i)
+ c.Assert(o, Equals, t.e, Commentf("subtest %d, input=%q", i, t.i))
+ }
+}
diff --git a/diff/diff.go b/diff/diff.go
new file mode 100644
index 0000000..b840ad6
--- /dev/null
+++ b/diff/diff.go
@@ -0,0 +1,45 @@
+// Package diff implements line oriented diffs, similar to the ancient
+// Unix diff command.
+//
+// The current implementation is just a wrapper around Sergi's
+// go-diff/diffmatchpatch library, which is a go port of Neil
+// Fraser's google-diff-match-patch code
+package diff
+
+import (
+ "bytes"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+)
+
+// Do computes the (line oriented) modifications needed to turn the src
+// string into the dst string.
+func Do(src, dst string) (diffs []diffmatchpatch.Diff) {
+ dmp := diffmatchpatch.New()
+ wSrc, wDst, warray := dmp.DiffLinesToChars(src, dst)
+ diffs = dmp.DiffMain(wSrc, wDst, false)
+ diffs = dmp.DiffCharsToLines(diffs, warray)
+ return diffs
+}
+
+// Dst computes and returns the destination text.
+func Dst(diffs []diffmatchpatch.Diff) string {
+ var text bytes.Buffer
+ for _, d := range diffs {
+ if d.Type != diffmatchpatch.DiffDelete {
+ text.WriteString(d.Text)
+ }
+ }
+ return text.String()
+}
+
+// Src computes and returns the source text
+func Src(diffs []diffmatchpatch.Diff) string {
+ var text bytes.Buffer
+ for _, d := range diffs {
+ if d.Type != diffmatchpatch.DiffInsert {
+ text.WriteString(d.Text)
+ }
+ }
+ return text.String()
+}
diff --git a/diff/diff_ext_test.go b/diff/diff_ext_test.go
new file mode 100644
index 0000000..460cf8a
--- /dev/null
+++ b/diff/diff_ext_test.go
@@ -0,0 +1,109 @@
+package diff_test
+
+import (
+ "testing"
+
+ "gopkg.in/src-d/go-git.v2/diff"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type suiteCommon struct{}
+
+var _ = Suite(&suiteCommon{})
+
+var diffTests = [...]struct {
+ src string // the src string to diff
+ dst string // the dst string to diff
+}{
+ // equal inputs
+ {"", ""},
+ {"a", "a"},
+ {"a\n", "a\n"},
+ {"a\nb", "a\nb"},
+ {"a\nb\n", "a\nb\n"},
+ {"a\nb\nc", "a\nb\nc"},
+ {"a\nb\nc\n", "a\nb\nc\n"},
+ // missing '\n'
+ {"", "\n"},
+ {"\n", ""},
+ {"a", "a\n"},
+ {"a\n", "a"},
+ {"a\nb", "a\nb"},
+ {"a\nb\n", "a\nb\n"},
+ {"a\nb\nc", "a\nb\nc"},
+ {"a\nb\nc\n", "a\nb\nc\n"},
+ // generic
+ {"a\nbbbbb\n\tccc\ndd\n\tfffffffff\n", "bbbbb\n\tccc\n\tDD\n\tffff\n"},
+}
+
+func (s *suiteCommon) TestAll(c *C) {
+ for i, t := range diffTests {
+ diffs := diff.Do(t.src, t.dst)
+ src := diff.Src(diffs)
+ dst := diff.Dst(diffs)
+ c.Assert(src, Equals, t.src, Commentf("subtest %d, src=%q, dst=%q, bad calculated src", i, t.src, t.dst))
+ c.Assert(dst, Equals, t.dst, Commentf("subtest %d, src=%q, dst=%q, bad calculated dst", i, t.src, t.dst))
+ }
+}
+
+var doTests = [...]struct {
+ src, dst string
+ expected []diffmatchpatch.Diff
+}{
+ {
+ src: "",
+ dst: "",
+ expected: []diffmatchpatch.Diff{},
+ },
+ {
+ src: "a",
+ dst: "a",
+ expected: []diffmatchpatch.Diff{
+ {
+ Type: 0,
+ Text: "a",
+ },
+ },
+ },
+ {
+ src: "",
+ dst: "abc\ncba",
+ expected: []diffmatchpatch.Diff{
+ {
+ Type: 1,
+ Text: "abc\ncba",
+ },
+ },
+ },
+ {
+ src: "abc\ncba",
+ dst: "",
+ expected: []diffmatchpatch.Diff{
+ {
+ Type: -1,
+ Text: "abc\ncba",
+ },
+ },
+ },
+ {
+ src: "abc\nbcd\ncde",
+ dst: "000\nabc\n111\nBCD\n",
+ expected: []diffmatchpatch.Diff{
+ {Type: 1, Text: "000\n"},
+ {Type: 0, Text: "abc\n"},
+ {Type: -1, Text: "bcd\ncde"},
+ {Type: 1, Text: "111\nBCD\n"},
+ },
+ },
+}
+
+func (s *suiteCommon) TestDo(c *C) {
+ for i, t := range doTests {
+ diffs := diff.Do(t.src, t.dst)
+ c.Assert(diffs, DeepEquals, t.expected, Commentf("subtest %d", i))
+ }
+}
diff --git a/file.go b/file.go
new file mode 100644
index 0000000..86baf7d
--- /dev/null
+++ b/file.go
@@ -0,0 +1,35 @@
+package git
+
+import (
+ "bytes"
+ "io"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v2/core"
+)
+
+// File represents git file objects.
+type File struct {
+ Name string
+ io.Reader
+ Hash core.Hash
+}
+
+// Contents returns the contents of a file as a string.
+func (f *File) Contents() string {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(f)
+ return buf.String()
+}
+
+// Lines returns a slice of lines from the contents of a file, stripping
+// all end of line characters. If the last line is empty (does not end
+// in an end of line), it is also stripped.
+func (f *File) Lines() []string {
+ splits := strings.Split(f.Contents(), "\n")
+ // remove the last line if it is empty
+ if splits[len(splits)-1] == "" {
+ return splits[:len(splits)-1]
+ }
+ return splits
+}
diff --git a/file_test.go b/file_test.go
new file mode 100644
index 0000000..8c22bb3
--- /dev/null
+++ b/file_test.go
@@ -0,0 +1,133 @@
+package git
+
+import (
+ "os"
+
+ "gopkg.in/src-d/go-git.v2/core"
+ "gopkg.in/src-d/go-git.v2/formats/packfile"
+
+ . "gopkg.in/check.v1"
+)
+
+type SuiteFile struct {
+ repos map[string]*Repository
+}
+
+var _ = Suite(&SuiteFile{})
+
+// create the repositories of the fixtures
+func (s *SuiteFile) SetUpSuite(c *C) {
+ fixtureRepos := [...]struct {
+ url string
+ packfile string
+ }{
+ {"https://github.com/tyba/git-fixture.git", "formats/packfile/fixtures/git-fixture.ofs-delta"},
+ }
+ s.repos = make(map[string]*Repository, 0)
+ for _, fixRepo := range fixtureRepos {
+ s.repos[fixRepo.url] = NewPlainRepository()
+
+ d, err := os.Open(fixRepo.packfile)
+ c.Assert(err, IsNil)
+
+ r := packfile.NewReader(d)
+ r.Format = packfile.OFSDeltaFormat
+
+ _, err = r.Read(s.repos[fixRepo.url].Storage)
+ c.Assert(err, IsNil)
+
+ c.Assert(d.Close(), IsNil)
+ }
+}
+
+var contentsTests = []struct {
+ repo string // the repo name as in localRepos
+ commit string // the commit to search for the file
+ path string // the path of the file to find
+ contents string // expected contents of the file
+}{
+ {
+ "https://github.com/tyba/git-fixture.git",
+ "b029517f6300c2da0f4b651b8642506cd6aaf45d",
+ ".gitignore",
+ `*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+`,
+ },
+ {
+ "https://github.com/tyba/git-fixture.git",
+ "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
+ "CHANGELOG",
+ `Initial changelog
+`,
+ },
+}
+
+func (s *SuiteFile) TestContents(c *C) {
+ for i, t := range contentsTests {
+ commit, err := s.repos[t.repo].Commit(core.NewHash(t.commit))
+ c.Assert(err, IsNil, Commentf("subtest %d: %v (%s)", i, err, t.commit))
+
+ file, err := commit.File(t.path)
+ c.Assert(err, IsNil)
+ c.Assert(file.Contents(), Equals, t.contents, Commentf(
+ "subtest %d: commit=%s, path=%s", i, t.commit, t.path))
+ }
+}
+
+var linesTests = []struct {
+ repo string // the repo name as in localRepos
+ commit string // the commit to search for the file
+ path string // the path of the file to find
+ lines []string // expected lines in the file
+}{
+ {
+ "https://github.com/tyba/git-fixture.git",
+ "b029517f6300c2da0f4b651b8642506cd6aaf45d",
+ ".gitignore",
+ []string{
+ "*.class",
+ "",
+ "# Mobile Tools for Java (J2ME)",
+ ".mtj.tmp/",
+ "",
+ "# Package Files #",
+ "*.jar",
+ "*.war",
+ "*.ear",
+ "",
+ "# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml",
+ "hs_err_pid*",
+ },
+ },
+ {
+ "https://github.com/tyba/git-fixture.git",
+ "6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
+ "CHANGELOG",
+ []string{
+ "Initial changelog",
+ },
+ },
+}
+
+func (s *SuiteFile) TestLines(c *C) {
+ for i, t := range linesTests {
+ commit, err := s.repos[t.repo].Commit(core.NewHash(t.commit))
+ c.Assert(err, IsNil, Commentf("subtest %d: %v (%s)", i, err, t.commit))
+
+ file, err := commit.File(t.path)
+ c.Assert(err, IsNil)
+ c.Assert(file.Lines(), DeepEquals, t.lines, Commentf(
+ "subtest %d: commit=%s, path=%s", i, t.commit, t.path))
+ }
+}
diff --git a/repository.go b/repository.go
index 32a6fcf..e63869a 100644
--- a/repository.go
+++ b/repository.go
@@ -20,6 +20,7 @@ const (
type Repository struct {
Remotes map[string]*Remote
Storage *core.RAWObjectStorage
+ URL string
}
// NewRepository creates a new repository setting remote as default remote
@@ -39,6 +40,7 @@ func NewRepository(url string, auth common.AuthMethod) (*Repository, error) {
r := NewPlainRepository()
r.Remotes[DefaultRemoteName] = remote
+ r.URL = url
return r, nil
}
diff --git a/repository_test.go b/repository_test.go
index a8fe50a..20aaf0c 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -1,9 +1,10 @@
package git
import (
- . "gopkg.in/check.v1"
"gopkg.in/src-d/go-git.v2/clients/http"
"gopkg.in/src-d/go-git.v2/core"
+
+ . "gopkg.in/check.v1"
)
type SuiteRepository struct{}
@@ -14,6 +15,7 @@ func (s *SuiteRepository) TestNewRepository(c *C) {
r, err := NewRepository(RepositoryFixture, nil)
c.Assert(err, IsNil)
c.Assert(r.Remotes["origin"].Auth, IsNil)
+ c.Assert(r.URL, Equals, RepositoryFixture)
}
func (s *SuiteRepository) TestNewRepositoryWithAuth(c *C) {
diff --git a/revlist/revlist.go b/revlist/revlist.go
new file mode 100644
index 0000000..bbc7e1f
--- /dev/null
+++ b/revlist/revlist.go
@@ -0,0 +1,274 @@
+// Package revlist allows to create the revision history of a file, this
+// is, the list of commits in the past that affect the file.
+//
+// The general idea is to traverse the git commit graph backward,
+// flattening the graph into a linear history, and skipping commits that
+// are irrelevant for the particular file.
+//
+// There is no single answer for this operation. The git command
+// "git-revlist" returns different histories depending on its arguments
+// and some internal heuristics.
+//
+// The current implementation tries to get something similar to what you
+// whould get using git-revlist. See the failing tests for some
+// insight about how the current implementation and git-revlist differs.
+//
+// Another way to get the revision history for a file is:
+// git log --follow -p -- file
+package revlist
+
+import (
+ "bytes"
+ "io"
+ "sort"
+
+ "gopkg.in/src-d/go-git.v2"
+ "gopkg.in/src-d/go-git.v2/core"
+ "gopkg.in/src-d/go-git.v2/diff"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+)
+
+// A Revs is a list of revisions for a file (basically a list of commits).
+// It implements sort.Interface using the commit time.
+type Revs []*git.Commit
+
+func (l Revs) Len() int {
+ return len(l)
+}
+
+// sorts from older to newer commit.
+func (l Revs) Less(i, j int) bool {
+ return l[i].Committer.When.Before(l[j].Committer.When)
+}
+
+func (l Revs) Swap(i, j int) {
+ l[i], l[j] = l[j], l[i]
+}
+
+// for debugging
+func (l Revs) GoString() string {
+ var buf bytes.Buffer
+ for _, c := range l {
+ buf.WriteString(c.Hash.String()[:8])
+ buf.WriteString("\n")
+ }
+ return buf.String()
+}
+
+// NewRevs returns a Revs pointer for the
+// file at "path", from commit "commit".
+// The commits are sorted in commit order.
+// It stops searching a branch for a file upon reaching the commit
+// were the file was created.
+// 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 NewRevs(repo *git.Repository, commit *git.Commit, path string) (Revs, error) {
+ result := make(Revs, 0)
+ seen := make(map[core.Hash]struct{}, 0)
+ err := walkGraph(&result, &seen, repo, commit, path)
+ if err != nil {
+ return nil, err
+ }
+ sort.Sort(result)
+ result, err = removeComp(path, result, equivalent) // for merges of identical cherry-picks
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+// Recursive traversal of the commit graph, generating a linear history
+// of the path.
+func walkGraph(result *Revs, seen *map[core.Hash]struct{}, repo *git.Repository, current *git.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 := parentsContainingPath(path, current)
+
+ 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, repo, 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, repo, p, path)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// TODO: benchmark this making git.Commit.parent public instead of using
+// an iterator
+func parentsContainingPath(path string, c *git.Commit) []*git.Commit {
+ var result []*git.Commit
+ iter := c.Parents()
+ for {
+ parent, err := iter.Next()
+ if err != nil {
+ if err == io.EOF {
+ return result
+ }
+ panic("unreachable")
+ }
+ 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 *git.Commit, cs []*git.Commit) ([]*git.Commit, error) {
+ result := make([]*git.Commit, 0, len(cs))
+ h, found := blobHash(path, c)
+ if !found {
+ return nil, git.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 *git.Commit) (hash core.Hash, found bool) {
+ file, err := commit.File(path)
+ if err != nil {
+ var empty core.Hash
+ return empty, found
+ }
+ return file.Hash, true
+}
+
+type contentsComparatorFn func(path string, a, b *git.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 []*git.Commit, comp contentsComparatorFn) ([]*git.Commit, error) {
+ result := make([]*git.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 *git.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 *git.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 := file.Contents()
+
+ // 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 = file.Contents()
+ }
+
+ // 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 git.CountLines(a.Text) == git.CountLines(b.Text)
+ case 1, -1:
+ return a.Text == b.Text
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/revlist/revlist2humantest.bash b/revlist/revlist2humantest.bash
new file mode 100755
index 0000000..b7d2672
--- /dev/null
+++ b/revlist/revlist2humantest.bash
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# you can run this over a whole repo with:
+#
+# for file in `find . -type f | sed 's/^\.\///' | egrep -v '^\.git\/.*$'` ; do revlist2humantest.bash $file ; done > /tmp/output
+#
+# be careful with files with spaces, though
+
+set -e
+
+repo=`git remote show origin | grep Fetch | cut -d' ' -f5`
+branch=`git branch | egrep '^\* .*' | cut -d' ' -f2`
+if [ "$#" -eq 1 ] ; then
+ commit=`git log | head -1 | cut -d' ' -f2`
+ path=$1
+elif [ "$#" -eq 2 ] ; then
+ commit=$1
+ path=$2
+else
+ echo "bad number of parameters" > /dev/stderr
+ echo > /dev/stderr
+ echo " try with: [commit] path" > /dev/stderr
+ exit
+fi
+
+hashes=`git rev-list --remove-empty --reverse $commit -- $path`
+
+# some remotes have the .git, other don't,
+# repoDot makes sure all have
+repoDot="${repo%.git}.git"
+
+echo -e "\t&humanTest{\"${repoDot}\", \"${branch}\", \"${commit}\", \"${path}\", []string{"
+for i in $hashes ; do
+ echo -e "\t\t\"${i}\","
+done
+echo -e "\t}},"
diff --git a/revlist/revlist_test.go b/revlist/revlist_test.go
new file mode 100644
index 0000000..2fe7c83
--- /dev/null
+++ b/revlist/revlist_test.go
@@ -0,0 +1,402 @@
+package revlist
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v2"
+ "gopkg.in/src-d/go-git.v2/core"
+ "gopkg.in/src-d/go-git.v2/formats/packfile"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct {
+ repos map[string]*git.Repository
+}
+
+var _ = Suite(&SuiteCommon{})
+
+var fixtureRepos = [...]struct {
+ url string
+ packfile string
+}{
+ {"https://github.com/tyba/git-fixture.git", "../formats/packfile/fixtures/git-fixture.ofs-delta"},
+ {"https://github.com/jamesob/desk.git", "../formats/packfile/fixtures/jamesob-desk.pack"},
+ {"https://github.com/spinnaker/spinnaker.git", "../formats/packfile/fixtures/spinnaker-spinnaker.pack"},
+}
+
+// create the repositories of the fixtures
+func (s *SuiteCommon) SetUpSuite(c *C) {
+ s.repos = make(map[string]*git.Repository, 0)
+ for _, fixRepo := range fixtureRepos {
+ s.repos[fixRepo.url] = git.NewPlainRepository()
+
+ d, err := os.Open(fixRepo.packfile)
+ defer d.Close()
+ c.Assert(err, IsNil)
+
+ r := packfile.NewReader(d)
+ r.Format = packfile.OFSDeltaFormat // TODO: how to know the format of a pack file ahead of time?
+
+ _, err = r.Read(s.repos[fixRepo.url].Storage)
+ c.Assert(err, IsNil)
+ }
+}
+
+var revListTests = [...]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/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "binary.jpg", []string{
+ "35e85108805c84807bc66a02d91535e1e24b38b9",
+ }},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "CHANGELOG", []string{
+ "b8e471f58bcbca63b07bda20e428190409c2db47",
+ }},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "go/example.go", []string{
+ "918c48b83bd081e863dbe1b80f8998f058cd8294",
+ }},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "json/long.json", []string{
+ "af2d6a6954d532f8ffb47615169c8fdf9d383a1a",
+ }},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "json/short.json", []string{
+ "af2d6a6954d532f8ffb47615169c8fdf9d383a1a",
+ }},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "LICENSE", []string{
+ "b029517f6300c2da0f4b651b8642506cd6aaf45d",
+ }},
+ {"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", "php/crappy.php", []string{
+ "918c48b83bd081e863dbe1b80f8998f058cd8294",
+ }},
+ {"https://github.com/tyba/git-fixture.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",
+ }},
+
+ // 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 *SuiteCommon) TestRevList(c *C) {
+ for _, t := range revListTests {
+ repo, ok := s.repos[t.repo]
+ c.Assert(ok, Equals, true)
+
+ commit, err := repo.Commit(core.NewHash(t.commit))
+ c.Assert(err, IsNil)
+
+ revs, err := NewRevs(repo, commit, t.path)
+ c.Assert(err, IsNil, Commentf("\nrepo=%s, commit=%s, path=%s\n",
+ t.repo, t.commit, t.path))
+
+ c.Assert(len(revs), Equals, len(t.revs), Commentf("\nrepo=%s, commit=%s, path=%s\n EXPECTED (len %d)\n%s\n OBTAINED (len %d)\n%s\n",
+ t.repo, t.commit, t.path, len(t.revs), t.revs, len(revs), revs.GoString()))
+ for i := range revs {
+ if revs[i].Hash.String() != t.revs[i] {
+ commit, err := repo.Commit(core.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))
+ }
+ }
+ }
+ fmt.Printf("OK repo=%s, commit=%s, path=%s\n",
+ t.repo, t.commit, t.path)
+ }
+}
+
+// same length is assumed
+func compareSideBySide(a []string, b []*git.Commit) string {
+ var buf bytes.Buffer
+ buf.WriteString("\t EXPECTED OBTAINED ")
+ var sep string
+ var obtained string
+ for i := range a {
+ obtained = b[i].Hash.String()
+ if a[i] != obtained {
+ sep = "------"
+ } else {
+ sep = " "
+ }
+ buf.WriteString(fmt.Sprintf("\n%d", i+1))
+ buf.WriteString(sep)
+ buf.WriteString(a[i])
+ buf.WriteString(sep)
+ buf.WriteString(obtained)
+ }
+ return buf.String()
+}
+
+var cherryPicks = [...][]string{
+ // repo, path, commit a, commit b
+ []string{"https://github.com/jamesob/desk.git", "desk", "094d0e7d5d69141c98a606910ba64786c5565da0", "3f34438d54f4a1ca86db8c0f03ed8eb38f20e22c"},
+}
+
+// should detect cherry picks
+func (s *SuiteCommon) 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 *SuiteCommon) commits(cc *C, repo string, hs ...string) []*git.Commit {
+ r, ok := s.repos[repo]
+ cc.Assert(ok, Equals, true)
+ result := make([]*git.Commit, 0, len(hs))
+ for _, h := range hs {
+ c, err := r.Commit(core.NewHash(h))
+ cc.Assert(err, IsNil)
+ result = append(result, c)
+ }
+ return result
+}
diff --git a/tree.go b/tree.go
index 2dcc5af..2ca084e 100644
--- a/tree.go
+++ b/tree.go
@@ -50,7 +50,7 @@ func (t *Tree) walkEntries(base string, ch chan *File) {
blob := &Blob{}
blob.Decode(obj)
- ch <- &File{Name: filepath.Join(base, entry.Name), Reader: blob.Reader()}
+ ch <- &File{Name: filepath.Join(base, entry.Name), Reader: blob.Reader(), Hash: entry.Hash}
}
}
@@ -115,8 +115,3 @@ func (i *TreeIter) Next() (*Tree, error) {
tree := &Tree{r: i.r}
return tree, tree.Decode(obj)
}
-
-type File struct {
- Name string
- io.Reader
-}