From 8597ed895053fab44870ef79e3ea77a52d590b89 Mon Sep 17 00:00:00 2001 From: Nithin Date: Mon, 8 Oct 2018 02:57:27 +0530 Subject: [PATCH 1/3] [log-file] plumbing: object, Add support for Log with filenames. Fixes #826 Signed-off-by: Nithin --- options.go | 2 + plumbing/object/commit_walker_file.go | 114 +++++++++++++++++++++ repository.go | 19 ++-- repository_test.go | 139 ++++++++++++++++++++++++++ 4 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 plumbing/object/commit_walker_file.go diff --git a/options.go b/options.go index b5727703c..6e000e72b 100644 --- a/options.go +++ b/options.go @@ -330,6 +330,8 @@ type LogOptions struct { // set Order=LogOrderCommitterTime for ordering by committer time (more compatible with `git log`) // set Order=LogOrderBSF for Breadth-first search Order LogOrder + + FileName *string } var ( diff --git a/plumbing/object/commit_walker_file.go b/plumbing/object/commit_walker_file.go new file mode 100644 index 000000000..01288a786 --- /dev/null +++ b/plumbing/object/commit_walker_file.go @@ -0,0 +1,114 @@ +package object + +import ( + "gopkg.in/src-d/go-git.v4/plumbing/storer" + "io" +) + +type commitFileIter struct { + fileName string + sourceIter CommitIter + currentCommit *Commit +} + +// NewCommitFileIterFromIter returns a commit iterator which performs diffTree between +// successive trees returned from the commit iterator from the argument. The purpose of this is +// to find the commits that explain how the files that match the path came to be. +func NewCommitFileIterFromIter(fileName string, commitIter CommitIter) CommitIter { + iterator := new(commitFileIter) + iterator.sourceIter = commitIter + iterator.fileName = fileName + return iterator +} + +func (c *commitFileIter) Next() (*Commit, error) { + var err error + if c.currentCommit == nil { + c.currentCommit, err = c.sourceIter.Next() + if err != nil { + return nil, err + } + } + + for { + // Parent-commit can be nil if the current-commit is the initial commit + parentCommit, parentCommitErr := c.sourceIter.Next() + if parentCommitErr != nil { + if parentCommitErr != io.EOF { + err = parentCommitErr + break + } + parentCommit = nil + } + + // Fetch the trees of the current and parent commits + currentTree, currTreeErr := c.currentCommit.Tree() + if currTreeErr != nil { + err = currTreeErr + break + } + + var parentTree *Tree + if parentCommit != nil { + var parentTreeErr error + parentTree, parentTreeErr = parentCommit.Tree() + if parentTreeErr != nil { + err = parentTreeErr + break + } + } + + // Find diff between current and parent trees + changes, diffErr := DiffTree(currentTree, parentTree) + if diffErr != nil { + err = diffErr + break + } + + foundChangeForFile := false + for _, change := range changes { + if change.name() == c.fileName { + foundChangeForFile = true + break + } + } + + // Storing the current-commit in-case a change is found, and + // Updating the current-commit for the next-iteration + prevCommit := c.currentCommit + c.currentCommit = parentCommit + + if foundChangeForFile == true { + return prevCommit, nil + } + + // If there are no more commits to be found, then return with EOF + if parentCommit == nil { + err = io.EOF + break + } + } + + // Setting current-commit to nil to prevent unwanted states when errors are raised + c.currentCommit = nil + return nil, err +} + +func (c *commitFileIter) ForEach(cb func(*Commit) error) error { + for { + commit, nextErr := c.Next() + if nextErr != nil { + return nextErr + } + err := cb(commit) + if err == storer.ErrStop { + return nil + } else if err != nil { + return err + } + } +} + +func (c *commitFileIter) Close() { + c.sourceIter.Close() +} diff --git a/repository.go b/repository.go index be1f0574f..62e22a6b6 100644 --- a/repository.go +++ b/repository.go @@ -965,19 +965,26 @@ func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) { return nil, err } + var commitIter object.CommitIter switch o.Order { case LogOrderDefault: - return object.NewCommitPreorderIter(commit, nil, nil), nil + commitIter = object.NewCommitPreorderIter(commit, nil, nil) case LogOrderDFS: - return object.NewCommitPreorderIter(commit, nil, nil), nil + commitIter = object.NewCommitPreorderIter(commit, nil, nil) case LogOrderDFSPost: - return object.NewCommitPostorderIter(commit, nil), nil + commitIter = object.NewCommitPostorderIter(commit, nil) case LogOrderBSF: - return object.NewCommitIterBSF(commit, nil, nil), nil + commitIter = object.NewCommitIterBSF(commit, nil, nil) case LogOrderCommitterTime: - return object.NewCommitIterCTime(commit, nil, nil), nil + commitIter = object.NewCommitIterCTime(commit, nil, nil) + default: + return nil, fmt.Errorf("invalid Order=%v", o.Order) + } + + if o.FileName == nil { + return commitIter, nil } - return nil, fmt.Errorf("invalid Order=%v", o.Order) + return object.NewCommitFileIterFromIter(*o.FileName, commitIter), nil } // Tags returns all the tag References in a repository. diff --git a/repository_test.go b/repository_test.go index 2710d9d0d..bf02933c7 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1143,6 +1143,145 @@ func (s *RepositorySuite) TestLogError(c *C) { c.Assert(err, NotNil) } +func (s *RepositorySuite) TestLogFileNext(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + + c.Assert(err, IsNil) + + fileName := "vendor/foo.go" + cIter, err := r.Log(&LogOptions{FileName: &fileName}) + + c.Assert(err, IsNil) + + commitOrder := []plumbing.Hash{ + plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"), + } + + for _, o := range commitOrder { + commit, err := cIter.Next() + c.Assert(err, IsNil) + c.Assert(commit.Hash, Equals, o) + } + _, err = cIter.Next() + c.Assert(err, Equals, io.EOF) +} + +func (s *RepositorySuite) TestLogFileForEach(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + + c.Assert(err, IsNil) + + fileName := "php/crappy.php" + cIter, err := r.Log(&LogOptions{FileName: &fileName}) + + c.Assert(err, IsNil) + + commitOrder := []plumbing.Hash{ + plumbing.NewHash("918c48b83bd081e863dbe1b80f8998f058cd8294"), + } + + expectedIndex := 0 + cIter.ForEach(func(commit *object.Commit) error { + expectedCommitHash := commitOrder[expectedIndex] + c.Assert(commit.Hash.String(), Equals, expectedCommitHash.String()) + expectedIndex += 1 + return nil + }) + c.Assert(expectedIndex, Equals, 1) +} + +func (s *RepositorySuite) TestLogInvalidFile(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + + // Throwing in a file that does not exist + fileName := "vendor/foo12.go" + cIter, err := r.Log(&LogOptions{FileName: &fileName}) + // Not raising an error since `git log -- vendor/foo12.go` responds silently + c.Assert(err, IsNil) + + _, err = cIter.Next() + c.Assert(err, Equals, io.EOF) +} + +func (s *RepositorySuite) TestLogFileInitialCommit(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + + fileName := "LICENSE" + cIter, err := r.Log(&LogOptions{ + Order: LogOrderCommitterTime, + FileName: &fileName, + }) + + c.Assert(err, IsNil) + + commitOrder := []plumbing.Hash{ + plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d"), + } + + expectedIndex := 0 + cIter.ForEach(func(commit *object.Commit) error { + expectedCommitHash := commitOrder[expectedIndex] + c.Assert(commit.Hash.String(), Equals, expectedCommitHash.String()) + expectedIndex += 1 + return nil + }) + c.Assert(expectedIndex, Equals, 1) +} + +func (s *RepositorySuite) TestLogFileWithOtherParamsFail(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + + fileName := "vendor/foo.go" + cIter, err := r.Log(&LogOptions{ + Order: LogOrderCommitterTime, + FileName: &fileName, + From: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), + }) + c.Assert(err, IsNil) + _, iterErr := cIter.Next() + c.Assert(iterErr, Equals, io.EOF) +} + +func (s *RepositorySuite) TestLogFileWithOtherParamsPass(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + + fileName := "LICENSE" + cIter, err := r.Log(&LogOptions{ + Order: LogOrderCommitterTime, + FileName: &fileName, + From: plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), + }) + c.Assert(err, IsNil) + commitVal, iterErr := cIter.Next() + c.Assert(iterErr, Equals, nil) + c.Assert(commitVal.Hash.String(), Equals, "b029517f6300c2da0f4b651b8642506cd6aaf45d") + + _, iterErr = cIter.Next() + c.Assert(iterErr, Equals, io.EOF) +} + func (s *RepositorySuite) TestCommit(c *C) { r, _ := Init(memory.NewStorage(), nil) err := r.clone(context.Background(), &CloneOptions{ From 1aaf455ca2950f6d23c69f5038ba869c2f181362 Mon Sep 17 00:00:00 2001 From: Nithin Date: Tue, 9 Oct 2018 12:22:10 +0530 Subject: [PATCH 2/3] [log-file] Add documentation for FileName option Signed-off-by: Nithin --- options.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options.go b/options.go index 6e000e72b..b8bc1e979 100644 --- a/options.go +++ b/options.go @@ -331,6 +331,8 @@ type LogOptions struct { // set Order=LogOrderBSF for Breadth-first search Order LogOrder + // Show only those commits in which the specified file was inserted/updated. + // It is equivalent to running `git log -- `. FileName *string } From 47e3f8208a6ed0e924ceeb0decf22b11616fdd48 Mon Sep 17 00:00:00 2001 From: Nithin Date: Tue, 9 Oct 2018 12:43:58 +0530 Subject: [PATCH 3/3] [log-file] Move the log-file logic into a function Signed-off-by: Nithin --- plumbing/object/commit_walker_file.go | 33 ++++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/plumbing/object/commit_walker_file.go b/plumbing/object/commit_walker_file.go index 01288a786..84e738ac6 100644 --- a/plumbing/object/commit_walker_file.go +++ b/plumbing/object/commit_walker_file.go @@ -22,21 +22,30 @@ func NewCommitFileIterFromIter(fileName string, commitIter CommitIter) CommitIte } func (c *commitFileIter) Next() (*Commit, error) { - var err error if c.currentCommit == nil { + var err error c.currentCommit, err = c.sourceIter.Next() if err != nil { return nil, err } } + commit, commitErr := c.getNextFileCommit() + // Setting current-commit to nil to prevent unwanted states when errors are raised + if commitErr != nil { + c.currentCommit = nil + } + return commit, commitErr +} + +func (c *commitFileIter) getNextFileCommit() (*Commit, error) { for { // Parent-commit can be nil if the current-commit is the initial commit parentCommit, parentCommitErr := c.sourceIter.Next() if parentCommitErr != nil { + // If the parent-commit is beyond the initial commit, keep it nil if parentCommitErr != io.EOF { - err = parentCommitErr - break + return nil, parentCommitErr } parentCommit = nil } @@ -44,8 +53,7 @@ func (c *commitFileIter) Next() (*Commit, error) { // Fetch the trees of the current and parent commits currentTree, currTreeErr := c.currentCommit.Tree() if currTreeErr != nil { - err = currTreeErr - break + return nil, currTreeErr } var parentTree *Tree @@ -53,16 +61,14 @@ func (c *commitFileIter) Next() (*Commit, error) { var parentTreeErr error parentTree, parentTreeErr = parentCommit.Tree() if parentTreeErr != nil { - err = parentTreeErr - break + return nil, parentTreeErr } } // Find diff between current and parent trees changes, diffErr := DiffTree(currentTree, parentTree) if diffErr != nil { - err = diffErr - break + return nil, diffErr } foundChangeForFile := false @@ -82,16 +88,11 @@ func (c *commitFileIter) Next() (*Commit, error) { return prevCommit, nil } - // If there are no more commits to be found, then return with EOF + // If not matches found and if parent-commit is beyond the initial commit, then return with EOF if parentCommit == nil { - err = io.EOF - break + return nil, io.EOF } } - - // Setting current-commit to nil to prevent unwanted states when errors are raised - c.currentCommit = nil - return nil, err } func (c *commitFileIter) ForEach(cb func(*Commit) error) error {