From 0fbae06f19d03ca50f3da50339dc94b92018ab3b Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Wed, 10 Jan 2024 15:45:57 +0000 Subject: [PATCH] feat: support reading paths from stdin Signed-off-by: Brian McGee --- flake.lock | 16 ++++++ flake.nix | 2 + internal/cache/cache.go | 9 +--- internal/cli/cli.go | 21 ++++---- internal/cli/format.go | 29 ++++++++--- internal/cli/format_test.go | 100 ++++++++++++++++++++++++++++++++++++ internal/walk/filesystem.go | 25 +++++++-- internal/walk/git.go | 60 +++++++++++++++++----- internal/walk/walker.go | 14 ++--- nix/packages.nix | 9 +++- nix/treefmt.nix | 9 +++- 11 files changed, 243 insertions(+), 51 deletions(-) diff --git a/flake.lock b/flake.lock index e372a35..70c5801 100644 --- a/flake.lock +++ b/flake.lock @@ -111,6 +111,21 @@ "type": "github" } }, + "nix-filter": { + "locked": { + "lastModified": 1705332318, + "narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "3449dc925982ad46246cfc36469baf66e1b64f17", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-filter", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1707689078, @@ -151,6 +166,7 @@ "flake-parts": "flake-parts", "flake-root": "flake-root", "gomod2nix": "gomod2nix", + "nix-filter": "nix-filter", "nixpkgs": "nixpkgs", "treefmt-nix": "treefmt-nix" } diff --git a/flake.nix b/flake.nix index 8a4ad1a..3fc06af 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,8 @@ url = "github:nix-community/gomod2nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + + nix-filter.url = "github:numtide/nix-filter"; }; outputs = inputs @ {flake-parts, ...}: 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..4837bd5 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,16 @@ 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."` + Stdin bool `help:"Format the context passed in via stdin"` } -func (c *Options) Configure() { +func (f *Format) Configure() { log.SetReportTimestamp(false) - if c.Verbosity == 0 { - log.SetLevel(log.WarnLevel) - } else if c.Verbosity == 1 { + if f.Verbosity == 0 { log.SetLevel(log.InfoLevel) - } else if c.Verbosity >= 2 { + } else if f.Verbosity > 0 { log.SetLevel(log.DebugLevel) } } diff --git a/internal/cli/format.go b/internal/cli/format.go index 253a413..0fc4731 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -1,6 +1,7 @@ package cli import ( + "bufio" "context" "errors" "fmt" @@ -10,6 +11,8 @@ import ( "syscall" "time" + "git.numtide.com/numtide/treefmt/internal/walk" + "git.numtide.com/numtide/treefmt/internal/config" "git.numtide.com/numtide/treefmt/internal/cache" @@ -19,8 +22,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 { @@ -201,7 +202,7 @@ func (f *Format) Run() error { return ErrFailOnChange } - fmt.Printf("%v files changed in %v", changes, time.Now().Sub(start)) + fmt.Printf("%v files changed in %v\n", changes, time.Now().Sub(start)) return nil }) @@ -235,10 +236,24 @@ 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) { + paths := Cli.Paths + + if len(paths) == 0 && Cli.Stdin { + // read in all the paths + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + paths = append(paths, scanner.Text()) + } + } + + walker, err := walk.New(Cli.Walk, Cli.TreeRoot, paths) + 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 de8bc55..62ce5d2 100644 --- a/internal/cli/format_test.go +++ b/internal/cli/format_test.go @@ -481,3 +481,103 @@ func TestOrderingFormatters(t *testing.T) { as.NoError(err) as.Contains(string(out), "8 files changed") } + +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 + cfg := config.Config{ + Formatters: map[string]*config.Formatter{ + "echo": { + Command: "echo", + Includes: []string{"*"}, + }, + }, + } + test.WriteConfig(t, configPath, cfg) + + // 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") +} + +func TestStdIn(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 + cfg := config.Config{ + Formatters: map[string]*config.Formatter{ + "echo": { + Command: "echo", + Includes: []string{"*"}, + }, + }, + } + test.WriteConfig(t, configPath, cfg) + + // swap out stdin + prevStdIn := os.Stdin + stdin, err := os.CreateTemp("", "stdin") + as.NoError(err) + + os.Stdin = stdin + + t.Cleanup(func() { + os.Stdin = prevStdIn + _ = os.Remove(stdin.Name()) + }) + + go func() { + _, err := stdin.WriteString(`treefmt.toml +elm/elm.json +go/main.go +`) + as.NoError(err, "failed to write to stdin") + as.NoError(stdin.Sync()) + _, _ = stdin.Seek(0, 0) + }() + + out, err := cmd(t, "-C", tempDir, "--stdin") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 3)) +} diff --git a/internal/walk/filesystem.go b/internal/walk/filesystem.go index bf84158..82e4faa 100644 --- a/internal/walk/filesystem.go +++ b/internal/walk/filesystem.go @@ -2,11 +2,13 @@ package walk import ( "context" + "os" "path/filepath" ) type filesystemWalker struct { - root string + root string + paths []string } func (f filesystemWalker) Root() string { @@ -14,9 +16,24 @@ func (f filesystemWalker) Root() string { } func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error { - return filepath.Walk(f.root, fn) + if len(f.paths) == 0 { + return filepath.Walk(f.root, fn) + } + + for _, path := range f.paths { + info, err := os.Stat(path) + if err = filepath.Walk(path, fn); err != nil { + return err + } + + if err = fn(path, info, err); err != nil { + return err + } + } + + return nil } -func NewFilesystem(root string) (Walker, error) { - return filesystemWalker{root}, nil +func NewFilesystem(root string, paths []string) (Walker, error) { + return filesystemWalker{root, paths}, nil } diff --git a/internal/walk/git.go b/internal/walk/git.go index 09fd3c7..ea5dc6d 100644 --- a/internal/walk/git.go +++ b/internal/walk/git.go @@ -2,16 +2,22 @@ package walk import ( "context" + "errors" "fmt" + "io/fs" "os" "path/filepath" + "github.com/charmbracelet/log" + "github.com/go-git/go-git/v5/plumbing/format/index" + "github.com/go-git/go-git/v5" ) type gitWalker struct { - root string - repo *git.Repository + root string + paths []string + repo *git.Repository } func (g *gitWalker) Root() string { @@ -24,28 +30,56 @@ func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error { return fmt.Errorf("%w: failed to open index", err) } - for _, entry := range idx.Entries { - select { - case <-ctx.Done(): - return ctx.Err() - default: - path := filepath.Join(g.root, entry.Name) + if len(g.paths) > 0 { + for _, path := range g.paths { - // stat the file - info, err := os.Lstat(path) - if err = fn(path, info, err); err != nil { + err = filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(g.root, path) + if err != nil { + return err + } + + if _, err = idx.Entry(relPath); errors.Is(err, index.ErrEntryNotFound) { + // we skip this path as it's not staged + log.Debugf("Path not found in git index, skipping: %v, %v", relPath, path) + return nil + } + + return fn(path, info, err) + }) + if err != nil { return err } + + } + } else { + for _, entry := range idx.Entries { + select { + case <-ctx.Done(): + return ctx.Err() + default: + path := filepath.Join(g.root, entry.Name) + + // stat the file + info, err := os.Lstat(path) + if err = fn(path, info, err); err != nil { + return err + } + } } } return nil } -func NewGit(root string) (Walker, error) { +func NewGit(root string, paths []string) (Walker, error) { repo, err := git.PlainOpen(root) if err != nil { return nil, fmt.Errorf("%w: failed to open git repo", err) } - return &gitWalker{root, repo}, nil + return &gitWalker{root, paths, repo}, nil } diff --git a/internal/walk/walker.go b/internal/walk/walker.go index b00f18e..a5b5e58 100644 --- a/internal/walk/walker.go +++ b/internal/walk/walker.go @@ -19,24 +19,24 @@ type Walker interface { Walk(ctx context.Context, fn filepath.WalkFunc) error } -func New(walkerType Type, root string) (Walker, error) { +func New(walkerType Type, root string, paths []string) (Walker, error) { switch walkerType { case Git: - return NewGit(root) + return NewGit(root, paths) case Auto: - return Detect(root) + return Detect(root, paths) case Filesystem: - return NewFilesystem(root) + return NewFilesystem(root, paths) default: return nil, fmt.Errorf("unknown walker type: %v", walkerType) } } -func Detect(root string) (Walker, error) { +func Detect(root string, paths []string) (Walker, error) { // for now, we keep it simple and try git first, filesystem second - w, err := NewGit(root) + w, err := NewGit(root, paths) if err == nil { return w, err } - return NewFilesystem(root) + return NewFilesystem(root, paths) } diff --git a/nix/packages.nix b/nix/packages.nix index 7029757..127eb08 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -18,7 +18,14 @@ # ensure we are using the same version of go to build with inherit (pkgs) go; - src = ../.; + src = let + filter = inputs.nix-filter.lib; + in + filter { + root = ../.; + exclude = [./nix]; + }; + modules = ../gomod2nix.toml; ldflags = [ diff --git a/nix/treefmt.nix b/nix/treefmt.nix index 3d5aac6..66022ac 100644 --- a/nix/treefmt.nix +++ b/nix/treefmt.nix @@ -2,11 +2,18 @@ imports = [ inputs.treefmt-nix.flakeModule ]; - perSystem = {config, ...}: { + perSystem = { + config, + self', + ... + }: { treefmt.config = { inherit (config.flake-root) projectRootFile; flakeCheck = true; flakeFormatter = true; + + package = self'.packages.default; + programs = { alejandra.enable = true; deadnix.enable = true;