Compare commits
4 Commits
5e4d61d6d6
...
72bd5960a7
Author | SHA1 | Date | |
---|---|---|---|
72bd5960a7 | |||
a8488986a7 | |||
b68ba86996 | |||
0fbae06f19 |
6
build/build.go
Normal file
6
build/build.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
var (
|
||||||
|
Name = "treefmt"
|
||||||
|
Version = "v0.0.1+dev"
|
||||||
|
)
|
13
internal/cache/cache.go → cache/cache.go
vendored
13
internal/cache/cache.go → cache/cache.go
vendored
@ -9,9 +9,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/walk"
|
"git.numtide.com/numtide/treefmt/format"
|
||||||
|
"git.numtide.com/numtide/treefmt/walk"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/format"
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
@ -173,7 +173,7 @@ func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error {
|
|||||||
|
|
||||||
// ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh.
|
// 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()
|
@ -1,35 +1,36 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.numtide.com/numtide/treefmt/internal/walk"
|
"git.numtide.com/numtide/treefmt/walk"
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"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."`
|
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"`
|
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."`
|
||||||
TreeRoot string `type:"existingdir" default:"."`
|
TreeRoot string `type:"existingdir" default:"."`
|
||||||
Walk walk.Type `enum:"auto,git,filesystem" default:"auto" help:"The method used to traverse the files within --tree-root. Currently supports 'auto', 'git' or 'filesystem'."`
|
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."`
|
||||||
|
Version bool `name:"version" short:"V" help:"Print version"`
|
||||||
|
|
||||||
Format Format `cmd:"" default:"."`
|
Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree."`
|
||||||
|
Stdin bool `help:"Format the context passed in via stdin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Options) Configure() {
|
func (f *Format) Configure() {
|
||||||
log.SetReportTimestamp(false)
|
log.SetReportTimestamp(false)
|
||||||
|
|
||||||
if c.Verbosity == 0 {
|
if f.Verbosity == 0 {
|
||||||
log.SetLevel(log.WarnLevel)
|
|
||||||
} else if c.Verbosity == 1 {
|
|
||||||
log.SetLevel(log.InfoLevel)
|
log.SetLevel(log.InfoLevel)
|
||||||
} else if c.Verbosity >= 2 {
|
} else if f.Verbosity > 0 {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,26 +1,26 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/config"
|
"git.numtide.com/numtide/treefmt/cache"
|
||||||
|
"git.numtide.com/numtide/treefmt/config"
|
||||||
"git.numtide.com/numtide/treefmt/internal/cache"
|
format2 "git.numtide.com/numtide/treefmt/format"
|
||||||
"git.numtide.com/numtide/treefmt/internal/format"
|
"git.numtide.com/numtide/treefmt/walk"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"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 {
|
||||||
@ -46,7 +46,7 @@ func (f *Format) Run() error {
|
|||||||
return fmt.Errorf("%w: failed to read config file", err)
|
return fmt.Errorf("%w: failed to read config file", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
globalExcludes, err := format.CompileGlobs(cfg.Global.Excludes)
|
globalExcludes, err := format2.CompileGlobs(cfg.Global.Excludes)
|
||||||
|
|
||||||
// create optional formatter filter set
|
// create optional formatter filter set
|
||||||
formatterSet := make(map[string]bool)
|
formatterSet := make(map[string]bool)
|
||||||
@ -67,7 +67,7 @@ func (f *Format) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formatters := make(map[string]*format.Formatter)
|
formatters := make(map[string]*format2.Formatter)
|
||||||
|
|
||||||
// detect broken dependencies
|
// detect broken dependencies
|
||||||
for name, formatterCfg := range cfg.Formatters {
|
for name, formatterCfg := range cfg.Formatters {
|
||||||
@ -114,8 +114,8 @@ func (f *Format) Run() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
formatter, err := format.NewFormatter(name, formatterCfg, globalExcludes)
|
formatter, err := format2.NewFormatter(name, formatterCfg, globalExcludes)
|
||||||
if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter {
|
if errors.Is(err, format2.ErrCommandNotFound) && Cli.AllowMissingFormatter {
|
||||||
l.Debugf("formatter not found: %v", name)
|
l.Debugf("formatter not found: %v", name)
|
||||||
continue
|
continue
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -146,7 +146,7 @@ func (f *Format) Run() error {
|
|||||||
//
|
//
|
||||||
completedCh := make(chan string, 1024)
|
completedCh := make(chan string, 1024)
|
||||||
|
|
||||||
ctx = format.SetCompletedChannel(ctx, completedCh)
|
ctx = format2.SetCompletedChannel(ctx, completedCh)
|
||||||
|
|
||||||
//
|
//
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
@ -169,6 +169,20 @@ func (f *Format) Run() error {
|
|||||||
|
|
||||||
var changes int
|
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:
|
LOOP:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -180,28 +194,23 @@ func (f *Format) Run() error {
|
|||||||
}
|
}
|
||||||
batch = append(batch, path)
|
batch = append(batch, path)
|
||||||
if len(batch) == batchSize {
|
if len(batch) == batchSize {
|
||||||
count, err := cache.Update(batch)
|
if err = processBatch(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
changes += count
|
|
||||||
batch = batch[:0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// final flush
|
// final flush
|
||||||
count, err := cache.Update(batch)
|
if err = processBatch(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
changes += count
|
|
||||||
|
|
||||||
if Cli.FailOnChange && changes != 0 {
|
if Cli.FailOnChange && changes != 0 {
|
||||||
return ErrFailOnChange
|
return ErrFailOnChange
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%v files changed in %v", changes, time.Now().Sub(start))
|
fmt.Printf("%v files changed in %v\n", changes, time.Now().Sub(start))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -235,10 +244,40 @@ 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)
|
paths := Cli.Paths
|
||||||
close(pathsCh)
|
|
||||||
return err
|
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
|
// listen for shutdown and call cancel if required
|
@ -8,15 +8,15 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/config"
|
config2 "git.numtide.com/numtide/treefmt/config"
|
||||||
|
"git.numtide.com/numtide/treefmt/format"
|
||||||
|
"git.numtide.com/numtide/treefmt/test"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/test"
|
|
||||||
"github.com/go-git/go-billy/v5/osfs"
|
"github.com/go-git/go-billy/v5/osfs"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing/cache"
|
"github.com/go-git/go-git/v5/plumbing/cache"
|
||||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/format"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,8 +26,8 @@ func TestAllowMissingFormatter(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
configPath := tempDir + "/treefmt.toml"
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, config.Config{
|
test.WriteConfig(t, configPath, config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"foo-fmt": {
|
"foo-fmt": {
|
||||||
Command: "foo-fmt",
|
Command: "foo-fmt",
|
||||||
},
|
},
|
||||||
@ -47,8 +47,8 @@ func TestDependencyCycle(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
configPath := tempDir + "/treefmt.toml"
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, config.Config{
|
test.WriteConfig(t, configPath, config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"a": {Command: "echo", Before: "b"},
|
"a": {Command: "echo", Before: "b"},
|
||||||
"b": {Command: "echo", Before: "c"},
|
"b": {Command: "echo", Before: "c"},
|
||||||
"c": {Command: "echo", Before: "a"},
|
"c": {Command: "echo", Before: "a"},
|
||||||
@ -68,8 +68,8 @@ func TestSpecifyingFormatters(t *testing.T) {
|
|||||||
tempDir := test.TempExamples(t)
|
tempDir := test.TempExamples(t)
|
||||||
configPath := tempDir + "/treefmt.toml"
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
test.WriteConfig(t, configPath, config.Config{
|
test.WriteConfig(t, configPath, config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"elm": {
|
"elm": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*.elm"},
|
Includes: []string{"*.elm"},
|
||||||
@ -117,8 +117,8 @@ func TestIncludesAndExcludes(t *testing.T) {
|
|||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/echo.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -189,8 +189,8 @@ func TestCache(t *testing.T) {
|
|||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/echo.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -206,6 +206,20 @@ func TestCache(t *testing.T) {
|
|||||||
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)
|
||||||
as.Contains(string(out), "0 files changed")
|
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)
|
||||||
|
as.Contains(string(out), fmt.Sprintf("%d files changed", 29))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChangeWorkingDirectory(t *testing.T) {
|
func TestChangeWorkingDirectory(t *testing.T) {
|
||||||
@ -224,8 +238,8 @@ func TestChangeWorkingDirectory(t *testing.T) {
|
|||||||
configPath := tempDir + "/treefmt.toml"
|
configPath := tempDir + "/treefmt.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -249,8 +263,8 @@ func TestFailOnChange(t *testing.T) {
|
|||||||
configPath := tempDir + "/echo.toml"
|
configPath := tempDir + "/echo.toml"
|
||||||
|
|
||||||
// test without any excludes
|
// test without any excludes
|
||||||
cfg := config.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -285,8 +299,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
|
|||||||
as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
|
as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
|
||||||
|
|
||||||
// start with 2 formatters
|
// start with 2 formatters
|
||||||
cfg := config.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"python": {
|
"python": {
|
||||||
Command: "black",
|
Command: "black",
|
||||||
Includes: []string{"*.py"},
|
Includes: []string{"*.py"},
|
||||||
@ -330,7 +344,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
|
||||||
cfg.Formatters["go"] = &config.Formatter{
|
cfg.Formatters["go"] = &config2.Formatter{
|
||||||
Command: "gofmt",
|
Command: "gofmt",
|
||||||
Options: []string{"-w"},
|
Options: []string{"-w"},
|
||||||
Includes: []string{"*.go"},
|
Includes: []string{"*.go"},
|
||||||
@ -380,8 +394,8 @@ func TestGitWorktree(t *testing.T) {
|
|||||||
configPath := filepath.Join(tempDir, "/treefmt.toml")
|
configPath := filepath.Join(tempDir, "/treefmt.toml")
|
||||||
|
|
||||||
// basic config
|
// basic config
|
||||||
cfg := config.Config{
|
cfg := config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"echo": {
|
"echo": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*"},
|
Includes: []string{"*"},
|
||||||
@ -435,8 +449,8 @@ func TestOrderingFormatters(t *testing.T) {
|
|||||||
configPath := path.Join(tempDir, "treefmt.toml")
|
configPath := path.Join(tempDir, "treefmt.toml")
|
||||||
|
|
||||||
// missing child
|
// missing child
|
||||||
test.WriteConfig(t, configPath, config.Config{
|
test.WriteConfig(t, configPath, config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"hs-a": {
|
"hs-a": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*.hs"},
|
Includes: []string{"*.hs"},
|
||||||
@ -449,8 +463,8 @@ func TestOrderingFormatters(t *testing.T) {
|
|||||||
as.ErrorContains(err, "formatter hs-a is before hs-b but config for hs-b was not found")
|
as.ErrorContains(err, "formatter hs-a is before hs-b but config for hs-b was not found")
|
||||||
|
|
||||||
// multiple roots
|
// multiple roots
|
||||||
test.WriteConfig(t, configPath, config.Config{
|
test.WriteConfig(t, configPath, config2.Config{
|
||||||
Formatters: map[string]*config.Formatter{
|
Formatters: map[string]*config2.Formatter{
|
||||||
"hs-a": {
|
"hs-a": {
|
||||||
Command: "echo",
|
Command: "echo",
|
||||||
Includes: []string{"*.hs"},
|
Includes: []string{"*.hs"},
|
||||||
@ -481,3 +495,103 @@ func TestOrderingFormatters(t *testing.T) {
|
|||||||
as.NoError(err)
|
as.NoError(err)
|
||||||
as.Contains(string(out), "8 files changed")
|
as.Contains(string(out), "8 files changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPathsArg(t *testing.T) {
|
||||||
|
as := require.New(t)
|
||||||
|
|
||||||
|
// capture current cwd, so we can replace it after the test is finished
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
as.NoError(err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// return to the previous working directory
|
||||||
|
as.NoError(os.Chdir(cwd))
|
||||||
|
})
|
||||||
|
|
||||||
|
tempDir := test.TempExamples(t)
|
||||||
|
configPath := filepath.Join(tempDir, "/treefmt.toml")
|
||||||
|
|
||||||
|
// change working directory to temp root
|
||||||
|
as.NoError(os.Chdir(tempDir))
|
||||||
|
|
||||||
|
// basic config
|
||||||
|
cfg := config2.Config{
|
||||||
|
Formatters: map[string]*config2.Formatter{
|
||||||
|
"echo": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.WriteConfig(t, configPath, cfg)
|
||||||
|
|
||||||
|
// without any path args
|
||||||
|
out, err := cmd(t, "-C", tempDir)
|
||||||
|
as.NoError(err)
|
||||||
|
as.Contains(string(out), fmt.Sprintf("%d files changed", 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
|
||||||
|
cfg := config2.Config{
|
||||||
|
Formatters: map[string]*config2.Formatter{
|
||||||
|
"echo": {
|
||||||
|
Command: "echo",
|
||||||
|
Includes: []string{"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.WriteConfig(t, configPath, cfg)
|
||||||
|
|
||||||
|
// swap out stdin
|
||||||
|
prevStdIn := os.Stdin
|
||||||
|
stdin, err := os.CreateTemp("", "stdin")
|
||||||
|
as.NoError(err)
|
||||||
|
|
||||||
|
os.Stdin = stdin
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Stdin = prevStdIn
|
||||||
|
_ = os.Remove(stdin.Name())
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := stdin.WriteString(`treefmt.toml
|
||||||
|
elm/elm.json
|
||||||
|
go/main.go
|
||||||
|
`)
|
||||||
|
as.NoError(err, "failed to write to stdin")
|
||||||
|
as.NoError(stdin.Sync())
|
||||||
|
_, _ = stdin.Seek(0, 0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := cmd(t, "-C", tempDir, "--stdin")
|
||||||
|
as.NoError(err)
|
||||||
|
as.Contains(string(out), fmt.Sprintf("%d files changed", 3))
|
||||||
|
}
|
@ -7,7 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/test"
|
"git.numtide.com/numtide/treefmt/test"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
@ -9,7 +9,7 @@ import (
|
|||||||
func TestReadConfigFile(t *testing.T) {
|
func TestReadConfigFile(t *testing.T) {
|
||||||
as := require.New(t)
|
as := require.New(t)
|
||||||
|
|
||||||
cfg, err := ReadFile("../../test/treefmt.toml")
|
cfg, err := ReadFile("../test/treefmt.toml")
|
||||||
as.NoError(err, "failed to read config file")
|
as.NoError(err, "failed to read config file")
|
||||||
|
|
||||||
as.NotNil(cfg)
|
as.NotNil(cfg)
|
16
flake.lock
16
flake.lock
@ -111,6 +111,21 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-filter": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1705332318,
|
||||||
|
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "nix-filter",
|
||||||
|
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "nix-filter",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707689078,
|
"lastModified": 1707689078,
|
||||||
@ -151,6 +166,7 @@
|
|||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"flake-root": "flake-root",
|
"flake-root": "flake-root",
|
||||||
"gomod2nix": "gomod2nix",
|
"gomod2nix": "gomod2nix",
|
||||||
|
"nix-filter": "nix-filter",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"treefmt-nix": "treefmt-nix"
|
"treefmt-nix": "treefmt-nix"
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
url = "github:nix-community/gomod2nix";
|
url = "github:nix-community/gomod2nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
nix-filter.url = "github:numtide/nix-filter";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs @ {flake-parts, ...}:
|
outputs = inputs @ {flake-parts, ...}:
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/config"
|
"git.numtide.com/numtide/treefmt/config"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
@ -1,22 +0,0 @@
|
|||||||
package walk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
type filesystemWalker struct {
|
|
||||||
root string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f filesystemWalker) Root() string {
|
|
||||||
return f.root
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error {
|
|
||||||
return filepath.Walk(f.root, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFilesystem(root string) (Walker, error) {
|
|
||||||
return filesystemWalker{root}, nil
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package walk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
type gitWalker struct {
|
|
||||||
root string
|
|
||||||
repo *git.Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gitWalker) Root() string {
|
|
||||||
return g.root
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
|
||||||
idx, err := g.repo.Storer.Index()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to open index", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range idx.Entries {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
path := filepath.Join(g.root, entry.Name)
|
|
||||||
|
|
||||||
// stat the file
|
|
||||||
info, err := os.Lstat(path)
|
|
||||||
if err = fn(path, info, err); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGit(root string) (Walker, error) {
|
|
||||||
repo, err := git.PlainOpen(root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: failed to open git repo", err)
|
|
||||||
}
|
|
||||||
return &gitWalker{root, repo}, nil
|
|
||||||
}
|
|
17
main.go
17
main.go
@ -1,11 +1,26 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.numtide.com/numtide/treefmt/internal/cli"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.numtide.com/numtide/treefmt/build"
|
||||||
|
"git.numtide.com/numtide/treefmt/cli"
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// This is to maintain compatibility with 1.0.0 which allows specifying the version with a `treefmt --version` flag
|
||||||
|
// on the 'default' command. With Kong it would be better to have `treefmt version` so it would be treated as a
|
||||||
|
// separate command. As it is, we would need to weaken some of the `existingdir` and `existingfile` checks kong is
|
||||||
|
// doing for us in the default format command.
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if arg == "--version" || arg == "-V" {
|
||||||
|
fmt.Printf("%s %s\n", build.Name, build.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx := kong.Parse(&cli.Cli)
|
ctx := kong.Parse(&cli.Cli)
|
||||||
ctx.FatalIfErrorf(ctx.Run())
|
ctx.FatalIfErrorf(ctx.Run())
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,19 @@
|
|||||||
packages = rec {
|
packages = rec {
|
||||||
treefmt = inputs'.gomod2nix.legacyPackages.buildGoApplication rec {
|
treefmt = inputs'.gomod2nix.legacyPackages.buildGoApplication rec {
|
||||||
pname = "treefmt";
|
pname = "treefmt";
|
||||||
version = "0.0.1+dev";
|
version = "2.0.0+dev";
|
||||||
|
|
||||||
# ensure we are using the same version of go to build with
|
# ensure we are using the same version of go to build with
|
||||||
inherit (pkgs) go;
|
inherit (pkgs) go;
|
||||||
|
|
||||||
src = ../.;
|
src = let
|
||||||
|
filter = inputs.nix-filter.lib;
|
||||||
|
in
|
||||||
|
filter {
|
||||||
|
root = ../.;
|
||||||
|
exclude = [./nix];
|
||||||
|
};
|
||||||
|
|
||||||
modules = ../gomod2nix.toml;
|
modules = ../gomod2nix.toml;
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
|
@ -2,11 +2,18 @@
|
|||||||
imports = [
|
imports = [
|
||||||
inputs.treefmt-nix.flakeModule
|
inputs.treefmt-nix.flakeModule
|
||||||
];
|
];
|
||||||
perSystem = {config, ...}: {
|
perSystem = {
|
||||||
|
config,
|
||||||
|
self',
|
||||||
|
...
|
||||||
|
}: {
|
||||||
treefmt.config = {
|
treefmt.config = {
|
||||||
inherit (config.flake-root) projectRootFile;
|
inherit (config.flake-root) projectRootFile;
|
||||||
flakeCheck = true;
|
flakeCheck = true;
|
||||||
flakeFormatter = true;
|
flakeFormatter = true;
|
||||||
|
|
||||||
|
package = self'.packages.default;
|
||||||
|
|
||||||
programs = {
|
programs = {
|
||||||
alejandra.enable = true;
|
alejandra.enable = true;
|
||||||
deadnix.enable = true;
|
deadnix.enable = true;
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/config"
|
"git.numtide.com/numtide/treefmt/config"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
cp "github.com/otiai10/copy"
|
cp "github.com/otiai10/copy"
|
||||||
@ -25,7 +25,7 @@ func WriteConfig(t *testing.T, path string, cfg config.Config) {
|
|||||||
|
|
||||||
func TempExamples(t *testing.T) string {
|
func TempExamples(t *testing.T) string {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
require.NoError(t, cp.Copy("../../test/examples", tempDir), "failed to copy test data to temp dir")
|
require.NoError(t, cp.Copy("../test/examples", tempDir), "failed to copy test data to temp dir")
|
||||||
return tempDir
|
return tempDir
|
||||||
}
|
}
|
||||||
|
|
39
walk/filesystem.go
Normal file
39
walk/filesystem.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package walk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filesystemWalker struct {
|
||||||
|
root string
|
||||||
|
paths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filesystemWalker) Root() string {
|
||||||
|
return f.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filesystemWalker) Walk(_ context.Context, fn filepath.WalkFunc) error {
|
||||||
|
if len(f.paths) == 0 {
|
||||||
|
return filepath.Walk(f.root, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range f.paths {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err = filepath.Walk(path, fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fn(path, info, err); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilesystem(root string, paths []string) (Walker, error) {
|
||||||
|
return filesystemWalker{root, paths}, nil
|
||||||
|
}
|
85
walk/git.go
Normal file
85
walk/git.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package walk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gitWalker struct {
|
||||||
|
root string
|
||||||
|
paths []string
|
||||||
|
repo *git.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gitWalker) Root() string {
|
||||||
|
return g.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gitWalker) Walk(ctx context.Context, fn filepath.WalkFunc) error {
|
||||||
|
idx, err := g.repo.Storer.Index()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: failed to open index", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.paths) > 0 {
|
||||||
|
for _, path := range g.paths {
|
||||||
|
|
||||||
|
err = filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(g.root, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = idx.Entry(relPath); errors.Is(err, index.ErrEntryNotFound) {
|
||||||
|
// we skip this path as it's not staged
|
||||||
|
log.Debugf("Path not found in git index, skipping: %v, %v", relPath, path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(path, info, err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, entry := range idx.Entries {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
path := filepath.Join(g.root, entry.Name)
|
||||||
|
|
||||||
|
// stat the file
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err = fn(path, info, err); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGit(root string, paths []string) (Walker, error) {
|
||||||
|
repo, err := git.PlainOpen(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: failed to open git repo", err)
|
||||||
|
}
|
||||||
|
return &gitWalker{root, paths, repo}, nil
|
||||||
|
}
|
@ -19,24 +19,24 @@ type Walker interface {
|
|||||||
Walk(ctx context.Context, fn filepath.WalkFunc) error
|
Walk(ctx context.Context, fn filepath.WalkFunc) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(walkerType Type, root string) (Walker, error) {
|
func New(walkerType Type, root string, paths []string) (Walker, error) {
|
||||||
switch walkerType {
|
switch walkerType {
|
||||||
case Git:
|
case Git:
|
||||||
return NewGit(root)
|
return NewGit(root, paths)
|
||||||
case Auto:
|
case Auto:
|
||||||
return Detect(root)
|
return Detect(root, paths)
|
||||||
case Filesystem:
|
case Filesystem:
|
||||||
return NewFilesystem(root)
|
return NewFilesystem(root, paths)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown walker type: %v", walkerType)
|
return nil, fmt.Errorf("unknown walker type: %v", walkerType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Detect(root string) (Walker, error) {
|
func Detect(root string, paths []string) (Walker, error) {
|
||||||
// for now, we keep it simple and try git first, filesystem second
|
// for now, we keep it simple and try git first, filesystem second
|
||||||
w, err := NewGit(root)
|
w, err := NewGit(root, paths)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return w, err
|
return w, err
|
||||||
}
|
}
|
||||||
return NewFilesystem(root)
|
return NewFilesystem(root, paths)
|
||||||
}
|
}
|
Reference in New Issue
Block a user