diff --git a/internal/cache/cache.go b/internal/cache/cache.go index a7d83dc..69ba5a3 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -9,6 +9,8 @@ import ( "os" "time" + "git.numtide.com/numtide/treefmt/internal/walk" + "git.numtide.com/numtide/treefmt/internal/format" "github.com/charmbracelet/log" @@ -171,7 +173,7 @@ func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error { // ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh. // It determines if a path is new or has changed by comparing against cache entries. -func ChangeSet(ctx context.Context, root string, walker string, pathsCh chan<- string) error { +func ChangeSet(ctx context.Context, root string, walkerType walk.Type, pathsCh chan<- string) error { var tx *bolt.Tx var bucket *bolt.Bucket var processed int @@ -183,7 +185,12 @@ func ChangeSet(ctx context.Context, root string, walker string, pathsCh chan<- s } }() - return walk(ctx, root, walker, func(path string, info fs.FileInfo, err error) error { + w, err := walk.New(walkerType, root) + if err != nil { + return fmt.Errorf("%w: failed to create walker", err) + } + + return w.Walk(ctx, func(path string, info fs.FileInfo, err error) error { select { case <-ctx.Done(): return ctx.Err() diff --git a/internal/cache/walk.go b/internal/cache/walk.go deleted file mode 100644 index 36ede84..0000000 --- a/internal/cache/walk.go +++ /dev/null @@ -1,83 +0,0 @@ -package cache - -import ( - "bufio" - "context" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - - "github.com/charmbracelet/log" - - "golang.org/x/sync/errgroup" -) - -const ( - GitWalker = "git" - FilesystemWalker = "filesystem" -) - -func walk(ctx context.Context, root string, walker string, fn filepath.WalkFunc) error { - l := log.WithPrefix("walk") - - if walker == GitWalker { - // check if we're dealing with a git repository - cmd := exec.Command("git", "-C", root, "rev-parse", "--is-inside-work-tree") - _, err := cmd.CombinedOutput() - if err != nil { - l.Info("git repo check failed, falling back to filesystem", "err", err) - walker = "filesystem" - } - } - - l.Infof("walking %s with %s", root, walker) - - switch walker { - case GitWalker: - return walkGit(ctx, root, fn) - case FilesystemWalker: - return filepath.Walk(root, fn) - default: - return fmt.Errorf("unknown walker: %s", walker) - } -} - -func walkGit(ctx context.Context, root string, fn filepath.WalkFunc) error { - r, w := io.Pipe() - - cmd := exec.Command("git", "-C", root, "ls-files") - cmd.Stdout = w - cmd.Stderr = w - - eg := errgroup.Group{} - - eg.Go(func() error { - scanner := bufio.NewScanner(r) - - for scanner.Scan() { - select { - case <-ctx.Done(): - return ctx.Err() - default: - line := scanner.Text() - path := filepath.Join(root, line) - - // stat the file - info, err := os.Lstat(path) - if err = fn(path, info, err); err != nil { - return err - } - } - } - - return nil - }) - - if err := w.CloseWithError(cmd.Run()); err != nil { - return err - } - - return eg.Wait() -} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 9526842..03bbb60 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "git.numtide.com/numtide/treefmt/internal/walk" "github.com/alecthomas/kong" "github.com/charmbracelet/log" ) @@ -15,7 +16,7 @@ type Options struct { FailOnChange bool `help:"Exit with error if any changes were made. Useful for CI."` Formatters []string `help:"Specify formatters to apply. Defaults to all formatters."` TreeRoot string `type:"existingdir" default:"."` - Walk string `enum:"filesystem,git" default:"git" help:"The method used to traverse the files within --tree-root. Currently supports 'git' or 'filesystem'."` + Walk walk.Type `enum:"auto,git,filesystem" default:"auto" help:"The method used to traverse the files within --tree-root. Currently supports 'auto', 'git' or 'filesystem'."` Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv."` Format Format `cmd:"" default:"."` diff --git a/internal/walk/filesystem.go b/internal/walk/filesystem.go new file mode 100644 index 0000000..830e9c0 --- /dev/null +++ b/internal/walk/filesystem.go @@ -0,0 +1,22 @@ +package walk + +import ( + "context" + "path/filepath" +) + +type filesystem struct { + root string +} + +func (f filesystem) Root() string { + return f.root +} + +func (f filesystem) Walk(_ context.Context, fn filepath.WalkFunc) error { + return filepath.Walk(f.root, fn) +} + +func NewFilesystem(root string) (Walker, error) { + return filesystem{root}, nil +} diff --git a/internal/walk/git.go b/internal/walk/git.go new file mode 100644 index 0000000..4f03afa --- /dev/null +++ b/internal/walk/git.go @@ -0,0 +1,69 @@ +package walk + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + + "golang.org/x/sync/errgroup" +) + +type git struct { + root string +} + +func (g *git) Root() string { + return g.root +} + +func (g *git) Walk(ctx context.Context, fn filepath.WalkFunc) error { + r, w := io.Pipe() + + cmd := exec.Command("git", "-C", g.root, "ls-files") + cmd.Stdout = w + cmd.Stderr = w + + eg := errgroup.Group{} + + eg.Go(func() error { + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return ctx.Err() + default: + line := scanner.Text() + path := filepath.Join(g.root, line) + + // stat the file + info, err := os.Lstat(path) + if err = fn(path, info, err); err != nil { + return err + } + } + } + + return nil + }) + + if err := w.CloseWithError(cmd.Run()); err != nil { + return err + } + + return eg.Wait() +} + +func NewGit(root string) (Walker, error) { + // check if we're dealing with a git repository + cmd := exec.Command("git", "-C", root, "rev-parse", "--is-inside-work-tree") + _, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%w: git repo check failed", err) + } + return &git{root}, nil +} diff --git a/internal/walk/walker.go b/internal/walk/walker.go new file mode 100644 index 0000000..b00f18e --- /dev/null +++ b/internal/walk/walker.go @@ -0,0 +1,42 @@ +package walk + +import ( + "context" + "fmt" + "path/filepath" +) + +type Type string + +const ( + Git Type = "git" + Auto Type = "auto" + Filesystem Type = "filesystem" +) + +type Walker interface { + Root() string + Walk(ctx context.Context, fn filepath.WalkFunc) error +} + +func New(walkerType Type, root string) (Walker, error) { + switch walkerType { + case Git: + return NewGit(root) + case Auto: + return Detect(root) + case Filesystem: + return NewFilesystem(root) + default: + return nil, fmt.Errorf("unknown walker type: %v", walkerType) + } +} + +func Detect(root string) (Walker, error) { + // for now, we keep it simple and try git first, filesystem second + w, err := NewGit(root) + if err == nil { + return w, err + } + return NewFilesystem(root) +}