Compare commits

...

8 Commits

Author SHA1 Message Date
9de4fd4cf9 Merge pull request 'Feature parity with treefmt.rs' (#22) from feat/explicit-paths-and-stdin into main
Reviewed-on: #22
Reviewed-by: Jonas Chevalier <zimbatm@noreply.git.numtide.com>
2024-02-19 09:54:58 +00:00
2b49923bf7
feat: add an example of 'before'
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-02-15 14:17:16 +00:00
f4700c4e63
doc: update licence copyright year
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-02-15 14:00:47 +00:00
cb8565d683
fix: reduce log verbosity
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-02-15 13:59:56 +00:00
da82b80f29
feat: support --no-cache
Signed-off-by: Brian McGee <brian@bmcgee.ie>

diff --git a/cli/cli.go b/cli/cli.go
index 8b23262..b370ee7 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -11,6 +11,7 @@ var Cli = Format{}
 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."`
diff --git a/cli/format.go b/cli/format.go
index 6c46096..14ac16c 100644
--- a/cli/format.go
+++ b/cli/format.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
 	"os/signal"
 	"strings"
@@ -168,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 {
@@ -179,22 +194,17 @@ 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
@@ -251,6 +261,22 @@ func (f *Format) Run() error {
 		}

 		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)
 	})

diff --git a/cli/format_test.go b/cli/format_test.go
index fb389fe..2349767 100644
--- a/cli/format_test.go
+++ b/cli/format_test.go
@@ -216,6 +216,15 @@ func TestCache(t *testing.T) {
 	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", 29))
+
+	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)
diff --git a/nix/packages.nix b/nix/packages.nix
index 127eb08..e0f8604 100644
--- a/nix/packages.nix
+++ b/nix/packages.nix
@@ -13,7 +13,7 @@
     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;

diff --git a/cli/cli.go b/cli/cli.go
index 8b23262..b370ee7 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -11,6 +11,7 @@ var Cli = Format{}
 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."`
diff --git a/cli/format.go b/cli/format.go
index 6c46096..14ac16c 100644
--- a/cli/format.go
+++ b/cli/format.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
 	"os/signal"
 	"strings"
@@ -168,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 {
@@ -179,22 +194,17 @@ 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
@@ -251,6 +261,22 @@ func (f *Format) Run() error {
 		}

 		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)
 	})

diff --git a/nix/packages.nix b/nix/packages.nix
index 127eb08..e0f8604 100644
--- a/nix/packages.nix
+++ b/nix/packages.nix
@@ -13,7 +13,7 @@
     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;
2024-02-15 13:59:56 +00:00
d53f98ea05
feat: support --version
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-02-15 13:59:56 +00:00
d4ab015bc6
chore: remove internal directory
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-02-15 13:59:55 +00:00
0fbae06f19
feat: support reading paths from stdin
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-02-15 10:37:56 +00:00
27 changed files with 450 additions and 177 deletions

View File

@ -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

6
build/build.go Normal file
View File

@ -0,0 +1,6 @@
package build
var (
Name = "treefmt"
Version = "v0.0.1+dev"
)

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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

View File

@ -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))
}

View File

@ -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"
)

View File

@ -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"]

View File

@ -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"
}

View File

@ -18,6 +18,8 @@
url = "github:nix-community/gomod2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-filter.url = "github:numtide/nix-filter";
};
outputs = inputs @ {flake-parts, ...}:

View File

@ -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"

View File

@ -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
}

View File

@ -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
}

17
main.go
View File

@ -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())
}

View File

@ -12,5 +12,7 @@ with pkgs; [
rustfmt
shellcheck
shfmt
statix
deadnix
terraform
]

View File

@ -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 = [

View File

@ -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;

View File

@ -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"

View File

@ -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
}

39
walk/filesystem.go Normal file
View File

@ -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
}

85
walk/git.go Normal file
View File

@ -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
}

View File

@ -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)
}