Compare commits
4 Commits
f7ad34ca68
...
557b206227
Author | SHA1 | Date | |
---|---|---|---|
557b206227 | |||
2aff340c1b | |||
c7d0138a02 | |||
80e99b6d75 |
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"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 +19,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 {
|
||||||
@ -64,8 +65,46 @@ func (f *Format) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatters := make(map[string]*format.Formatter)
|
||||||
|
|
||||||
|
// detect broken dependencies
|
||||||
|
for name, config := range cfg.Formatters {
|
||||||
|
before := config.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, config := range cfg.Formatters {
|
||||||
|
var ok bool
|
||||||
|
var history []string
|
||||||
|
childName := name
|
||||||
|
for {
|
||||||
|
// add to history
|
||||||
|
history = append(history, childName)
|
||||||
|
|
||||||
|
if config.Before == "" {
|
||||||
|
break
|
||||||
|
} else if config.Before == name {
|
||||||
|
return fmt.Errorf("formatter cycle detected %v", strings.Join(history, " -> "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// load child config
|
||||||
|
childName = config.Before
|
||||||
|
config, ok = cfg.Formatters[config.Before]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("formatter not found: %v", config.Before)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// init formatters
|
// init formatters
|
||||||
for name, formatter := range cfg.Formatters {
|
for name, config := range cfg.Formatters {
|
||||||
if !includeFormatter(name) {
|
if !includeFormatter(name) {
|
||||||
// remove this formatter
|
// remove this formatter
|
||||||
delete(cfg.Formatters, name)
|
delete(cfg.Formatters, name)
|
||||||
@ -73,24 +112,36 @@ func (f *Format) Run() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = formatter.Init(name, globalExcludes)
|
formatter, err := format.NewFormatter(name, config, globalExcludes)
|
||||||
if errors.Is(err, format.ErrFormatterNotFound) && Cli.AllowMissingFormatter {
|
if errors.Is(err, format.ErrFormatterNotFound) && Cli.AllowMissingFormatter {
|
||||||
l.Debugf("formatter not found: %v", name)
|
l.Debugf("formatter not found: %v", name)
|
||||||
// remove this formatter
|
continue
|
||||||
delete(cfg.Formatters, name)
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
|
return fmt.Errorf("%w: failed to initialise formatter: %v", err, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatters[name] = formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = format.RegisterFormatters(ctx, cfg.Formatters)
|
// 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, cfg.Formatters); err != nil {
|
if err = cache.Open(Cli.TreeRoot, Cli.ClearCache, formatters); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
pendingCh := make(chan string, 1024)
|
|
||||||
completedCh := make(chan string, 1024)
|
completedCh := make(chan string, 1024)
|
||||||
|
|
||||||
ctx = format.SetCompletedChannel(ctx, completedCh)
|
ctx = format.SetCompletedChannel(ctx, completedCh)
|
||||||
@ -99,8 +150,8 @@ func (f *Format) Run() error {
|
|||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// start the formatters
|
// start the formatters
|
||||||
for name := range cfg.Formatters {
|
for name := range formatters {
|
||||||
formatter := cfg.Formatters[name]
|
formatter := formatters[name]
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return formatter.Run(ctx)
|
return formatter.Run(ctx)
|
||||||
})
|
})
|
||||||
@ -114,20 +165,13 @@ func (f *Format) Run() error {
|
|||||||
batchSize := 1024
|
batchSize := 1024
|
||||||
batch := make([]string, 0, batchSize)
|
batch := make([]string, 0, batchSize)
|
||||||
|
|
||||||
var pending, completed, changes int
|
var changes int
|
||||||
|
|
||||||
LOOP:
|
LOOP:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case _, ok := <-pendingCh:
|
|
||||||
if ok {
|
|
||||||
pending += 1
|
|
||||||
} else if pending == completed {
|
|
||||||
break LOOP
|
|
||||||
}
|
|
||||||
|
|
||||||
case path, ok := <-completedCh:
|
case path, ok := <-completedCh:
|
||||||
if !ok {
|
if !ok {
|
||||||
break LOOP
|
break LOOP
|
||||||
@ -141,12 +185,6 @@ func (f *Format) Run() error {
|
|||||||
changes += count
|
changes += count
|
||||||
batch = batch[:0]
|
batch = batch[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
completed += 1
|
|
||||||
|
|
||||||
if completed == pending {
|
|
||||||
close(completedCh)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,33 +204,52 @@ func (f *Format) Run() error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
count := 0
|
// pass paths to each formatter
|
||||||
|
|
||||||
for path := range pathsCh {
|
for path := range pathsCh {
|
||||||
for _, formatter := range cfg.Formatters {
|
for _, formatter := range formatters {
|
||||||
if formatter.Wants(path) {
|
if formatter.Wants(path) {
|
||||||
pendingCh <- path
|
|
||||||
count += 1
|
|
||||||
formatter.Put(path)
|
formatter.Put(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, formatter := range cfg.Formatters {
|
// 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()
|
formatter.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
// await completion
|
||||||
close(completedCh)
|
for _, formatter := range formatters {
|
||||||
|
formatter.AwaitCompletion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indicate no more completion events
|
||||||
|
close(completedCh)
|
||||||
|
|
||||||
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
|
||||||
|
@ -25,7 +25,7 @@ func TestAllowMissingFormatter(t *testing.T) {
|
|||||||
configPath := tempDir + "/treefmt.toml"
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, format.Config{
|
test.WriteConfig(t, configPath, format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"foo-fmt": {
|
"foo-fmt": {
|
||||||
Command: "foo-fmt",
|
Command: "foo-fmt",
|
||||||
},
|
},
|
||||||
@ -39,6 +39,27 @@ func TestAllowMissingFormatter(t *testing.T) {
|
|||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDependencyCycle(t *testing.T) {
|
||||||
|
as := require.New(t)
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
|
test.WriteConfig(t, configPath, format.Config{
|
||||||
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
|
"a": {Command: "echo", Before: "b"},
|
||||||
|
"b": {Command: "echo", Before: "c"},
|
||||||
|
"c": {Command: "echo", Before: "a"},
|
||||||
|
"d": {Command: "echo", Before: "e"},
|
||||||
|
"e": {Command: "echo", Before: "f"},
|
||||||
|
"f": {Command: "echo"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
|
as.ErrorContains(err, "formatter cycle detected a -> b -> c")
|
||||||
|
}
|
||||||
|
|
||||||
func TestSpecifyingFormatters(t *testing.T) {
|
func TestSpecifyingFormatters(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
@ -46,7 +67,7 @@ func TestSpecifyingFormatters(t *testing.T) {
|
|||||||
configPath := tempDir + "/treefmt.toml"
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, format.Config{
|
test.WriteConfig(t, configPath, format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"elm": {
|
"elm": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*.elm"},
|
Includes: []string{"*.elm"},
|
||||||
@ -95,7 +116,7 @@ func TestIncludesAndExcludes(t *testing.T) {
|
|||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
config := format.Config{
|
config := format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -167,7 +188,7 @@ func TestCache(t *testing.T) {
|
|||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
config := format.Config{
|
config := format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -202,7 +223,7 @@ func TestChangeWorkingDirectory(t *testing.T) {
|
|||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
config := format.Config{
|
config := format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -227,7 +248,7 @@ func TestFailOnChange(t *testing.T) {
|
|||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
config := format.Config{
|
config := format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -263,7 +284,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
|
|||||||
|
|
||||||
// start with 2 formatters
|
// start with 2 formatters
|
||||||
config := format.Config{
|
config := format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"python": {
|
"python": {
|
||||||
Command: "black",
|
Command: "black",
|
||||||
Includes: []string{"*.py"},
|
Includes: []string{"*.py"},
|
||||||
@ -307,7 +328,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
|
|||||||
as.Contains(string(out), "0 files changed")
|
as.Contains(string(out), "0 files changed")
|
||||||
|
|
||||||
// add go formatter
|
// add go formatter
|
||||||
config.Formatters["go"] = &format.Formatter{
|
config.Formatters["go"] = &format.FormatterConfig{
|
||||||
Command: "gofmt",
|
Command: "gofmt",
|
||||||
Options: []string{"-w"},
|
Options: []string{"-w"},
|
||||||
Includes: []string{"*.go"},
|
Includes: []string{"*.go"},
|
||||||
@ -358,7 +379,7 @@ func TestGitWorktree(t *testing.T) {
|
|||||||
|
|
||||||
// basic config
|
// basic config
|
||||||
config := format.Config{
|
config := format.Config{
|
||||||
Formatters: map[string]*format.Formatter{
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -404,3 +425,157 @@ 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 TestOrderingFormatters(t *testing.T) {
|
||||||
|
as := require.New(t)
|
||||||
|
|
||||||
|
tempDir := test.TempExamples(t)
|
||||||
|
configPath := path.Join(tempDir, "treefmt.toml")
|
||||||
|
|
||||||
|
// missing child
|
||||||
|
test.WriteConfig(t, configPath, format.Config{
|
||||||
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
|
"hs-a": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*.hs"},
|
||||||
|
Before: "hs-b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
|
as.ErrorContains(err, "formatter hs-a is before hs-b but config for hs-b was not found")
|
||||||
|
|
||||||
|
// multiple roots
|
||||||
|
test.WriteConfig(t, configPath, format.Config{
|
||||||
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
|
"hs-a": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*.hs"},
|
||||||
|
Before: "hs-b",
|
||||||
|
},
|
||||||
|
"hs-b": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*.hs"},
|
||||||
|
Before: "hs-c",
|
||||||
|
},
|
||||||
|
"hs-c": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*.hs"},
|
||||||
|
},
|
||||||
|
"py-a": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*.py"},
|
||||||
|
Before: "py-b",
|
||||||
|
},
|
||||||
|
"py-b": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*.py"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
out, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir)
|
||||||
|
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
|
||||||
|
config := format.Config{
|
||||||
|
Formatters: map[string]*format.FormatterConfig{
|
||||||
|
"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.FormatterConfig{
|
||||||
|
"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))
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ type Config struct {
|
|||||||
// Excludes is an optional list of glob patterns used to exclude certain files from all formatters.
|
// Excludes is an optional list of glob patterns used to exclude certain files from all formatters.
|
||||||
Excludes []string
|
Excludes []string
|
||||||
}
|
}
|
||||||
Formatters map[string]*Formatter `toml:"formatter"`
|
Formatters map[string]*FormatterConfig `toml:"formatter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadConfigFile reads from path and unmarshals toml into a Config instance.
|
// ReadConfigFile reads from path and unmarshals toml into a Config instance.
|
||||||
|
@ -5,28 +5,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
formattersKey = "formatters"
|
|
||||||
completedChKey = "completedCh"
|
completedChKey = "completedCh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterFormatters is used to set a map of formatters in the provided context.
|
|
||||||
func RegisterFormatters(ctx context.Context, formatters map[string]*Formatter) context.Context {
|
|
||||||
return context.WithValue(ctx, formattersKey, formatters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFormatters is used to retrieve a formatters map from the provided context.
|
|
||||||
func GetFormatters(ctx context.Context) map[string]*Formatter {
|
|
||||||
return ctx.Value(formattersKey).(map[string]*Formatter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCompletedChannel is used to set a channel for indication processing completion in the provided context.
|
// SetCompletedChannel is used to set a channel for indication processing completion in the provided context.
|
||||||
func SetCompletedChannel(ctx context.Context, completedCh chan string) context.Context {
|
func SetCompletedChannel(ctx context.Context, completedCh chan string) context.Context {
|
||||||
return context.WithValue(ctx, completedChKey, completedCh)
|
return context.WithValue(ctx, completedChKey, completedCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkFormatComplete is used to indicate that all processing has finished for the provided path.
|
// MarkPathComplete is used to indicate that all processing has finished for the provided path.
|
||||||
// This is done by adding the path to the completion channel which should have already been set using
|
// This is done by adding the path to the completion channel which should have already been set using
|
||||||
// SetCompletedChannel.
|
// SetCompletedChannel.
|
||||||
func MarkFormatComplete(ctx context.Context, path string) {
|
func MarkPathComplete(ctx context.Context, path string) {
|
||||||
ctx.Value(completedChKey).(chan string) <- path
|
ctx.Value(completedChKey).(chan string) <- path
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,7 @@ import (
|
|||||||
// ErrFormatterNotFound is returned when the Command for a Formatter is not available.
|
// ErrFormatterNotFound is returned when the Command for a Formatter is not available.
|
||||||
var ErrFormatterNotFound = errors.New("formatter not found")
|
var ErrFormatterNotFound = errors.New("formatter not found")
|
||||||
|
|
||||||
// Formatter represents a command which should be applied to a filesystem.
|
type FormatterConfig struct {
|
||||||
type Formatter struct {
|
|
||||||
// Command is the command invoke when applying this Formatter.
|
// Command is the command invoke when applying this Formatter.
|
||||||
Command string
|
Command string
|
||||||
// Options are an optional list of args to be passed to Command.
|
// Options are an optional list of args to be passed to Command.
|
||||||
@ -24,64 +23,100 @@ type Formatter struct {
|
|||||||
Includes []string
|
Includes []string
|
||||||
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.
|
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.
|
||||||
Excludes []string
|
Excludes []string
|
||||||
|
// Before is the name of another formatter which must process a path after this one
|
||||||
|
Before string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatter represents a command which should be applied to a filesystem.
|
||||||
|
type Formatter struct {
|
||||||
|
name string
|
||||||
|
config *FormatterConfig
|
||||||
|
|
||||||
name string
|
|
||||||
log *log.Logger
|
log *log.Logger
|
||||||
executable string // path to the executable described by Command
|
executable string // path to the executable described by Command
|
||||||
|
|
||||||
|
before string
|
||||||
|
parent *Formatter
|
||||||
|
child *Formatter
|
||||||
|
|
||||||
// internal compiled versions of Includes and Excludes.
|
// internal compiled versions of Includes and Excludes.
|
||||||
includes []glob.Glob
|
includes []glob.Glob
|
||||||
excludes []glob.Glob
|
excludes []glob.Glob
|
||||||
|
|
||||||
// inbox is used to accept new paths for formatting.
|
// inboxCh is used to accept new paths for formatting.
|
||||||
inbox chan string
|
inboxCh chan string
|
||||||
|
// completedCh is used to wait for this formatter to finish all processing.
|
||||||
|
completedCh chan interface{}
|
||||||
|
|
||||||
// Entries from inbox are batched according to batchSize and stored in batch for processing when the batchSize has
|
// Entries from inboxCh are batched according to batchSize and stored in batch for processing when the batchSize has
|
||||||
// been reached or Close is invoked.
|
// been reached or Close is invoked.
|
||||||
batch []string
|
batch []string
|
||||||
batchSize int
|
batchSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) Before() string {
|
||||||
|
return f.before
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) ResetBefore() {
|
||||||
|
f.before = ""
|
||||||
|
}
|
||||||
|
|
||||||
// Executable returns the path to the executable defined by Command
|
// Executable returns the path to the executable defined by Command
|
||||||
func (f *Formatter) Executable() string {
|
func (f *Formatter) Executable() string {
|
||||||
return f.executable
|
return f.executable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init is used to initialise internal state before this Formatter is ready to accept paths.
|
// NewFormatter is used to create a new Formatter.
|
||||||
func (f *Formatter) Init(name string, globalExcludes []glob.Glob) error {
|
func NewFormatter(name string, config *FormatterConfig, globalExcludes []glob.Glob) (*Formatter, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
f := Formatter{}
|
||||||
// capture the name from the config file
|
// capture the name from the config file
|
||||||
f.name = name
|
f.name = name
|
||||||
|
f.config = config
|
||||||
|
f.before = config.Before
|
||||||
|
|
||||||
// test if the formatter is available
|
// test if the formatter is available
|
||||||
executable, err := exec.LookPath(f.Command)
|
executable, err := exec.LookPath(config.Command)
|
||||||
if errors.Is(err, exec.ErrNotFound) {
|
if errors.Is(err, exec.ErrNotFound) {
|
||||||
return ErrFormatterNotFound
|
return nil, ErrFormatterNotFound
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.executable = executable
|
f.executable = executable
|
||||||
|
|
||||||
// initialise internal state
|
// initialise internal state
|
||||||
f.log = log.WithPrefix("format | " + name)
|
f.log = log.WithPrefix("format | " + name)
|
||||||
f.batchSize = 1024
|
f.batchSize = 1024
|
||||||
f.inbox = make(chan string, f.batchSize)
|
f.batch = make([]string, 0, f.batchSize)
|
||||||
f.batch = make([]string, f.batchSize)
|
f.inboxCh = make(chan string, f.batchSize)
|
||||||
f.batch = f.batch[:0]
|
f.completedCh = make(chan interface{}, 1)
|
||||||
|
|
||||||
f.includes, err = CompileGlobs(f.Includes)
|
f.includes, err = CompileGlobs(config.Includes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: formatter '%v' includes", err, f.name)
|
return nil, fmt.Errorf("%w: formatter '%v' includes", err, f.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.excludes, err = CompileGlobs(f.Excludes)
|
f.excludes, err = CompileGlobs(config.Excludes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: formatter '%v' excludes", err, f.name)
|
return nil, fmt.Errorf("%w: formatter '%v' excludes", err, f.name)
|
||||||
}
|
}
|
||||||
f.excludes = append(f.excludes, globalExcludes...)
|
f.excludes = append(f.excludes, globalExcludes...)
|
||||||
|
|
||||||
return nil
|
return &f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) SetParent(formatter *Formatter) {
|
||||||
|
f.parent = formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) Parent() *Formatter {
|
||||||
|
return f.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) SetChild(formatter *Formatter) {
|
||||||
|
f.child = formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wants is used to test if a Formatter wants path based on it's configured Includes and Excludes patterns.
|
// Wants is used to test if a Formatter wants path based on it's configured Includes and Excludes patterns.
|
||||||
@ -94,16 +129,26 @@ func (f *Formatter) Wants(path string) bool {
|
|||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put add path into this Formatter's inbox for processing.
|
// Put add path into this Formatter's inboxCh for processing.
|
||||||
func (f *Formatter) Put(path string) {
|
func (f *Formatter) Put(path string) {
|
||||||
f.inbox <- path
|
f.inboxCh <- path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run is the main processing loop for this Formatter.
|
// Run is the main processing loop for this Formatter.
|
||||||
// It accepts a context which is used to lookup certain dependencies and for cancellation.
|
// It accepts a context which is used to lookup certain dependencies and for cancellation.
|
||||||
func (f *Formatter) Run(ctx context.Context) (err error) {
|
func (f *Formatter) Run(ctx context.Context) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if f.child != nil {
|
||||||
|
// indicate no further processing for the child formatter
|
||||||
|
f.child.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// indicate this formatter has finished processing
|
||||||
|
f.completedCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
LOOP:
|
LOOP:
|
||||||
// keep processing until ctx has been cancelled or inbox has been closed
|
// keep processing until ctx has been cancelled or inboxCh has been closed
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
||||||
@ -112,8 +157,8 @@ LOOP:
|
|||||||
err = ctx.Err()
|
err = ctx.Err()
|
||||||
break LOOP
|
break LOOP
|
||||||
|
|
||||||
case path, ok := <-f.inbox:
|
case path, ok := <-f.inboxCh:
|
||||||
// check if the inbox has been closed
|
// check if the inboxCh has been closed
|
||||||
if !ok {
|
if !ok {
|
||||||
break LOOP
|
break LOOP
|
||||||
}
|
}
|
||||||
@ -148,7 +193,7 @@ func (f *Formatter) apply(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// construct args, starting with config
|
// construct args, starting with config
|
||||||
args := f.Options
|
args := f.config.Options
|
||||||
|
|
||||||
// append each file path
|
// append each file path
|
||||||
for _, path := range f.batch {
|
for _, path := range f.batch {
|
||||||
@ -157,7 +202,7 @@ func (f *Formatter) apply(ctx context.Context) error {
|
|||||||
|
|
||||||
// execute
|
// execute
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
cmd := exec.CommandContext(ctx, f.Command, args...)
|
cmd := exec.CommandContext(ctx, f.config.Command, args...)
|
||||||
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
f.log.Debugf("\n%v", string(out))
|
f.log.Debugf("\n%v", string(out))
|
||||||
@ -167,9 +212,16 @@ func (f *Formatter) apply(ctx context.Context) error {
|
|||||||
|
|
||||||
f.log.Infof("%v files processed in %v", len(f.batch), time.Now().Sub(start))
|
f.log.Infof("%v files processed in %v", len(f.batch), time.Now().Sub(start))
|
||||||
|
|
||||||
// mark each path in this batch as completed
|
if f.child == nil {
|
||||||
for _, path := range f.batch {
|
// mark each path in this batch as completed
|
||||||
MarkFormatComplete(ctx, path)
|
for _, path := range f.batch {
|
||||||
|
MarkPathComplete(ctx, path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise forward each path onto the next formatter for processing
|
||||||
|
for _, path := range f.batch {
|
||||||
|
f.child.Put(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset batch
|
// reset batch
|
||||||
@ -180,5 +232,10 @@ func (f *Formatter) apply(ctx context.Context) error {
|
|||||||
|
|
||||||
// Close is used to indicate that a Formatter should process any remaining paths and then stop it's processing loop.
|
// Close is used to indicate that a Formatter should process any remaining paths and then stop it's processing loop.
|
||||||
func (f *Formatter) Close() {
|
func (f *Formatter) Close() {
|
||||||
close(f.inbox)
|
close(f.inboxCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) AwaitCompletion() {
|
||||||
|
// todo support a timeout
|
||||||
|
<-f.completedCh
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,18 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type filesystem struct {
|
type filesystemWalker struct {
|
||||||
root string
|
root string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f filesystem) Root() string {
|
func (f filesystemWalker) Root() string {
|
||||||
return f.root
|
return f.root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f filesystem) Walk(_ context.Context, fn filepath.WalkFunc) error {
|
func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error {
|
||||||
return filepath.Walk(f.root, fn)
|
return filepath.Walk(f.root, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFilesystem(root string) (Walker, error) {
|
func NewFilesystem(root string) (Walker, error) {
|
||||||
return filesystem{root}, nil
|
return filesystemWalker{root}, nil
|
||||||
}
|
}
|
||||||
|
@ -1,69 +1,53 @@
|
|||||||
package walk
|
package walk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"github.com/go-git/go-git/v5"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type git struct {
|
type gitWalker struct {
|
||||||
root string
|
root string
|
||||||
|
repo *git.Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *git) Root() string {
|
func (g *gitWalker) Root() string {
|
||||||
return g.root
|
return g.root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *git) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
r, w := io.Pipe()
|
|
||||||
|
|
||||||
cmd := exec.Command("git", "-C", g.root, "ls-files")
|
idx, err := g.repo.Storer.Index()
|
||||||
cmd.Stdout = w
|
if err != nil {
|
||||||
cmd.Stderr = w
|
return fmt.Errorf("%w: failed to open index", err)
|
||||||
|
}
|
||||||
|
|
||||||
eg := errgroup.Group{}
|
for _, entry := range idx.Entries {
|
||||||
|
|
||||||
eg.Go(func() error {
|
select {
|
||||||
scanner := bufio.NewScanner(r)
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
path := filepath.Join(g.root, entry.Name)
|
||||||
|
|
||||||
for scanner.Scan() {
|
// stat the file
|
||||||
select {
|
info, err := os.Lstat(path)
|
||||||
case <-ctx.Done():
|
if err = fn(path, info, err); err != nil {
|
||||||
return ctx.Err()
|
return err
|
||||||
default:
|
|
||||||
line := scanner.Text()
|
|
||||||
path := filepath.Join(g.root, line)
|
|
||||||
|
|
||||||
// stat the file
|
|
||||||
info, err := os.Lstat(path)
|
|
||||||
if err = fn(path, info, err); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := w.CloseWithError(cmd.Run()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return eg.Wait()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGit(root string) (Walker, error) {
|
func NewGit(root string) (Walker, error) {
|
||||||
// check if we're dealing with a git repository
|
repo, err := git.PlainOpen(root)
|
||||||
cmd := exec.Command("git", "-C", root, "rev-parse", "--is-inside-work-tree")
|
|
||||||
_, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: git repo check failed", err)
|
return nil, fmt.Errorf("%w: failed to open git repo", err)
|
||||||
}
|
}
|
||||||
return &git{root}, nil
|
return &gitWalker{root, repo}, nil
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
@ -26,14 +26,9 @@
|
|||||||
"-X 'build.Version=${version}'"
|
"-X 'build.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
# needed for git ls-files
|
|
||||||
buildInputs = [pkgs.git];
|
|
||||||
|
|
||||||
nativeBuildInputs =
|
nativeBuildInputs =
|
||||||
# needed for git ls-files
|
|
||||||
[pkgs.git]
|
|
||||||
# we need some formatters available for the tests
|
# we need some formatters available for the tests
|
||||||
++ (import ./formatters.nix pkgs);
|
(import ./formatters.nix pkgs);
|
||||||
|
|
||||||
preCheck = ''
|
preCheck = ''
|
||||||
XDG_CACHE_HOME=$(mktemp -d)
|
XDG_CACHE_HOME=$(mktemp -d)
|
||||||
|
Reference in New Issue
Block a user