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"
@@ -164,6 +166,8 @@ func NewViper() (*viper.Viper, error) {
164
166
165
167
// FromViper takes a viper instance and produces a Config instance.
166
168
func FromViper (v * viper.Viper ) (* Config , error ) {
169
+ logger := log .WithPrefix ("config" )
170
+
167
171
configReset := map [string ]any {
168
172
"ci" : false ,
169
173
"clear-cache" : false ,
@@ -198,7 +202,7 @@ func FromViper(v *viper.Viper) (*Config, error) {
198
202
}
199
203
200
204
// determine tree root
201
- if err = determineTreeRoot (v , cfg ); err != nil {
205
+ if err = determineTreeRoot (v , cfg , logger ); err != nil {
202
206
return nil , fmt .Errorf ("failed to determine tree root: %w" , err )
203
207
}
204
208
@@ -258,7 +262,7 @@ func FromViper(v *viper.Viper) (*Config, error) {
258
262
return cfg , nil
259
263
}
260
264
261
- func determineTreeRoot (v * viper.Viper , cfg * Config ) error {
265
+ func determineTreeRoot (v * viper.Viper , cfg * Config , logger * log. Logger ) error {
262
266
var err error
263
267
264
268
// enforce the various tree root options are mutually exclusive
@@ -286,34 +290,37 @@ func determineTreeRoot(v *viper.Viper, cfg *Config) error {
286
290
if cfg .Walk == walk .Git .String () && count == 0 {
287
291
cfg .TreeRootCmd = "git rev-parse --show-toplevel"
288
292
289
- log .Infof (
293
+ logger .Infof (
290
294
"git walker enabled and tree root has not been specified: defaulting tree-root-cmd to '%s'" ,
291
295
cfg .TreeRootCmd ,
292
296
)
293
297
}
294
298
295
299
switch {
296
300
case cfg .TreeRoot != "" :
297
- log .Debugf ("tree root specified explicitly: %s" , cfg .TreeRoot )
301
+ logger .Debugf ("tree root specified explicitly: %s" , cfg .TreeRoot )
298
302
299
303
case cfg .TreeRootFile != "" :
300
- log .Debugf ("searching for tree root using -- tree-root-file: %s" , cfg .TreeRootFile )
304
+ logger .Debugf ("searching for tree root using tree-root-file: %s" , cfg .TreeRootFile )
301
305
302
306
_ , cfg .TreeRoot , err = FindUp (cfg .WorkingDirectory , cfg .TreeRootFile )
303
307
if err != nil {
304
308
return fmt .Errorf ("failed to find tree-root based on tree-root-file: %w" , err )
305
309
}
306
310
307
311
case cfg .TreeRootCmd != "" :
308
- log .Debugf ("searching for tree root using -- tree-root-cmd: %s" , cfg .TreeRootCmd )
312
+ logger .Debugf ("searching for tree root using tree-root-cmd: %s" , cfg .TreeRootCmd )
309
313
310
314
if cfg .TreeRoot , err = execTreeRootCmd (cfg ); err != nil {
311
315
return err
312
316
}
313
317
314
318
default :
315
319
// no tree root was specified
316
- log .Debugf ("no tree root specified, defaulting to the directory containing the config file: %s" , v .ConfigFileUsed ())
320
+ logger .Debugf (
321
+ "no tree root specified, defaulting to the directory containing the config file: %s" ,
322
+ v .ConfigFileUsed (),
323
+ )
317
324
318
325
cfg .TreeRoot = filepath .Dir (v .ConfigFileUsed ())
319
326
}
@@ -323,7 +330,7 @@ func determineTreeRoot(v *viper.Viper, cfg *Config) error {
323
330
return fmt .Errorf ("failed to get absolute path for tree root: %w" , err )
324
331
}
325
332
326
- log .Debugf ("tree root: %s" , cfg .TreeRoot )
333
+ logger .Debugf ("tree root: %s" , cfg .TreeRoot )
327
334
328
335
return nil
329
336
}
@@ -337,24 +344,83 @@ func execTreeRootCmd(cfg *Config) (string, error) {
337
344
338
345
// set a reasonable timeout of 2 seconds to wait for the command to return
339
346
// 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 )
347
+ executionTimeout := 2 * time .Second
348
+
349
+ ctx , cancel := context .WithTimeout (context .Background (), executionTimeout )
341
350
defer cancel ()
342
351
343
352
// construct the command, setting the correct working directory
344
353
//nolint:gosec
345
354
cmd := exec .CommandContext (ctx , parts [0 ], parts [1 :]... )
346
355
cmd .Dir = cfg .WorkingDirectory
347
356
348
- // execute
349
- out , cmdErr := cmd .CombinedOutput ()
350
- if cmdErr != nil {
351
- log .Errorf ("tree-root-cmd output: \n %s" , out )
357
+ // setup some pipes to capture stdout and stderr
358
+ stdout , err := cmd .StdoutPipe ()
359
+ if err != nil {
360
+ return "" , fmt .Errorf ("failed to create stdout pipe for tree-root-cmd: %w" , err )
361
+ }
362
+
363
+ stderr , err := cmd .StderrPipe ()
364
+ if err != nil {
365
+ return "" , fmt .Errorf ("failed to create stderr pipe for tree-root-cmd: %w" , err )
366
+ }
367
+
368
+ // start processing stderr before we begin executing the command
369
+ go func () {
370
+ // capture stderr line by line and log
371
+ l := log .WithPrefix ("tree-root-cmd | stderr" )
372
+
373
+ scanner := bufio .NewScanner (stderr )
374
+ for scanner .Scan () {
375
+ l .Debugf ("%s" , scanner .Text ())
376
+ }
377
+ }()
378
+
379
+ // start executing without waiting
380
+ if cmdErr := cmd .Start (); cmdErr != nil {
381
+ return "" , fmt .Errorf ("failed to start tree-root-cmd: %w" , cmdErr )
382
+ }
383
+
384
+ // read stdout until it is closed (command exits)
385
+ output , err := io .ReadAll (stdout )
386
+ if err != nil {
387
+ return "" , fmt .Errorf ("failed to read stdout from tree-root-cmd: %w" , err )
388
+ }
389
+
390
+ log .WithPrefix ("tree-root-cmd | stdout" ).Debugf ("%s" , output )
391
+
392
+ // check execution error
393
+ if cmdErr := cmd .Wait (); cmdErr != nil {
394
+ var exitErr * exec.ExitError
395
+
396
+ // by experimenting, I noticed that sometimes we received the deadline exceeded error first, other times
397
+ // the exit error indicating the process was killed, therefore, we look for both
398
+ tookTooLong := errors .Is (cmdErr , context .DeadlineExceeded )
399
+ tookTooLong = tookTooLong || (errors .As (cmdErr , & exitErr ) && exitErr .ProcessState .String () == "signal: killed" )
400
+
401
+ if tookTooLong {
402
+ return "" , fmt .Errorf (
403
+ "tree-root-cmd was killed after taking more than %v to execute" ,
404
+ executionTimeout ,
405
+ )
406
+ }
407
+
408
+ // otherwise, some other kind of error occurred
409
+ return "" , fmt .Errorf ("failed to execute tree-root-cmd: %w" , cmdErr )
410
+ }
411
+
412
+ // trim the output and check it's not empty
413
+ treeRoot := strings .TrimSpace (string (output ))
414
+
415
+ if treeRoot == "" {
416
+ return "" , fmt .Errorf ("empty output received after executing tree-root-cmd: %s" , cfg .TreeRootCmd )
417
+ }
352
418
353
- return "" , fmt .Errorf ("failed to run tree-root-cmd: %w" , cmdErr )
419
+ if strings .Contains (treeRoot , "\n " ) {
420
+ return "" , fmt .Errorf ("tree-root-cmd cannot output multiple lines: %s" , cfg .TreeRootCmd )
354
421
}
355
422
356
- // trim the output and return
357
- return strings .TrimSpace (string (out )), nil
423
+ return treeRoot , nil
358
424
}
359
425
360
426
func Find (searchDir string , fileNames ... string ) (path string , err error ) {
0 commit comments