This repository has been archived on 2024-05-03. You can view files and clone it, but cannot push or open issues or pull requests.
treefmt/cli/format.go
Brian McGee 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

293 lines
6.2 KiB
Go

package cli
import (
"bufio"
"context"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"strings"
"syscall"
"time"
"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"
)
var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
func (f *Format) Run() error {
start := time.Now()
Cli.Configure()
l := log.WithPrefix("format")
defer func() {
if err := cache.Close(); err != nil {
l.Errorf("failed to close cache: %v", err)
}
}()
// create an overall context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// read config
cfg, err := config.ReadFile(Cli.ConfigFile)
if err != nil {
return fmt.Errorf("%w: failed to read config file", err)
}
globalExcludes, err := format2.CompileGlobs(cfg.Global.Excludes)
// create optional formatter filter set
formatterSet := make(map[string]bool)
for _, name := range Cli.Formatters {
_, ok := cfg.Formatters[name]
if !ok {
return fmt.Errorf("%w: formatter not found in config: %v", err, name)
}
formatterSet[name] = true
}
includeFormatter := func(name string) bool {
if len(formatterSet) == 0 {
return true
} else {
_, include := formatterSet[name]
return include
}
}
formatters := make(map[string]*format2.Formatter)
// detect broken dependencies
for name, formatterCfg := range cfg.Formatters {
before := formatterCfg.Before
if before != "" {
// check child formatter exists
_, ok := cfg.Formatters[before]
if !ok {
return fmt.Errorf("formatter %v is before %v but config for %v was not found", name, before, before)
}
}
}
// dependency cycle detection
for name, formatterCfg := range cfg.Formatters {
var ok bool
var history []string
childName := name
for {
// add to history
history = append(history, childName)
if formatterCfg.Before == "" {
break
} else if formatterCfg.Before == name {
return fmt.Errorf("formatter cycle detected %v", strings.Join(history, " -> "))
}
// load child config
childName = formatterCfg.Before
formatterCfg, ok = cfg.Formatters[formatterCfg.Before]
if !ok {
return fmt.Errorf("formatter not found: %v", formatterCfg.Before)
}
}
}
// init formatters
for name, formatterCfg := range cfg.Formatters {
if !includeFormatter(name) {
// remove this formatter
delete(cfg.Formatters, name)
l.Debugf("formatter %v is not in formatter list %v, skipping", name, Cli.Formatters)
continue
}
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 {
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
}
formatters[name] = formatter
}
// iterate the initialised formatters configuring parent/child relationships
for _, formatter := range formatters {
if formatter.Before() != "" {
child, ok := formatters[formatter.Before()]
if !ok {
// formatter has been filtered out by the user
formatter.ResetBefore()
continue
}
formatter.SetChild(child)
child.SetParent(formatter)
}
}
if err = cache.Open(Cli.TreeRoot, Cli.ClearCache, formatters); err != nil {
return err
}
//
completedCh := make(chan string, 1024)
ctx = format2.SetCompletedChannel(ctx, completedCh)
//
eg, ctx := errgroup.WithContext(ctx)
// start the formatters
for name := range formatters {
formatter := formatters[name]
eg.Go(func() error {
return formatter.Run(ctx)
})
}
// determine paths to be formatted
pathsCh := make(chan string, 1024)
// update cache as paths are completed
eg.Go(func() error {
batchSize := 1024
batch := make([]string, 0, batchSize)
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 {
case <-ctx.Done():
return ctx.Err()
case path, ok := <-completedCh:
if !ok {
break LOOP
}
batch = append(batch, path)
if len(batch) == batchSize {
if err = processBatch(); err != nil {
return err
}
}
}
}
// final flush
if err = processBatch(); err != nil {
return err
}
if Cli.FailOnChange && changes != 0 {
return ErrFailOnChange
}
fmt.Printf("%v files changed in %v\n", changes, time.Now().Sub(start))
return nil
})
eg.Go(func() error {
// pass paths to each formatter
for path := range pathsCh {
for _, formatter := range formatters {
if formatter.Wants(path) {
formatter.Put(path)
}
}
}
// indicate no more paths for each formatter
for _, formatter := range formatters {
if formatter.Parent() != nil {
// this formatter is not a root, it will be closed by a parent
continue
}
formatter.Close()
}
// await completion
for _, formatter := range formatters {
formatter.AwaitCompletion()
}
// indicate no more completion events
close(completedCh)
return nil
})
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
go func() {
exit := make(chan os.Signal, 1)
signal.Notify(exit, os.Interrupt, syscall.SIGTERM)
<-exit
cancel()
}()
return eg.Wait()
}