Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

plumbing: object, Add support for Log with filenames. Fixes #826 #979

Merged
merged 3 commits into from
Oct 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you document this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// Show only those commits in which the specified file was inserted/updated.
// It is equivalent to running `git log -- <file-name>`.
FileName *string
}

var (
Expand Down
115 changes: 115 additions & 0 deletions plumbing/object/commit_walker_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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) {
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move this logic to a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// 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 {
return nil, parentCommitErr
}
parentCommit = nil
}

// Fetch the trees of the current and parent commits
currentTree, currTreeErr := c.currentCommit.Tree()
if currTreeErr != nil {
return nil, currTreeErr
}

var parentTree *Tree
if parentCommit != nil {
var parentTreeErr error
parentTree, parentTreeErr = parentCommit.Tree()
if parentTreeErr != nil {
return nil, parentTreeErr
}
}

// Find diff between current and parent trees
changes, diffErr := DiffTree(currentTree, parentTree)
if diffErr != nil {
return nil, diffErr
}

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 not matches found and if parent-commit is beyond the initial commit, then return with EOF
if parentCommit == nil {
return nil, io.EOF
}
}
}

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()
}
19 changes: 13 additions & 6 deletions repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
139 changes: 139 additions & 0 deletions repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down