diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 69ba5a3..2043619 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -173,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, walkerType walk.Type, pathsCh chan<- string) error { +func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) error { var tx *bolt.Tx var bucket *bolt.Bucket var processed int @@ -185,12 +185,7 @@ func ChangeSet(ctx context.Context, root string, walkerType walk.Type, pathsCh c } }() - 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 { + return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error { select { case <-ctx.Done(): return ctx.Err() diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 03bbb60..8cb444f 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,12 +6,12 @@ import ( "github.com/charmbracelet/log" ) -var Cli = Options{} +var Cli = Format{} -type Options struct { - AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing."` - WorkingDirectory kong.ChangeDirFlag `default:"." short:"C" help:"Run as if treefmt was started in the specified working directory instead of the current working directory."` - ClearCache bool `short:"c" help:"Reset the evaluation cache. Use in case the cache is not precise enough."` +type Format struct { + AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing"` + WorkingDirectory kong.ChangeDirFlag `default:"." short:"C" help:"Run as if treefmt was started in the specified working directory instead of the current working directory"` + ClearCache bool `short:"c" help:"Reset the evaluation cache. Use in case the cache is not precise enough"` ConfigFile string `type:"existingfile" default:"./treefmt.toml"` 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."` @@ -19,17 +19,17 @@ type Options struct { 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:"."` + Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree."` } -func (c *Options) Configure() { +func (f *Format) Configure() { log.SetReportTimestamp(false) - if c.Verbosity == 0 { + if f.Verbosity == 0 { log.SetLevel(log.WarnLevel) - } else if c.Verbosity == 1 { + } else if f.Verbosity == 1 { log.SetLevel(log.InfoLevel) - } else if c.Verbosity >= 2 { + } else if f.Verbosity >= 2 { log.SetLevel(log.DebugLevel) } } diff --git a/internal/cli/format.go b/internal/cli/format.go index cfa254a..68694de 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -9,6 +9,8 @@ import ( "syscall" "time" + "git.numtide.com/numtide/treefmt/internal/walk" + "git.numtide.com/numtide/treefmt/internal/cache" "git.numtide.com/numtide/treefmt/internal/format" @@ -16,8 +18,6 @@ import ( "golang.org/x/sync/errgroup" ) -type Format struct{} - var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled") func (f *Format) Run() error { @@ -189,10 +189,21 @@ func (f *Format) Run() error { return nil }) - eg.Go(func() error { - err := cache.ChangeSet(ctx, Cli.TreeRoot, Cli.Walk, pathsCh) - close(pathsCh) - return err + eg.Go(func() (err error) { + var walker walk.Walker + + if len(Cli.Paths) > 0 { + walker, err = walk.NewPathList(Cli.Paths) + } else { + walker, err = walk.New(Cli.Walk, Cli.TreeRoot) + } + + if err != nil { + return fmt.Errorf("%w: failed to create walker", err) + } + + defer close(pathsCh) + return cache.ChangeSet(ctx, walker, pathsCh) }) // listen for shutdown and call cancel if required diff --git a/internal/cli/format_test.go b/internal/cli/format_test.go index 065b2d6..2dffcc3 100644 --- a/internal/cli/format_test.go +++ b/internal/cli/format_test.go @@ -404,3 +404,47 @@ func TestGitWorktree(t *testing.T) { as.NoError(err) as.Contains(string(out), fmt.Sprintf("%d files changed", 55)) } + +func TestPathsArg(t *testing.T) { + as := require.New(t) + + // capture current cwd, so we can replace it after the test is finished + cwd, err := os.Getwd() + as.NoError(err) + + t.Cleanup(func() { + // return to the previous working directory + as.NoError(os.Chdir(cwd)) + }) + + tempDir := test.TempExamples(t) + configPath := filepath.Join(tempDir, "/treefmt.toml") + + // change working directory to temp root + as.NoError(os.Chdir(tempDir)) + + // basic config + config := format.Config{ + Formatters: map[string]*format.Formatter{ + "echo": { + Command: "echo", + Includes: []string{"*"}, + }, + }, + } + test.WriteConfig(t, configPath, config) + + // without any path args + out, err := cmd(t, "-C", tempDir) + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) + + // specify some explicit paths + out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 2)) + + // specify a bad path + out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs") + as.ErrorContains(err, "no such file or directory") +} diff --git a/internal/walk/paths.go b/internal/walk/paths.go new file mode 100644 index 0000000..0ac5086 --- /dev/null +++ b/internal/walk/paths.go @@ -0,0 +1,56 @@ +package walk + +import ( + "bufio" + "context" + "io" + "os" + "path/filepath" +) + +type paths struct { + reader io.Reader +} + +func (p *paths) Root() string { + return "" +} + +func (p *paths) Walk(ctx context.Context, fn filepath.WalkFunc) error { + scanner := bufio.NewScanner(p.reader) + for scanner.Scan() { + select { + case <-ctx.Done(): + return ctx.Err() + default: + path := scanner.Text() + // stat the file + info, err := os.Lstat(path) + if err = fn(path, info, err); err != nil { + return err + } + } + } + return nil +} + +func NewPathReader(reader io.Reader) (Walker, error) { + return &paths{reader}, nil +} + +func NewPathList(paths []string) (Walker, error) { + r, w := io.Pipe() + + go func() { + for _, path := range paths { + if _, err := w.Write([]byte(path)); err != nil { + _ = r.CloseWithError(err) + } else if _, err = w.Write([]byte("\n")); err != nil { + _ = r.CloseWithError(err) + } + } + _ = r.Close() + }() + + return NewPathReader(r) +}