gitlab.com/greut/eclint@v0.5.2-0.20240402114752-14681fe6e0bf/files.go (about) 1 package eclint 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io/fs" 9 "os" 10 "os/exec" 11 12 "github.com/go-logr/logr" 13 ) 14 15 // ListFilesContext lists the files in an asynchronous fashion 16 // 17 // When its empty, it relies on `git ls-files` first, which 18 // would fail if `git` is not present or the current working 19 // directory is not managed by it. In that case, it work the 20 // current working directory. 21 // 22 // When args are given, it recursively walks into them. 23 func ListFilesContext(ctx context.Context, args ...string) (<-chan string, <-chan error) { 24 if len(args) > 0 { 25 return WalkContext(ctx, args...) 26 } 27 28 dir := "." 29 30 log := logr.FromContextOrDiscard(ctx) 31 32 log.V(3).Info("fallback to `git ls-files`", "dir", dir) 33 34 return GitLsFilesContext(ctx, dir) 35 } 36 37 // WalkContext iterates on each path item recursively (asynchronously). 38 // 39 // Future work: use godirwalk. 40 func WalkContext(ctx context.Context, paths ...string) (<-chan string, <-chan error) { 41 filesChan := make(chan string, 128) 42 errChan := make(chan error, 1) 43 44 go func() { 45 defer close(filesChan) 46 defer close(errChan) 47 48 for _, path := range paths { 49 // shortcircuit files 50 if fi, err := os.Stat(path); err == nil && !fi.IsDir() { 51 filesChan <- path 52 53 break 54 } 55 56 err := fs.WalkDir(os.DirFS(path), ".", func(filename string, _ fs.DirEntry, err error) error { 57 if err != nil { 58 return err 59 } 60 61 select { 62 case filesChan <- filename: 63 return nil 64 case <-ctx.Done(): 65 return fmt.Errorf("walking dir got interrupted: %w", ctx.Err()) 66 } 67 }) 68 if err != nil { 69 errChan <- err 70 71 break 72 } 73 } 74 }() 75 76 return filesChan, errChan 77 } 78 79 // GitLsFilesContext returns the list of file base on what is in the git index (asynchronously). 80 // 81 // -z is mandatory as some repositories non-ASCII file names which creates 82 // quoted and escaped file names. This method also returns directories for 83 // any submodule there is. Submodule will be skipped afterwards and thus 84 // not checked. 85 func GitLsFilesContext(ctx context.Context, path string) (<-chan string, <-chan error) { 86 filesChan := make(chan string, 128) 87 errChan := make(chan error, 1) 88 89 go func() { 90 defer close(filesChan) 91 defer close(errChan) 92 93 output, err := exec.CommandContext(ctx, "git", "ls-files", "-z", path).Output() 94 if err != nil { 95 var e *exec.ExitError 96 if ok := errors.As(err, &e); ok { 97 if e.ExitCode() == 128 { 98 err = fmt.Errorf("not a git repository: %w", e) 99 } else { 100 err = fmt.Errorf("git ls-files failed with %s: %w", e.Stderr, e) 101 } 102 } 103 104 errChan <- err 105 106 return 107 } 108 109 fs := bytes.Split(output, []byte{0}) 110 // last line is empty 111 for _, f := range fs[:len(fs)-1] { 112 select { 113 case filesChan <- string(f): 114 // everything is good 115 case <-ctx.Done(): 116 return 117 } 118 } 119 }() 120 121 return filesChan, errChan 122 }