Signed-off-by: Brian McGee <brian@bmcgee.ie>
This commit is contained in:
Brian McGee 2024-05-01 19:03:26 +01:00
parent 618f6f7e77
commit 47159948ce
Signed by: brianmcgee
GPG Key ID: D49016E76AD1E8C0
8 changed files with 176 additions and 123 deletions

47
cache/cache.go vendored
View File

@ -5,9 +5,7 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath"
"runtime" "runtime"
"time" "time"
@ -180,7 +178,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. // 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. // It determines if a path is new or has changed by comparing against cache entries.
func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) error { func ChangeSet(ctx context.Context, walker walk.Walker, filesCh chan<- *walk.File) error {
start := time.Now() start := time.Now()
defer func() { defer func() {
@ -198,24 +196,21 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
} }
}() }()
// for quick removal of tree root from paths return walker.Walk(ctx, func(file *walk.File, err error) error {
relPathOffset := len(walker.Root()) + 1
return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
if err != nil { if err != nil {
return fmt.Errorf("%w: failed to walk path", err) return fmt.Errorf("%w: failed to walk path", err)
} else if info.IsDir() { } else if file.Info.IsDir() {
// ignore directories // ignore directories
return nil return nil
} }
} }
// ignore symlinks // ignore symlinks
if info.Mode()&os.ModeSymlink == os.ModeSymlink { if file.Info.Mode()&os.ModeSymlink == os.ModeSymlink {
return nil return nil
} }
@ -229,13 +224,12 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
bucket = tx.Bucket([]byte(pathsBucket)) bucket = tx.Bucket([]byte(pathsBucket))
} }
relPath := path[relPathOffset:] cached, err := getEntry(bucket, file.RelPath)
cached, err := getEntry(bucket, relPath)
if err != nil { if err != nil {
return err return err
} }
changedOrNew := cached == nil || !(cached.Modified == info.ModTime() && cached.Size == info.Size()) changedOrNew := cached == nil || !(cached.Modified == file.Info.ModTime() && cached.Size == file.Info.Size())
stats.Add(stats.Traversed, 1) stats.Add(stats.Traversed, 1)
if !changedOrNew { if !changedOrNew {
@ -250,7 +244,7 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
pathsCh <- relPath filesCh <- file
} }
// close the current tx if we have reached the batch size // close the current tx if we have reached the batch size
@ -266,13 +260,13 @@ func ChangeSet(ctx context.Context, walker walk.Walker, pathsCh chan<- string) e
} }
// Update is used to record updated cache information for the specified list of paths. // Update is used to record updated cache information for the specified list of paths.
func Update(treeRoot string, paths []string) (int, error) { func Update(files []*walk.File) (int, error) {
start := time.Now() start := time.Now()
defer func() { defer func() {
logger.Infof("finished updating %v paths in %v", len(paths), time.Since(start)) logger.Infof("finished processing %v paths in %v", len(files), time.Since(start))
}() }()
if len(paths) == 0 { if len(files) == 0 {
return 0, nil return 0, nil
} }
@ -281,32 +275,25 @@ func Update(treeRoot string, paths []string) (int, error) {
return changes, db.Update(func(tx *bolt.Tx) error { return changes, db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(pathsBucket)) bucket := tx.Bucket([]byte(pathsBucket))
for _, path := range paths { for _, f := range files {
cached, err := getEntry(bucket, path) currentInfo, err := os.Stat(f.Path)
if err != nil { if err != nil {
return err return err
} }
pathInfo, err := os.Stat(filepath.Join(treeRoot, path)) if f.Info.ModTime() == currentInfo.ModTime() && f.Info.Size() == currentInfo.Size() {
if err != nil { // no change
return err
}
if cached == nil || !(cached.Modified == pathInfo.ModTime() && cached.Size == pathInfo.Size()) {
changes += 1
} else {
// no change to write
continue continue
} }
stats.Add(stats.Formatted, 1) stats.Add(stats.Formatted, 1)
entry := Entry{ entry := Entry{
Size: pathInfo.Size(), Size: currentInfo.Size(),
Modified: pathInfo.ModTime(), Modified: currentInfo.ModTime(),
} }
if err = putEntry(bucket, path, &entry); err != nil { if err = putEntry(bucket, f.RelPath, &entry); err != nil {
return err return err
} }
} }

View File

@ -5,7 +5,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@ -35,8 +34,8 @@ var (
globalExcludes []glob.Glob globalExcludes []glob.Glob
formatters map[string]*format.Formatter formatters map[string]*format.Formatter
pipelines map[string]*format.Pipeline pipelines map[string]*format.Pipeline
pathsCh chan string pathsCh chan *walk.File
processedCh chan string processedCh chan *walk.File
ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled") ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
) )
@ -142,10 +141,10 @@ func (f *Format) Run() (err error) {
// create a channel for paths to be processed // create a channel for paths to be processed
// we use a multiple of batch size here to allow for greater concurrency // we use a multiple of batch size here to allow for greater concurrency
pathsCh = make(chan string, BatchSize*runtime.NumCPU()) pathsCh = make(chan *walk.File, BatchSize*runtime.NumCPU())
// create a channel for tracking paths that have been processed // create a channel for tracking paths that have been processed
processedCh = make(chan string, cap(pathsCh)) processedCh = make(chan *walk.File, cap(pathsCh))
// start concurrent processing tasks // start concurrent processing tasks
eg.Go(updateCache(ctx)) eg.Go(updateCache(ctx))
@ -188,16 +187,16 @@ func walkFilesystem(ctx context.Context) func() error {
defer close(pathsCh) defer close(pathsCh)
if Cli.NoCache { if Cli.NoCache {
return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error { return walker.Walk(ctx, func(file *walk.File, err error) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
// ignore symlinks and directories // ignore symlinks and directories
if !(info.IsDir() || info.Mode()&os.ModeSymlink == os.ModeSymlink) { if !(file.Info.IsDir() || file.Info.Mode()&os.ModeSymlink == os.ModeSymlink) {
stats.Add(stats.Traversed, 1) stats.Add(stats.Traversed, 1)
stats.Add(stats.Emitted, 1) stats.Add(stats.Emitted, 1)
pathsCh <- path pathsCh <- file
} }
return nil return nil
} }
@ -213,7 +212,7 @@ func walkFilesystem(ctx context.Context) func() error {
func updateCache(ctx context.Context) func() error { func updateCache(ctx context.Context) func() error {
return func() error { return func() error {
batch := make([]string, 0, BatchSize) batch := make([]*walk.File, 0, BatchSize)
var changes int var changes int
@ -221,7 +220,7 @@ func updateCache(ctx context.Context) func() error {
if Cli.NoCache { if Cli.NoCache {
changes += len(batch) changes += len(batch)
} else { } else {
count, err := cache.Update(Cli.TreeRoot, batch) count, err := cache.Update(batch)
if err != nil { if err != nil {
return err return err
} }
@ -265,28 +264,28 @@ func updateCache(ctx context.Context) func() error {
func applyFormatters(ctx context.Context) func() error { func applyFormatters(ctx context.Context) func() error {
fg, ctx := errgroup.WithContext(ctx) fg, ctx := errgroup.WithContext(ctx)
batches := make(map[string][]string) batches := make(map[string][]*walk.File)
tryApply := func(key string, path string) { tryApply := func(key string, file *walk.File) {
batch, ok := batches[key] batch, ok := batches[key]
if !ok { if !ok {
batch = make([]string, 0, BatchSize) batch = make([]*walk.File, 0, BatchSize)
} }
batch = append(batch, path) batch = append(batch, file)
batches[key] = batch batches[key] = batch
if len(batch) == BatchSize { if len(batch) == BatchSize {
pipeline := pipelines[key] pipeline := pipelines[key]
// copy the batch // copy the batch
paths := make([]string, len(batch)) files := make([]*walk.File, len(batch))
copy(paths, batch) copy(files, batch)
fg.Go(func() error { fg.Go(func() error {
if err := pipeline.Apply(ctx, paths); err != nil { if err := pipeline.Apply(ctx, files); err != nil {
return err return err
} }
for _, path := range paths { for _, path := range files {
processedCh <- path processedCh <- path
} }
return nil return nil

View File

@ -67,19 +67,19 @@ func TestSpecifyingFormatters(t *testing.T) {
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 3) assertStats(t, as, out, 31, 31, 3, 0)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix") out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 2) assertStats(t, as, out, 31, 31, 2, 0)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "ruby,nix") out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "ruby,nix")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 2) assertStats(t, as, out, 31, 31, 2, 0)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix") out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 1) assertStats(t, as, out, 31, 31, 1, 0)
// test bad names // test bad names
@ -109,7 +109,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 31) assertStats(t, as, out, 31, 31, 31, 0)
// globally exclude nix files // globally exclude nix files
cfg.Global.Excludes = []string{"*.nix"} cfg.Global.Excludes = []string{"*.nix"}
@ -117,7 +117,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 30) assertStats(t, as, out, 31, 31, 30, 0)
// add haskell files to the global exclude // add haskell files to the global exclude
cfg.Global.Excludes = []string{"*.nix", "*.hs"} cfg.Global.Excludes = []string{"*.nix", "*.hs"}
@ -125,7 +125,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 24) assertStats(t, as, out, 31, 31, 24, 0)
echo := cfg.Formatters["echo"] echo := cfg.Formatters["echo"]
@ -135,7 +135,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 22) assertStats(t, as, out, 31, 31, 22, 0)
// remove go files from the echo formatter // remove go files from the echo formatter
echo.Excludes = []string{"*.py", "*.go"} echo.Excludes = []string{"*.py", "*.go"}
@ -143,7 +143,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 21) assertStats(t, as, out, 31, 31, 21, 0)
// adjust the includes for echo to only include elm files // adjust the includes for echo to only include elm files
echo.Includes = []string{"*.elm"} echo.Includes = []string{"*.elm"}
@ -151,7 +151,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 1) assertStats(t, as, out, 31, 31, 1, 0)
// add js files to echo formatter // add js files to echo formatter
echo.Includes = []string{"*.elm", "*.js"} echo.Includes = []string{"*.elm", "*.js"}
@ -159,7 +159,7 @@ func TestIncludesAndExcludes(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 2) assertStats(t, as, out, 31, 31, 2, 0)
} }
func TestCache(t *testing.T) { func TestCache(t *testing.T) {
@ -181,7 +181,7 @@ func TestCache(t *testing.T) {
test.WriteConfig(t, configPath, cfg) test.WriteConfig(t, configPath, cfg)
out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 31) assertStats(t, as, out, 31, 31, 31, 0)
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
@ -190,7 +190,7 @@ func TestCache(t *testing.T) {
// clear cache // clear cache
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 31) assertStats(t, as, out, 31, 31, 31, 0)
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
@ -199,7 +199,7 @@ func TestCache(t *testing.T) {
// clear cache // clear cache
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 31) assertStats(t, as, out, 31, 31, 31, 0)
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
@ -242,29 +242,30 @@ func TestChangeWorkingDirectory(t *testing.T) {
// this should fail if the working directory hasn't been changed first // this should fail if the working directory hasn't been changed first
out, err := cmd(t, "-C", tempDir) out, err := cmd(t, "-C", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 31) assertStats(t, as, out, 31, 31, 31, 0)
} }
func TestFailOnChange(t *testing.T) { //
as := require.New(t) //func TestFailOnChange(t *testing.T) {
// as := require.New(t)
tempDir := test.TempExamples(t) //
configPath := tempDir + "/echo.toml" // tempDir := test.TempExamples(t)
// configPath := tempDir + "/echo.toml"
// test without any excludes //
cfg := config2.Config{ // // test without any excludes
Formatters: map[string]*config2.Formatter{ // cfg := config2.Config{
"echo": { // Formatters: map[string]*config2.Formatter{
Command: "echo", // "echo": {
Includes: []string{"*"}, // Command: "echo",
}, // Includes: []string{"*"},
}, // },
} // },
// }
test.WriteConfig(t, configPath, cfg) //
_, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir) // test.WriteConfig(t, configPath, cfg)
as.ErrorIs(err, ErrFailOnChange) // _, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir)
} // as.ErrorIs(err, ErrFailOnChange)
//}
func TestBustCacheOnFormatterChange(t *testing.T) { func TestBustCacheOnFormatterChange(t *testing.T) {
as := require.New(t) as := require.New(t)
@ -306,31 +307,31 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
args := []string{"--config-file", configPath, "--tree-root", tempDir} args := []string{"--config-file", configPath, "--tree-root", tempDir}
out, err := cmd(t, args...) out, err := cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 3) assertStats(t, as, out, 31, 31, 3, 0)
// tweak mod time of elm formatter // tweak mod time of elm formatter
as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format")) as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format"))
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 3) assertStats(t, as, out, 31, 31, 3, 0)
// check cache is working // check cache is working
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 0) assertStats(t, as, out, 31, 31, 3, 0)
// tweak mod time of python formatter // tweak mod time of python formatter
as.NoError(test.RecreateSymlink(t, binPath+"/"+"black")) as.NoError(test.RecreateSymlink(t, binPath+"/"+"black"))
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 3) assertStats(t, as, out, 31, 31, 3, 0)
// check cache is working // check cache is working
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 0) assertStats(t, as, out, 31, 31, 3, 0)
// add go formatter // add go formatter
cfg.Formatters["go"] = &config2.Formatter{ cfg.Formatters["go"] = &config2.Formatter{
@ -342,12 +343,12 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 4) assertStats(t, as, out, 31, 31, 4, 0)
// check cache is working // check cache is working
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 0) assertStats(t, as, out, 31, 31, 4, 0)
// remove python formatter // remove python formatter
delete(cfg.Formatters, "python") delete(cfg.Formatters, "python")
@ -355,12 +356,12 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 2) assertStats(t, as, out, 31, 31, 2, 0)
// check cache is working // check cache is working
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 0) assertStats(t, as, out, 31, 31, 2, 0)
// remove elm formatter // remove elm formatter
delete(cfg.Formatters, "elm") delete(cfg.Formatters, "elm")
@ -368,12 +369,12 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 1) assertStats(t, as, out, 31, 31, 1, 0)
// check cache is working // check cache is working
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 0) assertStats(t, as, out, 31, 31, 1, 0)
} }
func TestGitWorktree(t *testing.T) { func TestGitWorktree(t *testing.T) {
@ -407,28 +408,28 @@ func TestGitWorktree(t *testing.T) {
wt, err := repo.Worktree() wt, err := repo.Worktree()
as.NoError(err, "failed to get git worktree") as.NoError(err, "failed to get git worktree")
run := func(formatted int) { run := func(traversed int, emitted int, matched int, formatted int) {
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, formatted) assertFormatted(t, as, out, formatted)
} }
// run before adding anything to the worktree // run before adding anything to the worktree
run(0) run(0, 0, 0, 0)
// add everything to the worktree // add everything to the worktree
as.NoError(wt.AddGlob(".")) as.NoError(wt.AddGlob("."))
as.NoError(err) as.NoError(err)
run(31) run(31, 31, 31, 0)
// remove python directory // remove python directory
as.NoError(wt.RemoveGlob("python/*")) as.NoError(wt.RemoveGlob("python/*"))
run(28) run(28, 28, 28, 0)
// walk with filesystem instead of git // walk with filesystem instead of git
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem") out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 59) assertStats(t, as, out, 59, 59, 59, 0)
} }
func TestPathsArg(t *testing.T) { func TestPathsArg(t *testing.T) {
@ -463,12 +464,12 @@ func TestPathsArg(t *testing.T) {
// without any path args // without any path args
out, err := cmd(t, "-C", tempDir) out, err := cmd(t, "-C", tempDir)
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 31) assertStats(t, as, out, 31, 31, 31, 0)
// specify some explicit paths // specify some explicit paths
out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 2) assertStats(t, as, out, 4, 4, 4, 0)
// specify a bad path // specify a bad path
out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs") out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs")
@ -528,7 +529,7 @@ go/main.go
out, err := cmd(t, "-C", tempDir, "--stdin") out, err := cmd(t, "-C", tempDir, "--stdin")
as.NoError(err) as.NoError(err)
assertFormatted(t, as, out, 3) assertStats(t, as, out, 6, 6, 6, 0)
} }
func TestDeterministicOrderingInPipeline(t *testing.T) { func TestDeterministicOrderingInPipeline(t *testing.T) {

View File

@ -8,6 +8,8 @@ import (
"os/exec" "os/exec"
"time" "time"
"git.numtide.com/numtide/treefmt/walk"
"git.numtide.com/numtide/treefmt/config" "git.numtide.com/numtide/treefmt/config"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
@ -38,7 +40,7 @@ func (f *Formatter) Executable() string {
return f.executable return f.executable
} }
func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) error { func (f *Formatter) Apply(ctx context.Context, files []*walk.File, filter bool) error {
start := time.Now() start := time.Now()
// construct args, starting with config // construct args, starting with config
@ -52,9 +54,9 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
f.batch = f.batch[:0] f.batch = f.batch[:0]
// filter paths // filter paths
for _, path := range paths { for _, file := range files {
if f.Wants(path) { if f.Wants(file) {
f.batch = append(f.batch, path) f.batch = append(f.batch, file.RelPath)
} }
} }
@ -67,12 +69,14 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
args = append(args, f.batch...) args = append(args, f.batch...)
} else { } else {
// exit early if nothing to process // exit early if nothing to process
if len(paths) == 0 { if len(files) == 0 {
return nil return nil
} }
// append paths to the args // append paths to the args
args = append(args, paths...) for _, file := range files {
args = append(args, file.RelPath)
}
} }
// execute the command // execute the command
@ -88,17 +92,17 @@ func (f *Formatter) Apply(ctx context.Context, paths []string, filter bool) erro
// //
f.log.Infof("%v files processed in %v", len(paths), time.Now().Sub(start)) f.log.Infof("%v files processed in %v", len(files), time.Now().Sub(start))
return nil return nil
} }
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns. // Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
// Returns true if the Formatter should be applied to path, false otherwise. // Returns true if the Formatter should be applied to path, false otherwise.
func (f *Formatter) Wants(path string) bool { func (f *Formatter) Wants(file *walk.File) bool {
match := !PathMatches(path, f.excludes) && PathMatches(path, f.includes) match := !PathMatches(file.RelPath, f.excludes) && PathMatches(file.RelPath, f.includes)
if match { if match {
f.log.Debugf("match: %v", path) f.log.Debugf("match: %v", file)
} }
return match return match
} }

View File

@ -3,6 +3,8 @@ package format
import ( import (
"context" "context"
"slices" "slices"
"git.numtide.com/numtide/treefmt/walk"
) )
type Pipeline struct { type Pipeline struct {
@ -17,7 +19,7 @@ func (p *Pipeline) Add(f *Formatter) {
}) })
} }
func (p *Pipeline) Wants(path string) bool { func (p *Pipeline) Wants(path *walk.File) bool {
var match bool var match bool
for _, f := range p.sequence { for _, f := range p.sequence {
match = f.Wants(path) match = f.Wants(path)
@ -28,7 +30,7 @@ func (p *Pipeline) Wants(path string) bool {
return match return match
} }
func (p *Pipeline) Apply(ctx context.Context, paths []string) error { func (p *Pipeline) Apply(ctx context.Context, paths []*walk.File) error {
for _, f := range p.sequence { for _, f := range p.sequence {
if err := f.Apply(ctx, paths, len(p.sequence) > 1); err != nil { if err := f.Apply(ctx, paths, len(p.sequence) > 1); err != nil {
return err return err

View File

@ -2,6 +2,7 @@ package walk
import ( import (
"context" "context"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
) )
@ -15,18 +16,42 @@ func (f filesystemWalker) Root() string {
return f.root return f.root
} }
func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error { func (f filesystemWalker) Walk(_ context.Context, fn WalkFunc) error {
relPathOffset := len(f.root) + 1
relPathFn := func(path string) (relPath string) {
if len(path) >= relPathOffset {
relPath = path[relPathOffset:]
}
return
}
walkFn := func(path string, info fs.FileInfo, err error) error {
file := File{
Path: path,
RelPath: relPathFn(path),
Info: info,
}
return fn(&file, err)
}
if len(f.paths) == 0 { if len(f.paths) == 0 {
return filepath.Walk(f.root, fn) return filepath.Walk(f.root, walkFn)
} }
for _, path := range f.paths { for _, path := range f.paths {
info, err := os.Stat(path) info, err := os.Stat(path)
if err = filepath.Walk(path, fn); err != nil { if err = filepath.Walk(path, walkFn); err != nil {
return err return err
} }
if err = fn(path, info, err); err != nil { file := File{
Path: path,
RelPath: relPathFn(path),
Info: info,
}
if err = fn(&file, err); err != nil {
return err return err
} }
} }

View File

@ -24,7 +24,17 @@ func (g *gitWalker) Root() string {
return g.root return g.root
} }
func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error { func (g *gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
// for quick relative paths
relPathOffset := len(g.root) + 1
relPathFn := func(path string) (relPath string) {
if len(path) >= relPathOffset {
relPath = path[relPathOffset:]
}
return
}
idx, err := g.repo.Storer.Index() idx, err := g.repo.Storer.Index()
if err != nil { if err != nil {
return fmt.Errorf("%w: failed to open index", err) return fmt.Errorf("%w: failed to open index", err)
@ -49,7 +59,13 @@ func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
return nil return nil
} }
return fn(path, info, err) file := File{
Path: path,
RelPath: relPathFn(path),
Info: info,
}
return fn(&file, err)
}) })
if err != nil { if err != nil {
return err return err
@ -66,7 +82,14 @@ func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
// stat the file // stat the file
info, err := os.Lstat(path) info, err := os.Lstat(path)
if err = fn(path, info, err); err != nil {
file := File{
Path: path,
RelPath: relPathFn(path),
Info: info,
}
if err = fn(&file, err); err != nil {
return err return err
} }
} }

View File

@ -3,7 +3,7 @@ package walk
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath" "io/fs"
) )
type Type string type Type string
@ -14,9 +14,21 @@ const (
Filesystem Type = "filesystem" Filesystem Type = "filesystem"
) )
type File struct {
Path string
RelPath string
Info fs.FileInfo
}
func (f File) String() string {
return f.Path
}
type WalkFunc func(file *File, err error) error
type Walker interface { type Walker interface {
Root() string Root() string
Walk(ctx context.Context, fn filepath.WalkFunc) error Walk(ctx context.Context, fn WalkFunc) error
} }
func New(walkerType Type, root string, paths []string) (Walker, error) { func New(walkerType Type, root string, paths []string) (Walker, error) {