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

Commit 9df17e5

Browse files
committed
New iteration behavior via FileIter and TreeWalker
Instead of returning a channel of files, Tree.Files() now returns a FileIter with these qualities: * It returns files in the original order of the repository (relying on a * new Tree.OrderedNames property) * It can return errors encountered when retrieving files and trees from * underlying storage * It can be Closed without having to drain the entire channel * It defers the heavy lifting to a new TreeWalker type * Its behavior is a little more consistent with other Iter types * It's a little less prone to memory leaks This update includes a new TreeWalker type that will iterate through all of the entries of a tree and its descendant subtrees. It does the dirty work that Tree.walkEntries() used to do, but with a public API. A new TreeIter type is also included that just walks through subtrees. This could be useful for performing a directory search while ignoring files/blobs altogether.
1 parent 6b0a598 commit 9df17e5

File tree

11 files changed

+402
-62
lines changed

11 files changed

+402
-62
lines changed

commit_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func (s *SuiteCommit) SetUpSuite(c *C) {
4141
}
4242
}
4343

44-
var iterTests = []struct {
44+
var commitIterTests = []struct {
4545
repo string // the repo name in the test suite's map of fixtures
4646
commits []string // the commit hashes to iterate over in the test
4747
}{
@@ -59,15 +59,15 @@ var iterTests = []struct {
5959
}
6060

6161
func (s *SuiteCommit) TestIterSlice(c *C) {
62-
for i, t := range iterTests {
62+
for i, t := range commitIterTests {
6363
r := s.repos[t.repo]
6464
iter := NewCommitIter(r, core.NewObjectSliceIter(makeObjectSlice(t.commits, r.Storage)))
6565
s.checkIter(c, r, i, iter, t.commits)
6666
}
6767
}
6868

6969
func (s *SuiteCommit) TestIterLookup(c *C) {
70-
for i, t := range iterTests {
70+
for i, t := range commitIterTests {
7171
r := s.repos[t.repo]
7272
iter := NewCommitIter(r, core.NewObjectLookupIter(r.Storage, makeHashSlice(t.commits)))
7373
s.checkIter(c, r, i, iter, t.commits)
@@ -85,15 +85,15 @@ func (s *SuiteCommit) checkIter(c *C, r *Repository, subtest int, iter *CommitIt
8585
}
8686

8787
func (s *SuiteCommit) TestIterSliceClose(c *C) {
88-
for i, t := range iterTests {
88+
for i, t := range commitIterTests {
8989
r := s.repos[t.repo]
9090
iter := NewCommitIter(r, core.NewObjectSliceIter(makeObjectSlice(t.commits, r.Storage)))
9191
s.checkIterClose(c, i, iter)
9292
}
9393
}
9494

9595
func (s *SuiteCommit) TestIterLookupClose(c *C) {
96-
for i, t := range iterTests {
96+
for i, t := range commitIterTests {
9797
r := s.repos[t.repo]
9898
iter := NewCommitIter(r, core.NewObjectLookupIter(r.Storage, makeHashSlice(t.commits)))
9999
s.checkIterClose(c, i, iter)

file.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,34 @@ func (f *File) Lines() []string {
3333
}
3434
return splits
3535
}
36+
37+
type FileIter struct {
38+
w TreeWalker
39+
}
40+
41+
func NewFileIter(r *Repository, t *Tree) *FileIter {
42+
return &FileIter{w: *NewTreeWalker(r, t)}
43+
}
44+
45+
func (iter *FileIter) Next() (*File, error) {
46+
for {
47+
name, entry, obj, err := iter.w.Next()
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
if obj.Type() != core.BlobObject {
53+
// Skip non-blob objects
54+
continue
55+
}
56+
57+
blob := &Blob{}
58+
blob.Decode(obj)
59+
60+
return &File{Name: name, Reader: blob.Reader(), Hash: entry.Hash}, nil
61+
}
62+
}
63+
64+
func (iter *FileIter) Close() {
65+
iter.w.Close()
66+
}

file_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package git
22

33
import (
4+
"io"
45
"os"
56

67
"gopkg.in/src-d/go-git.v3/core"
@@ -41,6 +42,49 @@ func (s *SuiteFile) SetUpSuite(c *C) {
4142
}
4243
}
4344

45+
type fileIterExpectedEntry struct {
46+
Name string
47+
Hash string
48+
}
49+
50+
var fileIterTests = []struct {
51+
repo string // the repo name as in localRepos
52+
commit string // the commit to search for the file
53+
files []fileIterExpectedEntry
54+
}{
55+
// https://api.github.com/repos/tyba/git-fixture/git/trees/6ecf0ef2c2dffb796033e5a02219af86ec6584e5
56+
{"https://github.com/tyba/git-fixture.git", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", []fileIterExpectedEntry{
57+
{".gitignore", "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88"},
58+
{"CHANGELOG", "d3ff53e0564a9f87d8e84b6e28e5060e517008aa"},
59+
{"LICENSE", "c192bd6a24ea1ab01d78686e417c8bdc7c3d197f"},
60+
{"binary.jpg", "d5c0f4ab811897cadf03aec358ae60d21f91c50d"},
61+
{"go/example.go", "880cd14280f4b9b6ed3986d6671f907d7cc2a198"},
62+
{"json/long.json", "49c6bb89b17060d7b4deacb7b338fcc6ea2352a9"},
63+
{"json/short.json", "c8f1d8c61f9da76f4cb49fd86322b6e685dba956"},
64+
{"php/crappy.php", "9a48f23120e880dfbe41f7c9b7b708e9ee62a492"},
65+
{"vendor/foo.go", "9dea2395f5403188298c1dabe8bdafe562c491e3"},
66+
}},
67+
}
68+
69+
func (s *SuiteFile) TestIter(c *C) {
70+
for i, t := range fileIterTests {
71+
r := s.repos[t.repo]
72+
commit, err := r.Commit(core.NewHash(t.commit))
73+
c.Assert(err, IsNil, Commentf("subtest %d: %v (%s)", i, err, t.commit))
74+
75+
iter := NewFileIter(r, commit.Tree())
76+
for k := 0; k < len(t.files); k++ {
77+
expected := t.files[k]
78+
file, err := iter.Next()
79+
c.Assert(err, IsNil, Commentf("subtest %d, iter %d, err=%v", i, k, err))
80+
c.Assert(file.Name, Equals, expected.Name, Commentf("subtest %d, iter %d, name=%s, expected=%s", i, k, file.Name, expected.Hash))
81+
c.Assert(file.Hash.String(), Equals, expected.Hash, Commentf("subtest %d, iter %d, hash=%v, expected=%s", i, k, file.Hash.String(), expected.Hash))
82+
}
83+
_, err = iter.Next()
84+
c.Assert(err, Equals, io.EOF)
85+
}
86+
}
87+
4488
var contentsTests = []struct {
4589
repo string // the repo name as in localRepos
4690
commit string // the commit to search for the file
@@ -154,7 +198,9 @@ func (s *SuiteFile) TestIgnoreEmptyDirEntries(c *C) {
154198
commit, err := s.repos[t.repo].Commit(core.NewHash(t.commit))
155199
c.Assert(err, IsNil, Commentf("subtest %d: %v (%s)", i, err, t.commit))
156200

157-
for file := range commit.Tree().Files() {
201+
iter := commit.Tree().Files()
202+
defer iter.Close()
203+
for file, err := iter.Next(); err == nil; file, err = iter.Next() {
158204
_ = file.Contents()
159205
// this would probably panic if we are not ignoring empty dirs
160206
}

formats/packfile/reader_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111

1212
"gopkg.in/src-d/go-git.v3/core"
13-
"gopkg.in/src-d/go-git.v3/storages/memory"
13+
"gopkg.in/src-d/go-git.v3/storage/memory"
1414

1515
"github.com/dustin/go-humanize"
1616
. "gopkg.in/check.v1"

objects_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
. "gopkg.in/check.v1"
88
"gopkg.in/src-d/go-git.v3/core"
9-
"gopkg.in/src-d/go-git.v3/storages/memory"
9+
"gopkg.in/src-d/go-git.v3/storage/memory"
1010
)
1111

1212
type ObjectsSuite struct {
@@ -59,8 +59,9 @@ func (s *ObjectsSuite) TestParseTree(c *C) {
5959
c.Assert(tree.Entries[".gitignore"].Hash.String(), Equals, "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88")
6060

6161
count := 0
62-
ch := tree.Files()
63-
for f := range ch {
62+
iter := tree.Files()
63+
defer iter.Close()
64+
for f, err := iter.Next(); err == nil; f, err = iter.Next() {
6465
count++
6566
if f.Name == "go/example.go" {
6667
content, _ := ioutil.ReadAll(f)

remote_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package git
33
import (
44
"gopkg.in/src-d/go-git.v3/clients/http"
55
"gopkg.in/src-d/go-git.v3/formats/packfile"
6-
"gopkg.in/src-d/go-git.v3/storages/memory"
6+
"gopkg.in/src-d/go-git.v3/storage/memory"
77

88
. "gopkg.in/check.v1"
99
)

repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"gopkg.in/src-d/go-git.v3/clients/common"
88
"gopkg.in/src-d/go-git.v3/core"
99
"gopkg.in/src-d/go-git.v3/formats/packfile"
10-
"gopkg.in/src-d/go-git.v3/storages/memory"
10+
"gopkg.in/src-d/go-git.v3/storage/memory"
1111
)
1212

1313
var (

tree.go

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,28 @@ import (
55
"errors"
66
"io"
77
"os"
8-
"path"
98
"strconv"
109
"strings"
1110

1211
"gopkg.in/src-d/go-git.v3/core"
1312
)
1413

14+
const (
15+
maxTreeDepth = 1024
16+
)
17+
18+
// New errors defined by this package.
19+
var (
20+
ErrMaxTreeDepth = errors.New("maximum tree depth exceeded")
21+
ErrFileNotFound = errors.New("file not found")
22+
)
23+
1524
// Tree is basically like a directory - it references a bunch of other trees
1625
// and/or blobs (i.e. files and sub-directories)
1726
type Tree struct {
18-
Entries map[string]TreeEntry
19-
Hash core.Hash
27+
Entries map[string]TreeEntry
28+
OrderedNames []string
29+
Hash core.Hash
2030

2131
r *Repository
2232
}
@@ -28,9 +38,6 @@ type TreeEntry struct {
2838
Hash core.Hash
2939
}
3040

31-
// New errors defined by this package.
32-
var ErrFileNotFound = errors.New("file not found")
33-
3441
// File returns the hash of the file identified by the `path` argument.
3542
// The path is interpreted as relative to the tree receiver.
3643
func (t *Tree) File(path string) (*File, error) {
@@ -113,40 +120,8 @@ func (t *Tree) entry(baseName string) (*TreeEntry, error) {
113120
return &entry, nil
114121
}
115122

116-
func (t *Tree) Files() chan *File {
117-
ch := make(chan *File, 1)
118-
119-
go func() {
120-
defer func() { close(ch) }()
121-
t.walkEntries("", ch)
122-
}()
123-
124-
return ch
125-
}
126-
127-
func (t *Tree) walkEntries(base string, ch chan *File) {
128-
for _, entry := range t.Entries {
129-
obj, err := t.r.Storage.Get(entry.Hash)
130-
if err != nil {
131-
if err == core.ObjectNotFoundErr {
132-
continue // ignore entries without hash (= submodule dirs)
133-
}
134-
//FIXME: Refactor this function to return an error. Ideally this would be
135-
// moved into a FileIter type.
136-
}
137-
138-
if obj.Type() == core.TreeObject {
139-
tree := &Tree{r: t.r}
140-
tree.Decode(obj)
141-
tree.walkEntries(path.Join(base, entry.Name), ch)
142-
continue
143-
}
144-
145-
blob := &Blob{}
146-
blob.Decode(obj)
147-
148-
ch <- &File{Name: path.Join(base, entry.Name), Reader: blob.Reader(), Hash: entry.Hash}
149-
}
123+
func (t *Tree) Files() *FileIter {
124+
return NewFileIter(t.r, t)
150125
}
151126

152127
// Decode transform an core.Object into a Tree struct
@@ -195,26 +170,67 @@ func (t *Tree) Decode(o core.Object) error {
195170
Mode: os.FileMode(fm),
196171
Name: baseName,
197172
}
173+
t.OrderedNames = append(t.OrderedNames, baseName)
198174
}
199175

200176
return nil
201177
}
202178

179+
// TreeEntryIter facilitates iterating through the TreeEntry objects in a Tree.
180+
type TreeEntryIter struct {
181+
t *Tree
182+
pos int
183+
}
184+
185+
func NewTreeEntryIter(t *Tree) *TreeEntryIter {
186+
return &TreeEntryIter{t, 0}
187+
}
188+
189+
func (iter *TreeEntryIter) Next() (TreeEntry, error) {
190+
if iter.pos >= len(iter.t.OrderedNames) {
191+
return TreeEntry{}, io.EOF
192+
}
193+
194+
entry, ok := iter.t.Entries[iter.t.OrderedNames[iter.pos]]
195+
if !ok {
196+
// Probable race condition or internal bug
197+
// FIXME: Report more severe error or panic
198+
return TreeEntry{}, io.EOF
199+
}
200+
201+
iter.pos++
202+
203+
return entry, nil
204+
}
205+
206+
// TreeEntryIter facilitates iterating through the descendent subtrees of a
207+
// Tree.
203208
type TreeIter struct {
204-
core.ObjectIter
205-
r *Repository
209+
w TreeWalker
206210
}
207211

208-
func NewTreeIter(r *Repository, iter core.ObjectIter) *TreeIter {
209-
return &TreeIter{iter, r}
212+
func NewTreeIter(r *Repository, t *Tree) *TreeIter {
213+
return &TreeIter{
214+
w: *NewTreeWalker(r, t),
215+
}
210216
}
211217

212218
func (iter *TreeIter) Next() (*Tree, error) {
213-
obj, err := iter.ObjectIter.Next()
214-
if err != nil {
215-
return nil, err
219+
for {
220+
_, _, obj, err := iter.w.Next()
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
if obj.Type() != core.TreeObject {
226+
// Skip non-tree objects
227+
continue
228+
}
229+
230+
return iter.w.Tree(), nil
216231
}
232+
}
217233

218-
tree := &Tree{r: iter.r}
219-
return tree, tree.Decode(obj)
234+
func (iter *TreeIter) Close() {
235+
iter.w.Close()
220236
}

tree_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ func (s *SuiteTree) TestFiles(c *C) {
192192

193193
tree := commit.Tree()
194194
var output []string
195-
for file := range tree.Files() {
195+
iter := tree.Files()
196+
defer iter.Close()
197+
for file, err := iter.Next(); err == nil; file, err = iter.Next() {
196198
output = append(output, file.Name)
197199
}
198200
sort.Strings(output)

0 commit comments

Comments
 (0)