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

repository.Log: add alternatives for commit traversal order #771

Merged
merged 3 commits into from
Mar 12, 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
15 changes: 15 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,27 @@ func (o *ResetOptions) Validate(r *Repository) error {
return nil
}

type LogOrder int8

const (
LogOrderDefault LogOrder = iota
LogOrderDFS
LogOrderDFSPost
LogOrderBSF
LogOrderCommitterTime
)

// LogOptions describes how a log action should be performed.
type LogOptions struct {
// When the From option is set the log will only contain commits
// reachable from it. If this option is not set, HEAD will be used as
// the default From.
From plumbing.Hash

// The default traversal algorithm is Depth-first search
// set Order=LogOrderCommitterTime for ordering by committer time (more compatible with `git log`)
// set Order=LogOrderBSF for Breadth-first search
Order LogOrder
}

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

import (
"io"

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

type bfsCommitIterator struct {
seenExternal map[plumbing.Hash]bool
seen map[plumbing.Hash]bool
queue []*Commit
}

// NewCommitIterBSF returns a CommitIter that walks the commit history,
// starting at the given commit and visiting its parents in pre-order.
// The given callback will be called for each visited commit. Each commit will
// be visited only once. If the callback returns an error, walking will stop
// and will return the error. Other errors might be returned if the history
// cannot be traversed (e.g. missing objects). Ignore allows to skip some
// commits from being iterated.
func NewCommitIterBSF(
c *Commit,
seenExternal map[plumbing.Hash]bool,
ignore []plumbing.Hash,
) CommitIter {
seen := make(map[plumbing.Hash]bool)
for _, h := range ignore {
seen[h] = true
}

return &bfsCommitIterator{
seenExternal: seenExternal,
seen: seen,
queue: []*Commit{c},
}
}

func (w *bfsCommitIterator) appendHash(store storer.EncodedObjectStorer, h plumbing.Hash) error {
if w.seen[h] || w.seenExternal[h] {
return nil
}
c, err := GetCommit(store, h)
if err != nil {
return err
}
w.queue = append(w.queue, c)
return nil
}

func (w *bfsCommitIterator) Next() (*Commit, error) {
var c *Commit
for {
if len(w.queue) == 0 {
return nil, io.EOF
}
c = w.queue[0]
w.queue = w.queue[1:]

if w.seen[c.Hash] || w.seenExternal[c.Hash] {
continue
}

w.seen[c.Hash] = true

for _, h := range c.ParentHashes {
err := w.appendHash(c.s, h)
if err != nil {
return nil, nil
}
}

return c, nil
}
}

func (w *bfsCommitIterator) ForEach(cb func(*Commit) error) error {
for {
c, err := w.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 (w *bfsCommitIterator) Close() {}
103 changes: 103 additions & 0 deletions plumbing/object/commit_walker_ctime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package object

import (
"io"

"github.com/emirpasic/gods/trees/binaryheap"

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

type commitIteratorByCTime struct {
seenExternal map[plumbing.Hash]bool
seen map[plumbing.Hash]bool
heap *binaryheap.Heap
}

// NewCommitIterCTime returns a CommitIter that walks the commit history,
// starting at the given commit and visiting its parents while preserving Committer Time order.
// this appears to be the closest order to `git log`
// The given callback will be called for each visited commit. Each commit will
// be visited only once. If the callback returns an error, walking will stop
// and will return the error. Other errors might be returned if the history
// cannot be traversed (e.g. missing objects). Ignore allows to skip some
// commits from being iterated.
func NewCommitIterCTime(
c *Commit,
seenExternal map[plumbing.Hash]bool,
ignore []plumbing.Hash,
) CommitIter {
seen := make(map[plumbing.Hash]bool)
for _, h := range ignore {
seen[h] = true
}

heap := binaryheap.NewWith(func(a, b interface{}) int {
if a.(*Commit).Committer.When.Before(b.(*Commit).Committer.When) {
return 1
}
return -1
})
heap.Push(c)

return &commitIteratorByCTime{
seenExternal: seenExternal,
seen: seen,
heap: heap,
}
}

func (w *commitIteratorByCTime) Next() (*Commit, error) {
var c *Commit
for {
cIn, ok := w.heap.Pop()
if !ok {
return nil, io.EOF
}
c = cIn.(*Commit)

if w.seen[c.Hash] || w.seenExternal[c.Hash] {
continue
}

w.seen[c.Hash] = true

for _, h := range c.ParentHashes {
if w.seen[h] || w.seenExternal[h] {
continue
}
pc, err := GetCommit(c.s, h)
if err != nil {
return nil, err
}
w.heap.Push(pc)
}

return c, nil
}
}

func (w *commitIteratorByCTime) ForEach(cb func(*Commit) error) error {
for {
c, err := w.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 (w *commitIteratorByCTime) Close() {}
96 changes: 96 additions & 0 deletions plumbing/object/commit_walker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,99 @@ func (s *CommitWalkerSuite) TestCommitPostIteratorWithIgnore(c *C) {
c.Assert(commit.Hash.String(), Equals, expected[i])
}
}

func (s *CommitWalkerSuite) TestCommitCTimeIterator(c *C) {
commit := s.commit(c, s.Fixture.Head)

var commits []*Commit
NewCommitIterCTime(commit, nil, nil).ForEach(func(c *Commit) error {
commits = append(commits, c)
return nil
})

c.Assert(commits, HasLen, 8)

expected := []string{
"6ecf0ef2c2dffb796033e5a02219af86ec6584e5", // 2015-04-05T23:30:47+02:00
"918c48b83bd081e863dbe1b80f8998f058cd8294", // 2015-03-31T13:56:18+02:00
"af2d6a6954d532f8ffb47615169c8fdf9d383a1a", // 2015-03-31T13:51:51+02:00
"1669dce138d9b841a518c64b10914d88f5e488ea", // 2015-03-31T13:48:14+02:00
"a5b8b09e2f8fcb0bb99d3ccb0958157b40890d69", // 2015-03-31T13:47:14+02:00
"35e85108805c84807bc66a02d91535e1e24b38b9", // 2015-03-31T13:46:24+02:00
"b8e471f58bcbca63b07bda20e428190409c2db47", // 2015-03-31T13:44:52+02:00
"b029517f6300c2da0f4b651b8642506cd6aaf45d", // 2015-03-31T13:42:21+02:00
}
for i, commit := range commits {
c.Assert(commit.Hash.String(), Equals, expected[i])
}
}

func (s *CommitWalkerSuite) TestCommitCTimeIteratorWithIgnore(c *C) {
commit := s.commit(c, s.Fixture.Head)

var commits []*Commit
NewCommitIterCTime(commit, nil, []plumbing.Hash{
plumbing.NewHash("af2d6a6954d532f8ffb47615169c8fdf9d383a1a"),
}).ForEach(func(c *Commit) error {
commits = append(commits, c)
return nil
})

c.Assert(commits, HasLen, 2)

expected := []string{
"6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
"918c48b83bd081e863dbe1b80f8998f058cd8294",
}
for i, commit := range commits {
c.Assert(commit.Hash.String(), Equals, expected[i])
}
}

func (s *CommitWalkerSuite) TestCommitBSFIterator(c *C) {
commit := s.commit(c, s.Fixture.Head)

var commits []*Commit
NewCommitIterBSF(commit, nil, nil).ForEach(func(c *Commit) error {
commits = append(commits, c)
return nil
})

c.Assert(commits, HasLen, 8)

expected := []string{
"6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
"918c48b83bd081e863dbe1b80f8998f058cd8294",
"af2d6a6954d532f8ffb47615169c8fdf9d383a1a",
"1669dce138d9b841a518c64b10914d88f5e488ea",
"35e85108805c84807bc66a02d91535e1e24b38b9",
"a5b8b09e2f8fcb0bb99d3ccb0958157b40890d69",
"b029517f6300c2da0f4b651b8642506cd6aaf45d",
"b8e471f58bcbca63b07bda20e428190409c2db47",
}
for i, commit := range commits {
c.Assert(commit.Hash.String(), Equals, expected[i])
}
}

func (s *CommitWalkerSuite) TestCommitBSFIteratorWithIgnore(c *C) {
commit := s.commit(c, s.Fixture.Head)

var commits []*Commit
NewCommitIterBSF(commit, nil, []plumbing.Hash{
plumbing.NewHash("af2d6a6954d532f8ffb47615169c8fdf9d383a1a"),
}).ForEach(func(c *Commit) error {
commits = append(commits, c)
return nil
})

c.Assert(commits, HasLen, 2)

expected := []string{
"6ecf0ef2c2dffb796033e5a02219af86ec6584e5",
"918c48b83bd081e863dbe1b80f8998f058cd8294",
}
for i, commit := range commits {
c.Assert(commit.Hash.String(), Equals, expected[i])
}
}
14 changes: 13 additions & 1 deletion repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,19 @@ func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) {
return nil, err
}

return object.NewCommitPreorderIter(commit, nil, nil), nil
switch o.Order {
case LogOrderDefault:
return object.NewCommitPreorderIter(commit, nil, nil), nil
case LogOrderDFS:
return object.NewCommitPreorderIter(commit, nil, nil), nil
case LogOrderDFSPost:
return object.NewCommitPostorderIter(commit, nil), nil
case LogOrderBSF:
return object.NewCommitIterBSF(commit, nil, nil), nil
case LogOrderCommitterTime:
return object.NewCommitIterCTime(commit, nil, nil), nil
}
return nil, fmt.Errorf("invalid Order=%v", o.Order)
}

// Tags returns all the References from Tags. This method returns all the tag
Expand Down
4 changes: 2 additions & 2 deletions repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ func (s *RepositorySuite) TestLog(c *C) {
c.Assert(err, IsNil)

cIter, err := r.Log(&LogOptions{
plumbing.NewHash("b8e471f58bcbca63b07bda20e428190409c2db47"),
From: plumbing.NewHash("b8e471f58bcbca63b07bda20e428190409c2db47"),
})

c.Assert(err, IsNil)
Expand Down Expand Up @@ -930,7 +930,7 @@ func (s *RepositorySuite) TestLogError(c *C) {
c.Assert(err, IsNil)

_, err = r.Log(&LogOptions{
plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
From: plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
})
c.Assert(err, NotNil)
}
Expand Down