1
1
package config
2
2
3
3
import (
4
+ "bufio"
4
5
"context"
5
6
"errors"
6
7
"fmt"
8
+ "io"
7
9
"os"
8
10
"os/exec"
9
11
"path/filepath"
@@ -108,12 +110,15 @@ func SetFlags(fs *pflag.FlagSet) {
108
110
)
109
111
fs .String (
110
112
"tree-root" , "" ,
111
- "The root directory from which treefmt will start walking the filesystem (defaults to the directory " +
112
- "containing the config file). (env $TREEFMT_TREE_ROOT)" ,
113
+ "The root directory from which treefmt will start walking the filesystem. " +
114
+ "Defaults to the root of the current git worktree. If not in a git repo, defaults to the directory " +
115
+ "containing the config file. (env $TREEFMT_TREE_ROOT)" ,
113
116
)
114
117
fs .String (
115
118
"tree-root-cmd" , "" ,
116
- "Command to run to find the tree root. (env $TREEFMT_TREE_ROOT_CMD)" ,
119
+ "Command to run to find the tree root. It is parsed using shlex, to allow quoting arguments that " +
120
+ "contain whitespace. If you wish to pass arguments containing quotes, you should use nested quotes " +
121
+ "e.g. \" '\" or '\" '. (env $TREEFMT_TREE_ROOT_CMD)" ,
117
122
)
118
123
fs .String (
119
124
"tree-root-file" , "" ,
@@ -164,6 +169,8 @@ func NewViper() (*viper.Viper, error) {
164
169
165
170
// FromViper takes a viper instance and produces a Config instance.
166
171
func FromViper (v * viper.Viper ) (* Config , error ) {
172
+ logger := log .WithPrefix ("config" )
173
+
167
174
configReset := map [string ]any {
168
175
"ci" : false ,
169
176
"clear-cache" : false ,
@@ -198,7 +205,7 @@ func FromViper(v *viper.Viper) (*Config, error) {
198
205
}
199
206
200
207
// determine tree root
201
- if err = determineTreeRoot (v , cfg ); err != nil {
208
+ if err = determineTreeRoot (v , cfg , logger ); err != nil {
202
209
return nil , fmt .Errorf ("failed to determine tree root: %w" , err )
203
210
}
204
211
@@ -258,7 +265,7 @@ func FromViper(v *viper.Viper) (*Config, error) {
258
265
return cfg , nil
259
266
}
260
267
261
- func determineTreeRoot (v * viper.Viper , cfg * Config ) error {
268
+ func determineTreeRoot (v * viper.Viper , cfg * Config , logger * log. Logger ) error {
262
269
var err error
263
270
264
271
// enforce the various tree root options are mutually exclusive
@@ -279,41 +286,44 @@ func determineTreeRoot(v *viper.Viper, cfg *Config) error {
279
286
}
280
287
281
288
if count > 1 {
282
- return errors .New ("only one of tree-root, tree-root-cmd or tree-root-file can be specified" )
289
+ return errors .New ("at most one of tree-root, tree-root-cmd or tree-root-file can be specified" )
283
290
}
284
291
285
292
// set git-based tree root command if the walker is git and no tree root has been specified
286
293
if cfg .Walk == walk .Git .String () && count == 0 {
287
294
cfg .TreeRootCmd = "git rev-parse --show-toplevel"
288
295
289
- log .Infof (
296
+ logger .Infof (
290
297
"git walker enabled and tree root has not been specified: defaulting tree-root-cmd to '%s'" ,
291
298
cfg .TreeRootCmd ,
292
299
)
293
300
}
294
301
295
302
switch {
296
303
case cfg .TreeRoot != "" :
297
- log .Debugf ("tree root specified explicitly: %s" , cfg .TreeRoot )
304
+ logger .Debugf ("tree root specified explicitly: %s" , cfg .TreeRoot )
298
305
299
306
case cfg .TreeRootFile != "" :
300
- log .Debugf ("searching for tree root using -- tree-root-file: %s" , cfg .TreeRootFile )
307
+ logger .Debugf ("searching for tree root using tree-root-file: %s" , cfg .TreeRootFile )
301
308
302
309
_ , cfg .TreeRoot , err = FindUp (cfg .WorkingDirectory , cfg .TreeRootFile )
303
310
if err != nil {
304
311
return fmt .Errorf ("failed to find tree-root based on tree-root-file: %w" , err )
305
312
}
306
313
307
314
case cfg .TreeRootCmd != "" :
308
- log .Debugf ("searching for tree root using -- tree-root-cmd: %s" , cfg .TreeRootCmd )
315
+ logger .Debugf ("searching for tree root using tree-root-cmd: %s" , cfg .TreeRootCmd )
309
316
310
317
if cfg .TreeRoot , err = execTreeRootCmd (cfg ); err != nil {
311
318
return err
312
319
}
313
320
314
321
default :
315
322
// no tree root was specified
316
- log .Debugf ("no tree root specified, defaulting to the directory containing the config file: %s" , v .ConfigFileUsed ())
323
+ logger .Debugf (
324
+ "no tree root specified, defaulting to the directory containing the config file: %s" ,
325
+ v .ConfigFileUsed (),
326
+ )
317
327
318
328
cfg .TreeRoot = filepath .Dir (v .ConfigFileUsed ())
319
329
}
@@ -323,7 +333,7 @@ func determineTreeRoot(v *viper.Viper, cfg *Config) error {
323
333
return fmt .Errorf ("failed to get absolute path for tree root: %w" , err )
324
334
}
325
335
326
- log .Debugf ("tree root: %s" , cfg .TreeRoot )
336
+ logger .Debugf ("tree root: %s" , cfg .TreeRoot )
327
337
328
338
return nil
329
339
}
@@ -337,24 +347,83 @@ func execTreeRootCmd(cfg *Config) (string, error) {
337
347
338
348
// set a reasonable timeout of 2 seconds to wait for the command to return
339
349
// it shouldn't take anywhere near this amount of time unless there's a problem
340
- ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Second )
350
+ executionTimeout := 2 * time .Second
351
+
352
+ ctx , cancel := context .WithTimeout (context .Background (), executionTimeout )
341
353
defer cancel ()
342
354
343
355
// construct the command, setting the correct working directory
344
356
//nolint:gosec
345
357
cmd := exec .CommandContext (ctx , parts [0 ], parts [1 :]... )
346
358
cmd .Dir = cfg .WorkingDirectory
347
359
348
- // execute
349
- out , cmdErr := cmd .CombinedOutput ()
350
- if cmdErr != nil {
351
- log .Errorf ("tree-root-cmd output: \n %s" , out )
360
+ // setup some pipes to capture stdout and stderr
361
+ stdout , err := cmd .StdoutPipe ()
362
+ if err != nil {
363
+ return "" , fmt .Errorf ("failed to create stdout pipe for tree-root-cmd: %w" , err )
364
+ }
365
+
366
+ stderr , err := cmd .StderrPipe ()
367
+ if err != nil {
368
+ return "" , fmt .Errorf ("failed to create stderr pipe for tree-root-cmd: %w" , err )
369
+ }
370
+
371
+ // start processing stderr before we begin executing the command
372
+ go func () {
373
+ // capture stderr line by line and log
374
+ l := log .WithPrefix ("tree-root-cmd | stderr" )
375
+
376
+ scanner := bufio .NewScanner (stderr )
377
+ for scanner .Scan () {
378
+ l .Debugf ("%s" , scanner .Text ())
379
+ }
380
+ }()
381
+
382
+ // start executing without waiting
383
+ if cmdErr := cmd .Start (); cmdErr != nil {
384
+ return "" , fmt .Errorf ("failed to start tree-root-cmd: %w" , cmdErr )
385
+ }
386
+
387
+ // read stdout until it is closed (command exits)
388
+ output , err := io .ReadAll (stdout )
389
+ if err != nil {
390
+ return "" , fmt .Errorf ("failed to read stdout from tree-root-cmd: %w" , err )
391
+ }
392
+
393
+ log .WithPrefix ("tree-root-cmd | stdout" ).Debugf ("%s" , output )
394
+
395
+ // check execution error
396
+ if cmdErr := cmd .Wait (); cmdErr != nil {
397
+ var exitErr * exec.ExitError
398
+
399
+ // by experimenting, I noticed that sometimes we received the deadline exceeded error first, other times
400
+ // the exit error indicating the process was killed, therefore, we look for both
401
+ tookTooLong := errors .Is (cmdErr , context .DeadlineExceeded )
402
+ tookTooLong = tookTooLong || (errors .As (cmdErr , & exitErr ) && exitErr .ProcessState .String () == "signal: killed" )
403
+
404
+ if tookTooLong {
405
+ return "" , fmt .Errorf (
406
+ "tree-root-cmd was killed after taking more than %v to execute" ,
407
+ executionTimeout ,
408
+ )
409
+ }
410
+
411
+ // otherwise, some other kind of error occurred
412
+ return "" , fmt .Errorf ("failed to execute tree-root-cmd: %w" , cmdErr )
413
+ }
414
+
415
+ // trim the output and check it's not empty
416
+ treeRoot := strings .TrimSpace (string (output ))
417
+
418
+ if treeRoot == "" {
419
+ return "" , fmt .Errorf ("empty output received after executing tree-root-cmd: %s" , cfg .TreeRootCmd )
420
+ }
352
421
353
- return "" , fmt .Errorf ("failed to run tree-root-cmd: %w" , cmdErr )
422
+ if strings .Contains (treeRoot , "\n " ) {
423
+ return "" , fmt .Errorf ("tree-root-cmd cannot output multiple lines: %s" , cfg .TreeRootCmd )
354
424
}
355
425
356
- // trim the output and return
357
- return strings .TrimSpace (string (out )), nil
426
+ return treeRoot , nil
358
427
}
359
428
360
429
func Find (searchDir string , fileNames ... string ) (path string , err error ) {
0 commit comments