diff --git a/LICENSE.md b/LICENSE.md index 919790d..d8ade38 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Treefmt Contributors +Copyright (c) 2024 Treefmt Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build/build.go b/build/build.go new file mode 100644 index 0000000..bd1c65e --- /dev/null +++ b/build/build.go @@ -0,0 +1,6 @@ +package build + +var ( + Name = "treefmt" + Version = "v0.0.1+dev" +) diff --git a/internal/cache/cache.go b/cache/cache.go similarity index 94% rename from internal/cache/cache.go rename to cache/cache.go index 69ba5a3..eb56d15 100644 --- a/internal/cache/cache.go +++ b/cache/cache.go @@ -9,9 +9,9 @@ import ( "os" "time" - "git.numtide.com/numtide/treefmt/internal/walk" + "git.numtide.com/numtide/treefmt/format" + "git.numtide.com/numtide/treefmt/walk" - "git.numtide.com/numtide/treefmt/internal/format" "github.com/charmbracelet/log" "github.com/adrg/xdg" @@ -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/cli/cli.go similarity index 64% rename from internal/cli/cli.go rename to cli/cli.go index 03bbb60..e1ed3a6 100644 --- a/internal/cli/cli.go +++ b/cli/cli.go @@ -1,35 +1,38 @@ package cli import ( - "git.numtide.com/numtide/treefmt/internal/walk" + "git.numtide.com/numtide/treefmt/walk" "github.com/alecthomas/kong" "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"` + NoCache bool `help:"Ignore the evaluation cache entirely. Useful for CI"` + 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."` TreeRoot string `type:"existingdir" default:"."` 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."` + Version bool `name:"version" short:"V" help:"Print version"` - 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 { + 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 > 1 { log.SetLevel(log.DebugLevel) } } diff --git a/internal/cli/format.go b/cli/format.go similarity index 73% rename from internal/cli/format.go rename to cli/format.go index 253a413..14ac16c 100644 --- a/internal/cli/format.go +++ b/cli/format.go @@ -1,26 +1,26 @@ package cli import ( + "bufio" "context" "errors" "fmt" + "io/fs" "os" "os/signal" "strings" "syscall" "time" - "git.numtide.com/numtide/treefmt/internal/config" - - "git.numtide.com/numtide/treefmt/internal/cache" - "git.numtide.com/numtide/treefmt/internal/format" + "git.numtide.com/numtide/treefmt/cache" + "git.numtide.com/numtide/treefmt/config" + format2 "git.numtide.com/numtide/treefmt/format" + "git.numtide.com/numtide/treefmt/walk" "github.com/charmbracelet/log" "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 { @@ -46,7 +46,7 @@ func (f *Format) Run() error { return fmt.Errorf("%w: failed to read config file", err) } - globalExcludes, err := format.CompileGlobs(cfg.Global.Excludes) + globalExcludes, err := format2.CompileGlobs(cfg.Global.Excludes) // create optional formatter filter set formatterSet := make(map[string]bool) @@ -67,7 +67,7 @@ func (f *Format) Run() error { } } - formatters := make(map[string]*format.Formatter) + formatters := make(map[string]*format2.Formatter) // detect broken dependencies for name, formatterCfg := range cfg.Formatters { @@ -114,8 +114,8 @@ func (f *Format) Run() error { continue } - formatter, err := format.NewFormatter(name, formatterCfg, globalExcludes) - if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter { + formatter, err := format2.NewFormatter(name, formatterCfg, globalExcludes) + if errors.Is(err, format2.ErrCommandNotFound) && Cli.AllowMissingFormatter { l.Debugf("formatter not found: %v", name) continue } else if err != nil { @@ -146,7 +146,7 @@ func (f *Format) Run() error { // completedCh := make(chan string, 1024) - ctx = format.SetCompletedChannel(ctx, completedCh) + ctx = format2.SetCompletedChannel(ctx, completedCh) // eg, ctx := errgroup.WithContext(ctx) @@ -169,6 +169,20 @@ func (f *Format) Run() error { var changes int + processBatch := func() error { + if Cli.NoCache { + changes += len(batch) + } else { + count, err := cache.Update(batch) + if err != nil { + return err + } + changes += count + } + batch = batch[:0] + return nil + } + LOOP: for { select { @@ -180,28 +194,23 @@ func (f *Format) Run() error { } batch = append(batch, path) if len(batch) == batchSize { - count, err := cache.Update(batch) - if err != nil { + if err = processBatch(); err != nil { return err } - changes += count - batch = batch[:0] } } } // final flush - count, err := cache.Update(batch) - if err != nil { + if err = processBatch(); err != nil { return err } - changes += count if Cli.FailOnChange && changes != 0 { 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 +244,40 @@ 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) + + if Cli.NoCache { + return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + // ignore symlinks and directories + if !(info.IsDir() || info.Mode()&os.ModeSymlink == os.ModeSymlink) { + pathsCh <- path + } + return nil + } + }) + } + + return cache.ChangeSet(ctx, walker, pathsCh) }) // listen for shutdown and call cancel if required diff --git a/internal/cli/format_test.go b/cli/format_test.go similarity index 70% rename from internal/cli/format_test.go rename to cli/format_test.go index de8bc55..4f03231 100644 --- a/internal/cli/format_test.go +++ b/cli/format_test.go @@ -8,15 +8,15 @@ import ( "path/filepath" "testing" - "git.numtide.com/numtide/treefmt/internal/config" + config2 "git.numtide.com/numtide/treefmt/config" + "git.numtide.com/numtide/treefmt/format" + "git.numtide.com/numtide/treefmt/test" - "git.numtide.com/numtide/treefmt/internal/test" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/cache" "github.com/go-git/go-git/v5/storage/filesystem" - "git.numtide.com/numtide/treefmt/internal/format" "github.com/stretchr/testify/require" ) @@ -26,8 +26,8 @@ func TestAllowMissingFormatter(t *testing.T) { tempDir := t.TempDir() configPath := tempDir + "/treefmt.toml" - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, config2.Config{ + Formatters: map[string]*config2.Formatter{ "foo-fmt": { Command: "foo-fmt", }, @@ -47,8 +47,8 @@ func TestDependencyCycle(t *testing.T) { tempDir := t.TempDir() configPath := tempDir + "/treefmt.toml" - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, config2.Config{ + Formatters: map[string]*config2.Formatter{ "a": {Command: "echo", Before: "b"}, "b": {Command: "echo", Before: "c"}, "c": {Command: "echo", Before: "a"}, @@ -68,8 +68,8 @@ func TestSpecifyingFormatters(t *testing.T) { tempDir := test.TempExamples(t) configPath := tempDir + "/treefmt.toml" - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, config2.Config{ + Formatters: map[string]*config2.Formatter{ "elm": { Command: "echo", Includes: []string{"*.elm"}, @@ -117,8 +117,8 @@ func TestIncludesAndExcludes(t *testing.T) { configPath := tempDir + "/echo.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := config2.Config{ + Formatters: map[string]*config2.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -129,7 +129,7 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 30)) // globally exclude nix files cfg.Global.Excludes = []string{"*.nix"} @@ -137,7 +137,7 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 28)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) // add haskell files to the global exclude cfg.Global.Excludes = []string{"*.nix", "*.hs"} @@ -145,7 +145,7 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 22)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 23)) echo := cfg.Formatters["echo"] @@ -155,7 +155,7 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 20)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 21)) // remove go files from the echo formatter echo.Excludes = []string{"*.py", "*.go"} @@ -163,7 +163,7 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 19)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 20)) // adjust the includes for echo to only include elm files echo.Includes = []string{"*.elm"} @@ -189,8 +189,8 @@ func TestCache(t *testing.T) { configPath := tempDir + "/echo.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := config2.Config{ + Formatters: map[string]*config2.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -201,11 +201,34 @@ func TestCache(t *testing.T) { test.WriteConfig(t, configPath, cfg) out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 30)) out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) as.Contains(string(out), "0 files changed") + + // clear cache + out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 30)) + + out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + as.NoError(err) + as.Contains(string(out), "0 files changed") + + // clear cache + out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 30)) + + out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + as.NoError(err) + as.Contains(string(out), "0 files changed") + + // no cache + out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") + as.NoError(err) + as.Contains(string(out), fmt.Sprintf("%d files changed", 30)) } func TestChangeWorkingDirectory(t *testing.T) { @@ -224,8 +247,8 @@ func TestChangeWorkingDirectory(t *testing.T) { configPath := tempDir + "/treefmt.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := config2.Config{ + Formatters: map[string]*config2.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -239,7 +262,7 @@ func TestChangeWorkingDirectory(t *testing.T) { // this should fail if the working directory hasn't been changed first out, err := cmd(t, "-C", tempDir) as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 30)) } func TestFailOnChange(t *testing.T) { @@ -249,8 +272,8 @@ func TestFailOnChange(t *testing.T) { configPath := tempDir + "/echo.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := config2.Config{ + Formatters: map[string]*config2.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -285,8 +308,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) { as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH"))) // start with 2 formatters - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := config2.Config{ + Formatters: map[string]*config2.Formatter{ "python": { Command: "black", Includes: []string{"*.py"}, @@ -330,7 +353,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) { as.Contains(string(out), "0 files changed") // add go formatter - cfg.Formatters["go"] = &config.Formatter{ + cfg.Formatters["go"] = &config2.Formatter{ Command: "gofmt", Options: []string{"-w"}, Includes: []string{"*.go"}, @@ -380,8 +403,8 @@ func TestGitWorktree(t *testing.T) { configPath := filepath.Join(tempDir, "/treefmt.toml") // basic config - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := config2.Config{ + Formatters: map[string]*config2.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -416,16 +439,16 @@ func TestGitWorktree(t *testing.T) { // add everything to the worktree as.NoError(wt.AddGlob(".")) as.NoError(err) - run(29) + run(30) // remove python directory as.NoError(wt.RemoveGlob("python/*")) - run(26) + run(27) // walk with filesystem instead of git out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem") as.NoError(err) - as.Contains(string(out), fmt.Sprintf("%d files changed", 55)) + as.Contains(string(out), fmt.Sprintf("%d files changed", 57)) } func TestOrderingFormatters(t *testing.T) { @@ -435,8 +458,8 @@ func TestOrderingFormatters(t *testing.T) { configPath := path.Join(tempDir, "treefmt.toml") // missing child - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, config2.Config{ + Formatters: map[string]*config2.Formatter{ "hs-a": { Command: "echo", Includes: []string{"*.hs"}, @@ -449,8 +472,8 @@ func TestOrderingFormatters(t *testing.T) { as.ErrorContains(err, "formatter hs-a is before hs-b but config for hs-b was not found") // multiple roots - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, config2.Config{ + Formatters: map[string]*config2.Formatter{ "hs-a": { Command: "echo", Includes: []string{"*.hs"}, @@ -481,3 +504,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 := config2.Config{ + Formatters: map[string]*config2.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", 30)) + + // 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 := config2.Config{ + Formatters: map[string]*config2.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/cli/helpers_test.go b/cli/helpers_test.go similarity index 96% rename from internal/cli/helpers_test.go rename to cli/helpers_test.go index 7268d23..920c757 100644 --- a/internal/cli/helpers_test.go +++ b/cli/helpers_test.go @@ -7,7 +7,8 @@ import ( "path/filepath" "testing" - "git.numtide.com/numtide/treefmt/internal/test" + "git.numtide.com/numtide/treefmt/test" + "github.com/alecthomas/kong" "github.com/stretchr/testify/require" ) diff --git a/internal/config/config.go b/config/config.go similarity index 100% rename from internal/config/config.go rename to config/config.go diff --git a/internal/config/config_test.go b/config/config_test.go similarity index 89% rename from internal/config/config_test.go rename to config/config_test.go index 611fc04..f50e6c7 100644 --- a/internal/config/config_test.go +++ b/config/config_test.go @@ -9,7 +9,7 @@ import ( func TestReadConfigFile(t *testing.T) { as := require.New(t) - cfg, err := ReadFile("../../test/treefmt.toml") + cfg, err := ReadFile("../test/examples/treefmt.toml") as.NoError(err, "failed to read config file") as.NotNil(cfg) @@ -52,13 +52,13 @@ func TestReadConfigFile(t *testing.T) { as.Equal([]string{"*.hs"}, haskell.Includes) as.Equal([]string{"examples/haskell/"}, haskell.Excludes) - // nix - nix, ok := cfg.Formatters["nix"] - as.True(ok, "nix formatter not found") - as.Equal("alejandra", nix.Command) - as.Nil(nix.Options) - as.Equal([]string{"*.nix"}, nix.Includes) - as.Equal([]string{"examples/nix/sources.nix"}, nix.Excludes) + // alejandra + alejandra, ok := cfg.Formatters["alejandra"] + as.True(ok, "alejandra formatter not found") + as.Equal("alejandra", alejandra.Command) + as.Nil(alejandra.Options) + as.Equal([]string{"*.nix"}, alejandra.Includes) + as.Equal([]string{"examples/nix/sources.nix"}, alejandra.Excludes) // ruby ruby, ok := cfg.Formatters["ruby"] diff --git a/internal/config/formatter.go b/config/formatter.go similarity index 100% rename from internal/config/formatter.go rename to config/formatter.go 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/format/context.go b/format/context.go similarity index 100% rename from internal/format/context.go rename to format/context.go diff --git a/internal/format/formatter.go b/format/formatter.go similarity index 99% rename from internal/format/formatter.go rename to format/formatter.go index a2654dd..434addf 100644 --- a/internal/format/formatter.go +++ b/format/formatter.go @@ -7,7 +7,7 @@ import ( "os/exec" "time" - "git.numtide.com/numtide/treefmt/internal/config" + "git.numtide.com/numtide/treefmt/config" "github.com/charmbracelet/log" "github.com/gobwas/glob" diff --git a/internal/format/glob.go b/format/glob.go similarity index 100% rename from internal/format/glob.go rename to format/glob.go diff --git a/internal/walk/filesystem.go b/internal/walk/filesystem.go deleted file mode 100644 index bf84158..0000000 --- a/internal/walk/filesystem.go +++ /dev/null @@ -1,22 +0,0 @@ -package walk - -import ( - "context" - "path/filepath" -) - -type filesystemWalker struct { - root string -} - -func (f filesystemWalker) Root() string { - return f.root -} - -func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error { - return filepath.Walk(f.root, fn) -} - -func NewFilesystem(root string) (Walker, error) { - return filesystemWalker{root}, nil -} diff --git a/internal/walk/git.go b/internal/walk/git.go deleted file mode 100644 index 09fd3c7..0000000 --- a/internal/walk/git.go +++ /dev/null @@ -1,51 +0,0 @@ -package walk - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/go-git/go-git/v5" -) - -type gitWalker struct { - root string - repo *git.Repository -} - -func (g *gitWalker) Root() string { - return g.root -} - -func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error { - idx, err := g.repo.Storer.Index() - if err != nil { - 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) - - // 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) { - repo, err := git.PlainOpen(root) - if err != nil { - return nil, fmt.Errorf("%w: failed to open git repo", err) - } - return &gitWalker{root, repo}, nil -} diff --git a/main.go b/main.go index 72d162e..714a4af 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,26 @@ package main import ( - "git.numtide.com/numtide/treefmt/internal/cli" + "fmt" + "os" + + "git.numtide.com/numtide/treefmt/build" + "git.numtide.com/numtide/treefmt/cli" "github.com/alecthomas/kong" ) func main() { + // This is to maintain compatibility with 1.0.0 which allows specifying the version with a `treefmt --version` flag + // on the 'default' command. With Kong it would be better to have `treefmt version` so it would be treated as a + // separate command. As it is, we would need to weaken some of the `existingdir` and `existingfile` checks kong is + // doing for us in the default format command. + for _, arg := range os.Args { + if arg == "--version" || arg == "-V" { + fmt.Printf("%s %s\n", build.Name, build.Version) + return + } + } + ctx := kong.Parse(&cli.Cli) ctx.FatalIfErrorf(ctx.Run()) } diff --git a/nix/formatters.nix b/nix/formatters.nix index 0d968de..9788f7f 100644 --- a/nix/formatters.nix +++ b/nix/formatters.nix @@ -12,5 +12,7 @@ with pkgs; [ rustfmt shellcheck shfmt + statix + deadnix terraform ] diff --git a/nix/packages.nix b/nix/packages.nix index 7029757..e0f8604 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -13,12 +13,19 @@ packages = rec { treefmt = inputs'.gomod2nix.legacyPackages.buildGoApplication rec { pname = "treefmt"; - version = "0.0.1+dev"; + version = "2.0.0+dev"; # 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; diff --git a/test/echo.toml b/test/examples/echo.toml similarity index 100% rename from test/echo.toml rename to test/examples/echo.toml diff --git a/test/treefmt.toml b/test/examples/treefmt.toml similarity index 89% rename from test/treefmt.toml rename to test/examples/treefmt.toml index c78bd78..699665a 100644 --- a/test/treefmt.toml +++ b/test/examples/treefmt.toml @@ -26,11 +26,17 @@ options = [ includes = ["*.hs"] excludes = ["examples/haskell/"] -[formatter.nix] +[formatter.alejandra] command = "alejandra" includes = ["*.nix"] # Act as an example on how to exclude specific files excludes = ["examples/nix/sources.nix"] +# Make this run before deadnix +# Note this formatter determines the file set for any 'downstream' formatters +before = "deadnix" + +[formatter.deadnix] +command = "deadnix" [formatter.ruby] command = "rufo" diff --git a/internal/test/temp.go b/test/temp.go similarity index 86% rename from internal/test/temp.go rename to test/temp.go index 2324d1c..dbe6f50 100644 --- a/internal/test/temp.go +++ b/test/temp.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "git.numtide.com/numtide/treefmt/internal/config" + "git.numtide.com/numtide/treefmt/config" "github.com/BurntSushi/toml" cp "github.com/otiai10/copy" @@ -25,7 +25,7 @@ func WriteConfig(t *testing.T, path string, cfg config.Config) { func TempExamples(t *testing.T) string { tempDir := t.TempDir() - require.NoError(t, cp.Copy("../../test/examples", tempDir), "failed to copy test data to temp dir") + require.NoError(t, cp.Copy("../test/examples", tempDir), "failed to copy test data to temp dir") return tempDir } diff --git a/walk/filesystem.go b/walk/filesystem.go new file mode 100644 index 0000000..82e4faa --- /dev/null +++ b/walk/filesystem.go @@ -0,0 +1,39 @@ +package walk + +import ( + "context" + "os" + "path/filepath" +) + +type filesystemWalker struct { + root string + paths []string +} + +func (f filesystemWalker) Root() string { + return f.root +} + +func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error { + 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, paths []string) (Walker, error) { + return filesystemWalker{root, paths}, nil +} diff --git a/walk/git.go b/walk/git.go new file mode 100644 index 0000000..ea5dc6d --- /dev/null +++ b/walk/git.go @@ -0,0 +1,85 @@ +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 + paths []string + repo *git.Repository +} + +func (g *gitWalker) Root() string { + return g.root +} + +func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error { + idx, err := g.repo.Storer.Index() + if err != nil { + return fmt.Errorf("%w: failed to open index", err) + } + + if len(g.paths) > 0 { + for _, path := range g.paths { + + 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, 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, paths, repo}, nil +} diff --git a/internal/walk/walker.go b/walk/walker.go similarity index 63% rename from internal/walk/walker.go rename to walk/walker.go index b00f18e..a5b5e58 100644 --- a/internal/walk/walker.go +++ b/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) }