11package git
22
33import (
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
418415func (du * directoryUpdate ) Empty () bool {
0 commit comments