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

Commit

Permalink
Merge pull request #1045 from kuba--/enh-1024/log-all
Browse files Browse the repository at this point in the history
Implement git log --all
  • Loading branch information
mcuadros authored Jan 11, 2019
2 parents 791aea3 + c9609eb commit 434611b
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 32 deletions.
5 changes: 5 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ type LogOptions struct {
// Show only those commits in which the specified file was inserted/updated.
// It is equivalent to running `git log -- <file-name>`.
FileName *string

// Pretend as if all the refs in refs/, along with HEAD, are listed on the command line as <commit>.
// It is equivalent to running `git log --all`.
// If set on true, the From option will be ignored.
All bool
}

var (
Expand Down
132 changes: 132 additions & 0 deletions plumbing/object/commit_walker.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package object

import (
"container/list"
"io"

"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/storer"
"gopkg.in/src-d/go-git.v4/storage"
)

type commitPreIterator struct {
Expand Down Expand Up @@ -181,3 +183,133 @@ func (w *commitPostIterator) ForEach(cb func(*Commit) error) error {
}

func (w *commitPostIterator) Close() {}

// commitAllIterator stands for commit iterator for all refs.
type commitAllIterator struct {
// currCommit points to the current commit.
currCommit *list.Element
}

// NewCommitAllIter returns a new commit iterator for all refs.
// repoStorer is a repo Storer used to get commits and references.
// commitIterFunc is a commit iterator function, used to iterate through ref commits in chosen order
func NewCommitAllIter(repoStorer storage.Storer, commitIterFunc func(*Commit) CommitIter) (CommitIter, error) {
commitsPath := list.New()
commitsLookup := make(map[plumbing.Hash]*list.Element)
head, err := storer.ResolveReference(repoStorer, plumbing.HEAD)
if err != nil {
return nil, err
}

// add all references along with the HEAD
if err = addReference(repoStorer, commitIterFunc, head, commitsPath, commitsLookup); err != nil {
return nil, err
}
refIter, err := repoStorer.IterReferences()
if err != nil {
return nil, err
}
defer refIter.Close()
err = refIter.ForEach(
func(ref *plumbing.Reference) error {
return addReference(repoStorer, commitIterFunc, ref, commitsPath, commitsLookup)
},
)
if err != nil {
return nil, err
}

return &commitAllIterator{commitsPath.Front()}, nil
}

func addReference(
repoStorer storage.Storer,
commitIterFunc func(*Commit) CommitIter,
ref *plumbing.Reference,
commitsPath *list.List,
commitsLookup map[plumbing.Hash]*list.Element) error {

_, exists := commitsLookup[ref.Hash()]
if exists {
// we already have it - skip the reference.
return nil
}

refCommit, _ := GetCommit(repoStorer, ref.Hash())
if refCommit == nil {
// if it's not a commit - skip it.
return nil
}

var (
refCommits []*Commit
parent *list.Element
)
// collect all ref commits to add
commitIter := commitIterFunc(refCommit)
for c, e := commitIter.Next(); e == nil; {
parent, exists = commitsLookup[c.Hash]
if exists {
break
}
refCommits = append(refCommits, c)
c, e = commitIter.Next()
}
commitIter.Close()

if parent == nil {
// common parent - not found
// add all commits to the path from this ref (maybe it's a HEAD and we don't have anything, yet)
for _, c := range refCommits {
parent = commitsPath.PushBack(c)
commitsLookup[c.Hash] = parent
}
} else {
// add ref's commits to the path in reverse order (from the latest)
for i := len(refCommits) - 1; i >= 0; i-- {
c := refCommits[i]
// insert before found common parent
parent = commitsPath.InsertBefore(c, parent)
commitsLookup[c.Hash] = parent
}
}

return nil
}

func (it *commitAllIterator) Next() (*Commit, error) {
if it.currCommit == nil {
return nil, io.EOF
}

c := it.currCommit.Value.(*Commit)
it.currCommit = it.currCommit.Next()

return c, nil
}

func (it *commitAllIterator) ForEach(cb func(*Commit) error) error {
for {
c, err := it.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}

err = cb(c)
if err == storer.ErrStop {
break
}
if err != nil {
return err
}
}

return nil
}

func (it *commitAllIterator) Close() {
it.currCommit = nil
}
50 changes: 40 additions & 10 deletions plumbing/object/commit_walker_file.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package object

import (
"gopkg.in/src-d/go-git.v4/plumbing/storer"
"io"

"gopkg.in/src-d/go-git.v4/plumbing"

"gopkg.in/src-d/go-git.v4/plumbing/storer"
)

type commitFileIter struct {
fileName string
sourceIter CommitIter
currentCommit *Commit
checkParent bool
}

// 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 {
// If checkParent is true then the function double checks if potential parent (next commit in a path)
// is one of the parents in the tree (it's used by `git log --all`).
func NewCommitFileIterFromIter(fileName string, commitIter CommitIter, checkParent bool) CommitIter {
iterator := new(commitFileIter)
iterator.sourceIter = commitIter
iterator.fileName = fileName
iterator.checkParent = checkParent
return iterator
}

Expand Down Expand Up @@ -71,20 +78,14 @@ func (c *commitFileIter) getNextFileCommit() (*Commit, error) {
return nil, diffErr
}

foundChangeForFile := false
for _, change := range changes {
if change.name() == c.fileName {
foundChangeForFile = true
break
}
}
found := c.hasFileChange(changes, parentCommit)

// 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 {
if found {
return prevCommit, nil
}

Expand All @@ -95,6 +96,35 @@ func (c *commitFileIter) getNextFileCommit() (*Commit, error) {
}
}

