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

Implement git log --all #1045

Merged
merged 3 commits into from
Jan 11, 2019
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
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