support explicit paths and processing paths from stdin #21
9
internal/cache/cache.go
vendored
9
internal/cache/cache.go
vendored
@ -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.
|
// 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, 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 tx *bolt.Tx
|
||||||
var bucket *bolt.Bucket
|
var bucket *bolt.Bucket
|
||||||
var processed int
|
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)
|
return walker.Walk(ctx, func(path string, info fs.FileInfo, err error) error {
|
||||||
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 {
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
@ -6,12 +6,12 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Cli = Options{}
|
var Cli = Format{}
|
||||||
|
|
||||||
type Options struct {
|
type Format struct {
|
||||||
AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing."`
|
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."`
|
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."`
|
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"`
|
ConfigFile string `type:"existingfile" default:"./treefmt.toml"`
|
||||||
FailOnChange bool `help:"Exit with error if any changes were made. Useful for CI."`
|
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."`
|
Formatters []string `help:"Specify formatters to apply. Defaults to all formatters."`
|
||||||
@ -19,17 +19,18 @@ type Options struct {
|
|||||||
Walk walk.Type `enum:"auto,git,filesystem" default:"auto" help:"The method used to traverse the files within --tree-root. Currently supports 'auto', 'git' or 'filesystem'."`
|
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."`
|
Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv."`
|
||||||
|
|
||||||
Format Format `cmd:"" default:"."`
|
Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree."`
|
||||||
|
Stdin bool `help:"Format the context passed in via stdin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Options) Configure() {
|
func (f *Format) Configure() {
|
||||||
log.SetReportTimestamp(false)
|
log.SetReportTimestamp(false)
|
||||||
|
|
||||||
if c.Verbosity == 0 {
|
if f.Verbosity == 0 {
|
||||||
log.SetLevel(log.WarnLevel)
|
log.SetLevel(log.WarnLevel)
|
||||||
} else if c.Verbosity == 1 {
|
} else if f.Verbosity == 1 {
|
||||||
log.SetLevel(log.InfoLevel)
|
log.SetLevel(log.InfoLevel)
|
||||||
} else if c.Verbosity >= 2 {
|
} else if f.Verbosity >= 2 {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.numtide.com/numtide/treefmt/internal/walk"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/cache"
|
"git.numtide.com/numtide/treefmt/internal/cache"
|
||||||
"git.numtide.com/numtide/treefmt/internal/format"
|
"git.numtide.com/numtide/treefmt/internal/format"
|
||||||
|
|
||||||
@ -16,8 +18,6 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Format struct{}
|
|
||||||
|
|
||||||
var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
|
var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled")
|
||||||
|
|
||||||
func (f *Format) Run() error {
|
func (f *Format) Run() error {
|
||||||
@ -189,10 +189,23 @@ func (f *Format) Run() error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() (err error) {
|
||||||
err := cache.ChangeSet(ctx, Cli.TreeRoot, Cli.Walk, pathsCh)
|
var walker walk.Walker
|
||||||
close(pathsCh)
|
|
||||||
return err
|
if Cli.Stdin {
|
||||||
|
walker, err = walk.NewPathReader(os.Stdin)
|
||||||
|
} else if len(Cli.Paths) > 0 {
|
||||||
|
walker, err = walk.NewPathList(Cli.Paths)
|
||||||
|
} else {
|
||||||
|
walker, err = walk.New(Cli.Walk, Cli.TreeRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to create walker", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer close(pathsCh)
|
||||||
|
return cache.ChangeSet(ctx, walker, pathsCh)
|
||||||
})
|
})
|
||||||
|
|
||||||
// listen for shutdown and call cancel if required
|
// listen for shutdown and call cancel if required
|
||||||
|
@ -404,3 +404,103 @@ func TestGitWorktree(t *testing.T) {
|
|||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
as.Contains(string(out), fmt.Sprintf("%d files changed", 55))
|
as.Contains(string(out), fmt.Sprintf("%d files changed", 55))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
config := format.Config{
|
||||||
|
Formatters: map[string]*format.Formatter{
|
||||||
|
"echo": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.WriteConfig(t, configPath, config)
|
||||||
|
|
||||||
|
// without any path args
|
||||||
|
out, err := cmd(t, "-C", tempDir)
|
||||||
|
as.NoError(err)
|
||||||
|
as.Contains(string(out), fmt.Sprintf("%d files changed", 29))
|
||||||
|
|
||||||
|
// specify some explicit paths
|
||||||
|
out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Foo.hs")
|
||||||
|
as.NoError(err)
|
||||||
|
as.Contains(string(out), fmt.Sprintf("%d files changed", 2))
|
||||||
|
|
||||||
|
// specify a bad path
|
||||||
|
out, err = cmd(t, "-C", tempDir, "-c", "elm/elm.json", "haskell/Nested/Bar.hs")
|
||||||
|
as.ErrorContains(err, "no such file or directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdIn(t *testing.T) {
|
||||||
|
as := require.New(t)
|
||||||
|
|
||||||
|
// capture current cwd, so we can replace it after the test is finished
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
as.NoError(err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// return to the previous working directory
|
||||||
|
as.NoError(os.Chdir(cwd))
|
||||||
|
})
|
||||||
|
|
||||||
|
tempDir := test.TempExamples(t)
|
||||||
|
configPath := filepath.Join(tempDir, "/treefmt.toml")
|
||||||
|
|
||||||
|
// change working directory to temp root
|
||||||
|
as.NoError(os.Chdir(tempDir))
|
||||||
|
|
||||||
|
// basic config
|
||||||
|
config := format.Config{
|
||||||
|
Formatters: map[string]*format.Formatter{
|
||||||
|
"echo": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.WriteConfig(t, configPath, config)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
56
internal/walk/paths.go
Normal file
56
internal/walk/paths.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package walk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paths struct {
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *paths) Root() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *paths) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
|
scanner := bufio.NewScanner(p.reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
path := scanner.Text()
|
||||||
|
// stat the file
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err = fn(path, info, err); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPathReader(reader io.Reader) (Walker, error) {
|
||||||
|
return &paths{reader}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPathList(paths []string) (Walker, error) {
|
||||||
|
r, w := io.Pipe()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for _, path := range paths {
|
||||||
|
if _, err := w.Write([]byte(path)); err != nil {
|
||||||
|
_ = r.CloseWithError(err)
|
||||||
|
} else if _, err = w.Write([]byte("\n")); err != nil {
|
||||||
|
_ = r.CloseWithError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = r.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return NewPathReader(r)
|
||||||
|
}
|
Reference in New Issue
Block a user