func (c *commitFileIter) hasFileChange(changes Changes, parent *Commit) bool {
for _, change := range changes {
if change.name() != c.fileName {
continue
}

// filename matches, now check if source iterator contains all commits (from all refs)
if c.checkParent {
if parent != nil && isParentHash(parent.Hash, c.currentCommit) {
return true
}
continue
}

return true
}

return false
}

func isParentHash(hash plumbing.Hash, commit *Commit) bool {
for _, h := range commit.ParentHashes {
if h == hash {
return true
}
}
return false
}

func (c *commitFileIter) ForEach(cb func(*Commit) error) error {
for {
commit, nextErr := c.Next()
Expand Down
74 changes: 58 additions & 16 deletions repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -1027,8 +1027,36 @@ func (r *Repository) PushContext(ctx context.Context, o *PushOptions) error {

// Log returns the commit history from the given LogOptions.
func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) {
h := o.From
if o.From == plumbing.ZeroHash {
fn := commitIterFunc(o.Order)
if fn == nil {
return nil, fmt.Errorf("invalid Order=%v", o.Order)
}

var (
it object.CommitIter
err error
)
if o.All {
it, err = r.logAll(fn)
} else {
it, err = r.log(o.From, fn)
}

if err != nil {
return nil, err
}

if o.FileName != nil {
// for `git log --all` also check parent (if the next commit comes from the real parent)
it = r.logWithFile(*o.FileName, it, o.All)
}

return it, nil
}

func (r *Repository) log(from plumbing.Hash, commitIterFunc func(*object.Commit) object.CommitIter) (object.CommitIter, error) {
h := from
if from == plumbing.ZeroHash {
head, err := r.Head()
if err != nil {
return nil, err
Expand All @@ -1041,27 +1069,41 @@ func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) {
if err != nil {
return nil, err
}
return commitIterFunc(commit), nil
}

var commitIter object.CommitIter
switch o.Order {
func (r *Repository) logAll(commitIterFunc func(*object.Commit) object.CommitIter) (object.CommitIter, error) {
return object.NewCommitAllIter(r.Storer, commitIterFunc)
}

func (*Repository) logWithFile(fileName string, commitIter object.CommitIter, checkParent bool) object.CommitIter {
return object.NewCommitFileIterFromIter(fileName, commitIter, checkParent)
}

func commitIterFunc(order LogOrder) func(c *object.Commit) object.CommitIter {
switch order {
case LogOrderDefault:
commitIter = object.NewCommitPreorderIter(commit, nil, nil)
return func(c *object.Commit) object.CommitIter {
return object.NewCommitPreorderIter(c, nil, nil)
}
case LogOrderDFS:
commitIter = object.NewCommitPreorderIter(commit, nil, nil)
return func(c *object.Commit) object.CommitIter {
return object.NewCommitPreorderIter(c, nil, nil)
}
case LogOrderDFSPost:
commitIter = object.NewCommitPostorderIter(commit, nil)
return func(c *object.Commit) object.CommitIter {
return object.NewCommitPostorderIter(c, nil)
}
case LogOrderBSF:
commitIter = object.NewCommitIterBSF(commit, nil, nil)
return func(c *object.Commit) object.CommitIter {
return object.NewCommitIterBSF(c, nil, nil)
}
case LogOrderCommitterTime:
commitIter = object.NewCommitIterCTime(commit, nil, nil)
default:
return nil, fmt.Errorf("invalid Order=%v", o.Order)
}

if o.FileName == nil {
return commitIter, nil
return func(c *object.Commit) object.CommitIter {
return object.NewCommitIterCTime(c, nil, nil)
}
}
return object.NewCommitFileIterFromIter(*o.FileName, commitIter), nil
return nil
}

// Tags returns all the tag References in a repository.
Expand Down
Loading

0 comments on commit 434611b

Please sign in to comment.