Skip to content

Commit 3ecd76c

Browse files
authored
refac(git/tree): use iterators for Make, List, etc. (#683)
Avoids allocating slices repeatedly.
1 parent c030b62 commit 3ecd76c

File tree

5 files changed

+155
-131
lines changed

5 files changed

+155
-131
lines changed

internal/git/tree.go

Lines changed: 98 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package git
22

33
import (
4-
"bufio"
54
"bytes"
65
"context"
76
"errors"
87
"fmt"
9-
"io"
8+
"iter"
109
"path"
1110
"slices"
1211
"strconv"
@@ -65,29 +64,36 @@ type TreeEntry struct {
6564
}
6665

6766
// MakeTree creates a new Git tree from the given entries.
67+
//
6868
// The tree will contain *only* the given entries and nothing else.
6969
// Entries must not contain slashes in their names;
7070
// this operation does not create subtrees.
71-
func (r *Repository) MakeTree(ctx context.Context, ents []TreeEntry) (_ Hash, err error) {
71+
//
72+
// Returns the hash of the new tree and the number of entries written.
73+
func (r *Repository) MakeTree(ctx context.Context, ents iter.Seq2[TreeEntry, error]) (_ Hash, numEnts int, err error) {
7274
var stdout bytes.Buffer
7375
cmd := r.gitCmd(ctx, "mktree").Stdout(&stdout)
7476
stdin, err := cmd.StdinPipe()
7577
if err != nil {
76-
return ZeroHash, fmt.Errorf("pipe: %w", err)
78+
return ZeroHash, numEnts, fmt.Errorf("pipe: %w", err)
7779
}
7880

7981
if err := cmd.Start(r.exec); err != nil {
80-
return ZeroHash, fmt.Errorf("start: %w", err)
82+
return ZeroHash, numEnts, fmt.Errorf("start: %w", err)
8183
}
8284
defer func() {
8385
if err != nil {
8486
_ = cmd.Kill(r.exec)
8587
}
8688
}()
8789

88-
for _, ent := range ents {
90+
for ent, err := range ents {
91+
if err != nil {
92+
return ZeroHash, numEnts, fmt.Errorf("read entry: %w", err)
93+
}
94+
8995
if ent.Type == "" {
90-
return ZeroHash, fmt.Errorf("type not set for %q", ent.Name)
96+
return ZeroHash, numEnts, fmt.Errorf("type not set for %q", ent.Name)
9197
}
9298

9399
if ent.Mode == ZeroMode {
@@ -97,31 +103,33 @@ func (r *Repository) MakeTree(ctx context.Context, ents []TreeEntry) (_ Hash, er
97103
case TreeType:
98104
ent.Mode = DirMode
99105
default:
100-
return ZeroHash, fmt.Errorf("mode not set for %q", ent.Name)
106+
return ZeroHash, numEnts, fmt.Errorf("mode not set for %q", ent.Name)
101107
}
102108
}
103109

104110
if strings.Contains(ent.Name, "/") {
105-
return ZeroHash, fmt.Errorf("name %q contains a slash", ent.Name)
111+
return ZeroHash, numEnts, fmt.Errorf("name %q contains a slash", ent.Name)
106112
}
107113

108114
// mktree expects input in the form:
109115
// <mode> SP <type> SP <hash> TAB <name> NL
110116
_, err := fmt.Fprintf(stdin, "%s %s %s\t%s\n", ent.Mode, ent.Type, ent.Hash, ent.Name)
111117
if err != nil {
112-
return ZeroHash, fmt.Errorf("write: %w", err)
118+
return ZeroHash, numEnts, fmt.Errorf("write: %w", err)
113119
}
120+
121+
numEnts++
114122
}
115123

116124
if err := stdin.Close(); err != nil {
117-
return ZeroHash, fmt.Errorf("close: %w", err)
125+
return ZeroHash, numEnts, fmt.Errorf("close: %w", err)
118126
}
119127

120128
if err := cmd.Wait(r.exec); err != nil {
121-
return ZeroHash, fmt.Errorf("wait: %w", err)
129+
return ZeroHash, numEnts, fmt.Errorf("wait: %w", err)
122130
}
123131

124-
return Hash(bytes.TrimSpace(stdout.Bytes())), nil
132+
return Hash(bytes.TrimSpace(stdout.Bytes())), numEnts, nil
125133
}
126134

127135
// ListTreeOptions specifies options for the ListTree operation.
@@ -142,7 +150,7 @@ func (r *Repository) ListTree(
142150
ctx context.Context,
143151
tree Hash,
144152
opts ListTreeOptions,
145-
) (_ []TreeEntry, err error) {
153+
) iter.Seq2[TreeEntry, error] {
146154
args := []string{
147155
"ls-tree",
148156
"--full-tree", // don't limit listing to the current working directory
@@ -152,63 +160,44 @@ func (r *Repository) ListTree(
152160
}
153161
args = append(args, tree.String())
154162

155-
cmd := r.gitCmd(ctx, args...)
156-
stdout, err := cmd.StdoutPipe()
157-
if err != nil {
158-
return nil, fmt.Errorf("pipe: %w", err)
159-
}
163+
return func(yield func(TreeEntry, error) bool) {
164+
cmd := r.gitCmd(ctx, args...)
165+
for line, err := range cmd.ScanLines(r.exec) {
166+
if err != nil {
167+
yield(TreeEntry{}, fmt.Errorf("git ls-tree: %w", err))
168+
return
169+
}
160170

161-
if err := cmd.Start(r.exec); err != nil {
162-
return nil, fmt.Errorf("start: %w", err)
163-
}
164-
defer func() {
165-
if err != nil {
166-
_ = cmd.Kill(r.exec)
167-
_, _ = io.Copy(io.Discard, stdout)
168-
}
169-
}()
171+
// ls-tree output is in the form:
172+
// <mode> SP <type> SP <hash> TAB <name> NL
173+
modeTypeHash, name, ok := bytes.Cut(line, []byte{'\t'})
174+
if !ok {
175+
r.log.Warnf("ls-tree: skipping invalid line: %q", line)
176+
continue
177+
}
170178

171-
scanner := bufio.NewScanner(stdout)
172-
var ents []TreeEntry
173-
for scanner.Scan() {
174-
line := scanner.Bytes()
175-
// ls-tree output is in the form:
176-
// <mode> SP <type> SP <hash> TAB <name> NL
177-
modeTypeHash, name, ok := bytes.Cut(line, []byte{'\t'})
178-
if !ok {
179-
r.log.Warnf("ls-tree: skipping invalid line: %q", line)
180-
continue
181-
}
179+
toks := bytes.SplitN(modeTypeHash, []byte{' '}, 3)
180+
if len(toks) != 3 {
181+
r.log.Warnf("ls-tree: skipping invalid line: %q", line)
182+
continue
183+
}
182184

183-
toks := bytes.SplitN(modeTypeHash, []byte{' '}, 3)
184-
if len(toks) != 3 {
185-
r.log.Warnf("ls-tree: skipping invalid line: %q", line)
186-
continue
187-
}
185+
mode, err := ParseMode(string(toks[0]))
186+
if err != nil {
187+
r.log.Warnf("ls-tree: skipping invalid mode: %q: %v", toks[0], err)
188+
continue
189+
}
188190

189-
mode, err := ParseMode(string(toks[0]))
190-
if err != nil {
191-
r.log.Warnf("ls-tree: skipping invalid mode: %q: %v", toks[0], err)
192-
continue
191+
if !yield(TreeEntry{
192+
Mode: mode,
193+
Type: Type(toks[1]),
194+
Hash: Hash(toks[2]),
195+
Name: string(name),
196+
}, nil) {
197+
return
198+
}
193199
}
194-
195-
ents = append(ents, TreeEntry{
196-
Mode: mode,
197-
Type: Type(toks[1]),
198-
Hash: Hash(toks[2]),
199-
Name: string(name),
200-
})
201200
}
202-
203-
if err := scanner.Err(); err != nil {
204-
return nil, fmt.Errorf("scan: %w", err)
205-
}
206-
207-
if err := cmd.Wait(r.exec); err != nil {
208-
return nil, fmt.Errorf("git ls-tree: %w", err)
209-
}
210-
211-
return ents, nil
212201
}
213202

214203
// UpdateTreeRequest is a request to update an existing Git tree.
@@ -331,27 +320,25 @@ func (r *Repository) UpdateTree(ctx context.Context, req UpdateTreeRequest) (_ H
331320
oldHash = ZeroHash
332321
}
333322

334-
var entries []TreeEntry
323+
var entries iter.Seq2[TreeEntry, error]
335324
if oldHash != ZeroHash {
336-
entries, err = r.ListTree(ctx, oldHash, ListTreeOptions{})
337-
if err != nil {
338-
return ZeroHash, fmt.Errorf("list %v (%v): %w", dir, oldHash, err)
339-
}
325+
entries = r.ListTree(ctx, oldHash, ListTreeOptions{})
326+
} else {
327+
entries = func(func(TreeEntry, error) bool) {}
340328
}
341-
342329
entries = update.Apply(entries)
343-
if len(entries) == 0 {
344-
// If the directory is empty, we don't need to create a tree.
345-
// We can just delete the directory.
346-
parent, base := pathSplit(dir)
347-
ensureUpdate(parent).Delete(base)
348-
continue
349-
}
350330

351-
newHash, err := r.MakeTree(ctx, update.Apply(entries))
331+
newHash, numEnts, err := r.MakeTree(ctx, entries)
352332
if err != nil {
353333
return ZeroHash, fmt.Errorf("make tree: %w", err)
354334
}
335+
if numEnts == 0 {
336+
// If the directory is empty, delete the directory
337+
// from the parent.
338+
parent, base := pathSplit(dir)
339+
ensureUpdate(parent).Delete(base)
340+
continue
341+
}
355342

356343
// Update the parent directory with the new hash.
357344
parent, base := pathSplit(dir)
@@ -364,15 +351,14 @@ func (r *Repository) UpdateTree(ctx context.Context, req UpdateTreeRequest) (_ H
364351
}
365352

366353
// Process root directory separately.
367-
var entries []TreeEntry
354+
var entries iter.Seq2[TreeEntry, error]
368355
if req.Tree != ZeroHash && req.Tree != "" {
369-
entries, err = r.ListTree(ctx, req.Tree, ListTreeOptions{})
370-
if err != nil {
371-
return ZeroHash, fmt.Errorf("list root (%v): %w", req.Tree, err)
372-
}
356+
entries = r.ListTree(ctx, req.Tree, ListTreeOptions{})
357+
} else {
358+
entries = func(func(TreeEntry, error) bool) {}
373359
}
374360

375-
rootHash, err := r.MakeTree(ctx, updates["."].Apply(entries))
361+
rootHash, _, err := r.MakeTree(ctx, updates["."].Apply(entries))
376362
if err != nil {
377363
return ZeroHash, fmt.Errorf("make root tree: %w", err)
378364
}
@@ -389,30 +375,41 @@ type directoryUpdate struct {
389375
Deletes []string // sorted
390376
}
391377

392-
func (du *directoryUpdate) Apply(entries []TreeEntry) []TreeEntry {
378+
func (du *directoryUpdate) Apply(entries iter.Seq2[TreeEntry, error]) iter.Seq2[TreeEntry, error] {
393379
if du == nil {
394380
return entries
395381
}
396382

397-
newEntries := entries[:0]
398-
for _, ent := range entries {
399-
if idx, ok := slices.BinarySearch(du.Deletes, ent.Name); ok {
400-
du.Deletes = slices.Delete(du.Deletes, idx, idx+1)
401-
continue
402-
}
383+
return func(yield func(TreeEntry, error) bool) {
384+
for ent, err := range entries {
385+
if err != nil {
386+
yield(TreeEntry{}, err)
387+
return
388+
}
403389

404-
if idx, ok := slices.BinarySearchFunc(du.Writes, ent.Name, entryByName); ok {
405-
ent = du.Writes[idx]
406-
du.Writes = slices.Delete(du.Writes, idx, idx+1)
390+
if idx, ok := slices.BinarySearch(du.Deletes, ent.Name); ok {
391+
du.Deletes = slices.Delete(du.Deletes, idx, idx+1)
392+
continue
393+
}
394+
395+
if idx, ok := slices.BinarySearchFunc(du.Writes, ent.Name, entryByName); ok {
396+
ent = du.Writes[idx]
397+
du.Writes = slices.Delete(du.Writes, idx, idx+1)
398+
}
399+
400+
if !yield(ent, nil) {
401+
return
402+
}
407403
}
408404

409-
newEntries = append(newEntries, ent)
405+
// If there are any more writes remaining,
406+
// they are new entries.
407+
for _, ent := range du.Writes {
408+
if !yield(ent, nil) {
409+
return
410+
}
411+
}
410412
}
411-
412-
// If there are any more writes remaining,
413-
// they are new entries.
414-
newEntries = append(newEntries, du.Writes...)
415-
return newEntries
416413
}
417414

418415
func (du *directoryUpdate) Empty() bool {

0 commit comments

Comments
 (0)