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  }