@@ -64,7 +64,9 @@ func (f *Format) Run() (err error) {
64
64
pipelines = make (map [string ]* format.Pipeline )
65
65
formatters = make (map [string ]* format.Formatter )
66
66
67
+ // iterate the formatters in lexicographical order
67
68
for _ , name := range cfg .Names {
69
+ // init formatter
68
70
formatterCfg := cfg .Formatters [name ]
69
71
formatter , err := format .NewFormatter (name , Cli .TreeRoot , formatterCfg , globalExcludes )
70
72
if errors .Is (err , format .ErrCommandNotFound ) && Cli .AllowMissingFormatter {
@@ -74,8 +76,12 @@ func (f *Format) Run() (err error) {
74
76
return fmt .Errorf ("%w: failed to initialise formatter: %v" , err , name )
75
77
}
76
78
79
+ // store formatter by name
77
80
formatters [name ] = formatter
78
81
82
+ // If no pipeline is configured, we add the formatter to a nominal pipeline of size 1 with the key being the
83
+ // formatter's name. If a pipeline is configured, we add the formatter to a pipeline keyed by
84
+ // 'p:<pipeline_name>' in which it is sorted by priority.
79
85
if formatterCfg .Pipeline == "" {
80
86
pipeline := format.Pipeline {}
81
87
pipeline .Add (formatter )
@@ -110,17 +116,17 @@ func (f *Format) Run() (err error) {
110
116
// initialise stats collection
111
117
stats .Init ()
112
118
113
- // create some groups for concurrent processing and control flow
119
+ // create an overall error group for executing high level tasks concurrently
114
120
eg , ctx := errgroup .WithContext (ctx )
115
121
116
- // create a channel for paths to be processed
117
- // we use a multiple of batch size here to allow for greater concurrency
122
+ // create a channel for files needing to be processed
123
+ // we use a multiple of batch size here as a rudimentary concurrency optimization based on the host machine
118
124
filesCh = make (chan * walk.File , BatchSize * runtime .NumCPU ())
119
125
120
- // create a channel for tracking paths that have been processed
126
+ // create a channel for files that have been processed
121
127
processedCh = make (chan * walk.File , cap (filesCh ))
122
128
123
- // start concurrent processing tasks
129
+ // start concurrent processing tasks in reverse order
124
130
eg .Go (updateCache (ctx ))
125
131
eg .Go (applyFormatters (ctx ))
126
132
eg .Go (walkFilesystem (ctx ))
@@ -129,15 +135,70 @@ func (f *Format) Run() (err error) {
129
135
return eg .Wait ()
130
136
}
131
137
138
+ func updateCache (ctx context.Context ) func () error {
139
+ return func () error {
140
+ // used to batch updates for more efficient txs
141
+ batch := make ([]* walk.File , 0 , BatchSize )
142
+
143
+ // apply a batch
144
+ processBatch := func () error {
145
+ if err := cache .Update (batch ); err != nil {
146
+ return err
147
+ }
148
+ batch = batch [:0 ]
149
+ return nil
150
+ }
151
+
152
+ LOOP:
153
+ for {
154
+ select {
155
+ // detect ctx cancellation
156
+ case <- ctx .Done ():
157
+ return ctx .Err ()
158
+ // respond to processed files
159
+ case file , ok := <- processedCh :
160
+ if ! ok {
161
+ // channel has been closed, no further files to process
162
+ break LOOP
163
+ }
164
+ // append to batch and process if we have enough
165
+ batch = append (batch , file )
166
+ if len (batch ) == BatchSize {
167
+ if err := processBatch (); err != nil {
168
+ return err
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // final flush
175
+ if err := processBatch (); err != nil {
176
+ return err
177
+ }
178
+
179
+ // if fail on change has been enabled, check that no files were actually formatted, throwing an error if so
180
+ if Cli .FailOnChange && stats .Value (stats .Formatted ) != 0 {
181
+ return ErrFailOnChange
182
+ }
183
+
184
+ // print stats to stdout
185
+ stats .Print ()
186
+
187
+ return nil
188
+ }
189
+ }
190
+
132
191
func walkFilesystem (ctx context.Context ) func () error {
133
192
return func () error {
134
193
paths := Cli .Paths
135
194
195
+ // we read paths from stdin if the cli flag has been set and no paths were provided as cli args
136
196
if len (paths ) == 0 && Cli .Stdin {
137
197
198
+ // determine the current working directory
138
199
cwd , err := os .Getwd ()
139
200
if err != nil {
140
- return fmt .Errorf ("%w: failed to determine current working directory" , err )
201
+ return fmt .Errorf ("failed to determine current working directory: %w " , err )
141
202
}
142
203
143
204
// read in all the paths
@@ -149,17 +210,21 @@ func walkFilesystem(ctx context.Context) func() error {
149
210
path = filepath .Join (cwd , path )
150
211
}
151
212
213
+ // append the fully qualified path to our paths list
152
214
paths = append (paths , path )
153
215
}
154
216
}
155
217
218
+ // create a filesystem walker
156
219
walker , err := walk .New (Cli .Walk , Cli .TreeRoot , paths )
157
220
if err != nil {
158
221
return fmt .Errorf ("failed to create walker: %w" , err )
159
222
}
160
223
224
+ // close the files channel when we're done walking the file system
161
225
defer close (filesCh )
162
226
227
+ // if no cache has been configured, we invoke the walker directly
163
228
if Cli .NoCache {
164
229
return walker .Walk (ctx , func (file * walk.File , err error ) error {
165
230
select {
@@ -177,76 +242,42 @@ func walkFilesystem(ctx context.Context) func() error {
177
242
})
178
243
}
179
244
245
+ // otherwise we pass the walker to the cache and have it generate files for processing based on whether or not
246
+ // they have been added/changed since the last invocation
180
247
if err = cache .ChangeSet (ctx , walker , filesCh ); err != nil {
181
248
return fmt .Errorf ("failed to generate change set: %w" , err )
182
249
}
183
250
return nil
184
251
}
185
252
}
186
253
187
- func updateCache (ctx context.Context ) func () error {
188
- return func () error {
189
- batch := make ([]* walk.File , 0 , BatchSize )
190
-
191
- processBatch := func () error {
192
- if err := cache .Update (batch ); err != nil {
193
- return err
194
- }
195
- batch = batch [:0 ]
196
- return nil
197
- }
198
-
199
- LOOP:
200
- for {
201
- select {
202
- case <- ctx .Done ():
203
- return ctx .Err ()
204
- case path , ok := <- processedCh :
205
- if ! ok {
206
- break LOOP
207
- }
208
- batch = append (batch , path )
209
- if len (batch ) == BatchSize {
210
- if err := processBatch (); err != nil {
211
- return err
212
- }
213
- }
214
- }
215
- }
216
-
217
- // final flush
218
- if err := processBatch (); err != nil {
219
- return err
220
- }
221
-
222
- if Cli .FailOnChange && stats .Value (stats .Formatted ) != 0 {
223
- return ErrFailOnChange
224
- }
225
-
226
- stats .Print ()
227
- return nil
228
- }
229
- }
230
-
231
254
func applyFormatters (ctx context.Context ) func () error {
255
+ // create our own errgroup for concurrent formatting tasks
232
256
fg , ctx := errgroup .WithContext (ctx )
257
+
258
+ // pre-initialise batches keyed by pipeline
233
259
batches := make (map [string ][]* walk.File )
260
+ for key := range pipelines {
261
+ batches [key ] = make ([]* walk.File , 0 , BatchSize )
262
+ }
234
263
264
+ // for a given pipeline key, add the provided file to the current batch and trigger a format if the batch size has
265
+ // been reached
235
266
tryApply := func (key string , file * walk.File ) {
236
- batch , ok := batches [key ]
237
- if ! ok {
238
- batch = make ([]* walk.File , 0 , BatchSize )
239
- }
240
- batch = append (batch , file )
241
- batches [key ] = batch
267
+ // append to batch
268
+ batches [key ] = append (batches [key ], file )
242
269
270
+ // check if the batch is full
271
+ batch := batches [key ]
243
272
if len (batch ) == BatchSize {
273
+ // get the pipeline
244
274
pipeline := pipelines [key ]
245
275
246
276
// copy the batch
247
277
files := make ([]* walk.File , len (batch ))
248
278
copy (files , batch )
249
279
280
+ // apply to the pipeline
250
281
fg .Go (func () error {
251
282
if err := pipeline .Apply (ctx , files ); err != nil {
252
283
return err
@@ -257,10 +288,12 @@ func applyFormatters(ctx context.Context) func() error {
257
288
return nil
258
289
})
259
290
291
+ // reset the batch
260
292
batches [key ] = batch [:0 ]
261
293
}
262
294
}
263
295
296
+ // format any partial batches
264
297
flushBatches := func () {
265
298
for key , pipeline := range pipelines {
266
299
@@ -287,6 +320,7 @@ func applyFormatters(ctx context.Context) func() error {
287
320
close (processedCh )
288
321
}()
289
322
323
+ // iterate the files channel, checking if any pipeline wants it, and attempting to apply if so.
290
324
for file := range filesCh {
291
325
var matched bool
292
326
for key , pipeline := range pipelines {
@@ -299,6 +333,7 @@ func applyFormatters(ctx context.Context) func() error {
299
333
if matched {
300
334
stats .Add (stats .Matched , 1 )
301
335
} else {
336
+ // no match, so we send it direct to the processed channel
302
337
processedCh <- file
303
338
}
304
339
}
0 commit comments