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

Commit f57aa86

Browse files
committed
Merge pull request #30 from scjalliance/consistent-iterators
Improved consistency of Tree iterators
2 parents 6b0a598 + a72c4ec commit f57aa86

File tree

11 files changed

+413
-69
lines changed

11 files changed

+413
-69
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: 10 additions & 6 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 {
@@ -54,13 +54,17 @@ func (s *ObjectsSuite) TestParseTree(c *C) {
5454
c.Assert(err, IsNil)
5555

5656
c.Assert(tree.Entries, HasLen, 8)
57-
c.Assert(tree.Entries[".gitignore"].Name, Equals, ".gitignore")
58-
c.Assert(tree.Entries[".gitignore"].Mode.String(), Equals, "-rw-r--r--")
59-
c.Assert(tree.Entries[".gitignore"].Hash.String(), Equals, "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88")
57+
58+
tree.buildMap()
59+
c.Assert(tree.m, HasLen, 8)
60+
c.Assert(tree.m[".gitignore"].Name, Equals, ".gitignore")
61+
c.Assert(tree.m[".gitignore"].Mode.String(), Equals, "-rw-r--r--")
62+
c.Assert(tree.m[".gitignore"].Hash.String(), Equals, "32858aad3c383ed1ff0a0f9bdf231d54a00c9e88")
6063

6164
count := 0
62-
ch := tree.Files()
63-
for f := range ch {
65+
iter := tree.Files()
66+
defer iter.Close()
67+
for f, err := iter.Next(); err == nil; f, err = iter.Next() {
6468
count++
6569
if f.Name == "go/example.go" {
6670
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: 70 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,30 @@ 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
27+
Entries []TreeEntry
1928
Hash core.Hash
2029

2130
r *Repository
31+
m map[string]*TreeEntry
2232
}
2333

2434
// TreeEntry represents a file
@@ -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) {
@@ -105,48 +112,19 @@ func (t *Tree) dir(baseName string) (*Tree, error) {
105112
var errEntryNotFound = errors.New("entry not found")
106113

107114
func (t *Tree) entry(baseName string) (*TreeEntry, error) {
108-
entry, ok := t.Entries[baseName]
115+
if t.m == nil {
116+
t.buildMap()
117+
}
118+
entry, ok := t.m[baseName]
109119
if !ok {
110120
return nil, errEntryNotFound
111121
}
112122

113-
return &entry, nil
123+
return entry, nil
114124
}
115125

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-
}
126+
func (t *Tree) Files() *FileIter {
127+
return NewFileIter(t.r, t)
150128
}
151129

152130
// Decode transform an core.Object into a Tree struct
@@ -160,7 +138,8 @@ func (t *Tree) Decode(o core.Object) error {
160138
return nil
161139
}
162140

163-
t.Entries = make(map[string]TreeEntry)
141+
t.Entries = nil
142+
t.m = nil
164143

165144
r := bufio.NewReader(o.Reader())
166145
for {
@@ -190,31 +169,69 @@ func (t *Tree) Decode(o core.Object) error {
190169
}
191170

192171
baseName := name[:len(name)-1]
193-
t.Entries[baseName] = TreeEntry{
172+
t.Entries = append(t.Entries, TreeEntry{
194173
Hash: hash,
195174
Mode: os.FileMode(fm),
196175
Name: baseName,
197-
}
176+
})
198177
}
199178

200179
return nil
201180
}
202181

182+
func (t *Tree) buildMap() {
183+
t.m = make(map[string]*TreeEntry)
184+
for i := 0; i < len(t.Entries); i++ {
185+
t.m[t.Entries[i].Name] = &t.Entries[i]
186+
}
187+
}
188+
189+
// TreeEntryIter facilitates iterating through the TreeEntry objects in a Tree.
190+
type TreeEntryIter struct {
191+
t *Tree
192+
pos int
193+
}
194+
195+
func NewTreeEntryIter(t *Tree) *TreeEntryIter {
196+
return &TreeEntryIter{t, 0}
197+
}
198+
199+
func (iter *TreeEntryIter) Next() (TreeEntry, error) {
200+
if iter.pos >= len(iter.t.Entries) {
201+
return TreeEntry{}, io.EOF
202+
}
203+
iter.pos++
204+
return iter.t.Entries[iter.pos-1], nil
205+
}
206+
207+
// TreeEntryIter facilitates iterating through the descendent subtrees of a
208+
// Tree.
203209
type TreeIter struct {
204-
core.ObjectIter
205-
r *Repository
210+
w TreeWalker
206211
}
207212

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

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

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

0 commit comments

Comments
 (0)