From 7badb998e431a2c6a2ea29a5eb2790f390209614 Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Sat, 5 Oct 2024 18:35:30 +0100 Subject: [PATCH] feat: replace kong with cobra/viper `treefmt.toml` has been extended to include an entry for most of the flags that can be passed to `treefmt`. In addition, values can now be specified via the environment, prefixed with `TREEFMT_`. Finally, the order of precedence for value lookup has been fixed: flag -> env -> treefmt.toml. Closes #351 --- LICENSE | 1 + cli/cli.go | 61 --- cli/helpers_test.go | 85 ---- cli/mappers.go | 37 -- {cli => cmd/format}/format.go | 337 ++++++--------- cmd/init/init.go | 20 + cmd/init/init.toml | 11 + cmd/root.go | 127 ++++++ cli/format_test.go => cmd/root_test.go | 203 +++++++-- config/config.go | 270 +++++++++++- config/config_test.go | 566 ++++++++++++++++++++++++- config/formatter.go | 14 - go.mod | 24 +- go.sum | 98 +++-- init.toml | 38 ++ main.go | 34 +- nix/devshells/default.nix | 17 +- nix/packages/treefmt/gomod2nix.toml | 66 ++- test/examples/treefmt.toml | 1 - test/temp.go | 2 +- walk/type_enum.go | 94 ++++ walk/walker.go | 11 +- 22 files changed, 1541 insertions(+), 576 deletions(-) delete mode 100644 cli/cli.go delete mode 100644 cli/helpers_test.go delete mode 100644 cli/mappers.go rename {cli => cmd/format}/format.go (66%) create mode 100644 cmd/init/init.go create mode 100644 cmd/init/init.toml create mode 100644 cmd/root.go rename cli/format_test.go => cmd/root_test.go (80%) delete mode 100644 config/formatter.go create mode 100644 walk/type_enum.go diff --git a/LICENSE b/LICENSE index b5414631..2f00383d 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/cli/cli.go b/cli/cli.go deleted file mode 100644 index be69c65a..00000000 --- a/cli/cli.go +++ /dev/null @@ -1,61 +0,0 @@ -package cli - -import ( - "os" - - "github.com/gobwas/glob" - - "github.com/alecthomas/kong" - "github.com/charmbracelet/log" - "github.com/numtide/treefmt/format" - "github.com/numtide/treefmt/walk" -) - -func New() *Format { - return &Format{} -} - -type Format struct { - AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing."` - WorkingDirectory kong.ChangeDirFlag `default:"." short:"C" help:"Run as if treefmt was started in the specified working directory instead of the current working directory."` - NoCache bool `help:"Ignore the evaluation cache entirely. Useful for CI."` - ClearCache bool `short:"c" help:"Reset the evaluation cache. Use in case the cache is not precise enough."` - ConfigFile string `type:"existingfile" help:"Load the config file from the given path (defaults to searching upwards for treefmt.toml or .treefmt.toml)."` - FailOnChange bool `help:"Exit with error if any changes were made. Useful for CI."` - Formatters []string `short:"f" help:"Specify formatters to apply. Defaults to all formatters."` - TreeRoot string `type:"existingdir" xor:"tree-root" env:"PRJ_ROOT" help:"The root directory from which treefmt will start walking the filesystem (defaults to the directory containing the config file)."` - TreeRootFile string `type:"string" xor:"tree-root" help:"File to search for to find the project root (if --tree-root is not passed)."` - 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."` - Version bool `name:"version" short:"V" help:"Print version."` - Init bool `name:"init" short:"i" help:"Create a new treefmt.toml."` - - OnUnmatched log.Level `name:"on-unmatched" short:"u" default:"warn" help:"Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are ."` - - 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."` - - CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."` - - Ci bool `help:"Runs treefmt in a CI mode, enabling --no-cache, --fail-on-change and adjusting some other settings best suited to a CI use case."` - - formatters map[string]*format.Formatter - globalExcludes []glob.Glob - - filesCh chan *walk.File - formattedCh chan *format.Task - processedCh chan *format.Task -} - -func (f *Format) configureLogging() { - log.SetReportTimestamp(false) - log.SetOutput(os.Stderr) - - if f.Verbosity == 0 { - log.SetLevel(log.WarnLevel) - } else if f.Verbosity == 1 { - log.SetLevel(log.InfoLevel) - } else if f.Verbosity > 1 { - log.SetLevel(log.DebugLevel) - } -} diff --git a/cli/helpers_test.go b/cli/helpers_test.go deleted file mode 100644 index 64666cbb..00000000 --- a/cli/helpers_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "os" - "testing" - - "github.com/charmbracelet/log" - - "github.com/numtide/treefmt/stats" - - "github.com/numtide/treefmt/test" - - "github.com/alecthomas/kong" - "github.com/stretchr/testify/require" -) - -func newKong(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong { - t.Helper() - options = append([]kong.Option{ - kong.Name("test"), - kong.Exit(func(int) { - t.Helper() - t.Fatalf("unexpected exit()") - }), - }, options...) - parser, err := kong.New(cli, options...) - require.NoError(t, err) - return parser -} - -func cmd(t *testing.T, args ...string) ([]byte, error) { - t.Helper() - - // create a new kong context - p := newKong(t, New(), NewOptions()...) - ctx, err := p.Parse(args) - if err != nil { - return nil, err - } - - tempDir := t.TempDir() - tempOut := test.TempFile(t, tempDir, "combined_output", nil) - - // capture standard outputs before swapping them - stdout := os.Stdout - stderr := os.Stderr - - // swap them temporarily - os.Stdout = tempOut - os.Stderr = tempOut - - log.SetOutput(tempOut) - - // run the command - if err = ctx.Run(); err != nil { - return nil, err - } - - // reset and read the temporary output - if _, err = tempOut.Seek(0, 0); err != nil { - return nil, fmt.Errorf("failed to reset temp output for reading: %w", err) - } - - out, err := io.ReadAll(tempOut) - if err != nil { - return nil, fmt.Errorf("failed to read temp output: %w", err) - } - - // swap outputs back - os.Stdout = stdout - os.Stderr = stderr - log.SetOutput(stderr) - - return out, nil -} - -func assertStats(t *testing.T, as *require.Assertions, traversed int32, emitted int32, matched int32, formatted int32) { - t.Helper() - as.Equal(traversed, stats.Value(stats.Traversed), "stats.traversed") - as.Equal(emitted, stats.Value(stats.Emitted), "stats.emitted") - as.Equal(matched, stats.Value(stats.Matched), "stats.matched") - as.Equal(formatted, stats.Value(stats.Formatted), "stats.formatted") -} diff --git a/cli/mappers.go b/cli/mappers.go deleted file mode 100644 index 546f1d4f..00000000 --- a/cli/mappers.go +++ /dev/null @@ -1,37 +0,0 @@ -package cli - -import ( - "fmt" - "reflect" - - "github.com/alecthomas/kong" - "github.com/charmbracelet/log" -) - -func NewOptions() []kong.Option { - return []kong.Option{ - kong.TypeMapper(reflect.TypeOf(log.DebugLevel), logLevelDecoder()), - } -} - -func logLevelDecoder() kong.MapperFunc { - return func(ctx *kong.DecodeContext, target reflect.Value) error { - t, err := ctx.Scan.PopValue("string") - if err != nil { - return err - } - var str string - switch v := t.Value.(type) { - case string: - str = v - default: - return fmt.Errorf("expected a string but got %q (%T)", t, t.Value) - } - level, err := log.ParseLevel(str) - if err != nil { - return fmt.Errorf("failed to parse '%v' as log level: %w", level, err) - } - target.Set(reflect.ValueOf(level)) - return nil - } -} diff --git a/cli/format.go b/cmd/format/format.go similarity index 66% rename from cli/format.go rename to cmd/format/format.go index 5cb473d8..9046d297 100644 --- a/cli/format.go +++ b/cmd/format/format.go @@ -1,4 +1,4 @@ -package cli +package format import ( "context" @@ -10,20 +10,21 @@ import ( "path/filepath" "runtime" "runtime/pprof" - "strings" "syscall" "time" - "github.com/numtide/treefmt/format" - "github.com/numtide/treefmt/stats" - "mvdan.cc/sh/v3/expand" - + "github.com/charmbracelet/log" + "github.com/gobwas/glob" "github.com/numtide/treefmt/cache" "github.com/numtide/treefmt/config" + "github.com/numtide/treefmt/format" + "github.com/numtide/treefmt/stats" "github.com/numtide/treefmt/walk" - - "github.com/charmbracelet/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" "golang.org/x/sync/errgroup" + + "mvdan.cc/sh/v3/expand" ) const ( @@ -32,25 +33,18 @@ const ( var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled") -func (f *Format) Run() (err error) { - // set log level and other options - f.configureLogging() +func Run(v *viper.Viper, cmd *cobra.Command, paths []string) error { + cmd.SilenceUsage = true + + cfg, err := config.FromViper(v) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } // initialise stats collection stats.Init() - // ci mode - if f.Ci { - f.NoCache = true - f.FailOnChange = true - - // ensure INFO level - if f.Verbosity < 1 { - f.Verbosity = 1 - } - // reconfigure logging - f.configureLogging() - + if cfg.CI { log.Info("ci mode enabled") startAfter := time.Now(). @@ -70,9 +64,51 @@ func (f *Format) Run() (err error) { <-time.After(time.Until(startAfter)) } + if cfg.Stdin { + // check we have only received one path arg which we use for the file extension / matching to formatters + if len(paths) != 1 { + return fmt.Errorf("exactly one path should be specified when using the --stdin flag") + } + + // read stdin into a temporary file with the same file extension + pattern := fmt.Sprintf("*%s", filepath.Ext(paths[0])) + + file, err := os.CreateTemp("", pattern) + if err != nil { + return fmt.Errorf("failed to create a temporary file for processing stdin: %w", err) + } + + if _, err = io.Copy(file, os.Stdin); err != nil { + return fmt.Errorf("failed to copy stdin into a temporary file") + } + + // set the tree root to match the temp directory + cfg.TreeRoot, err = filepath.Abs(filepath.Dir(file.Name())) + if err != nil { + return fmt.Errorf("failed to get absolute path for tree root: %w", err) + } + + // configure filesystem walker to traverse the temporary tree root + cfg.Walk = "filesystem" + + // update paths with temp file + paths[0] = file.Name() + + } else { + // checks all paths are contained within the tree root + for idx, path := range paths { + rootPath := filepath.Join(cfg.TreeRoot, path) + if _, err = os.Stat(rootPath); err != nil { + return fmt.Errorf("path %s not found within the tree root %s", path, cfg.TreeRoot) + } + // update the path entry with an absolute path + paths[idx] = filepath.Clean(rootPath) + } + } + // cpu profiling - if f.CpuProfile != "" { - cpuProfile, err := os.Create(f.CpuProfile) + if cfg.CpuProfile != "" { + cpuProfile, err := os.Create(cfg.CpuProfile) if err != nil { return fmt.Errorf("failed to open file for writing cpu profile: %w", err) } else if err = pprof.StartCPUProfile(cpuProfile); err != nil { @@ -96,78 +132,21 @@ func (f *Format) Run() (err error) { } }() - // find the config file unless specified - if f.ConfigFile == "" { - pwd, err := os.Getwd() - if err != nil { - return err - } - f.ConfigFile, _, err = findUp(pwd, "treefmt.toml", ".treefmt.toml") - if err != nil { - return err - } - } - - // default the tree root to the directory containing the config file - if f.TreeRoot == "" { - f.TreeRoot = filepath.Dir(f.ConfigFile) - } - - // search the tree root using the --tree-root-file if specified - if f.TreeRootFile != "" { - pwd, err := os.Getwd() - if err != nil { - return err - } - _, f.TreeRoot, err = findUp(pwd, f.TreeRootFile) - if err != nil { - return err - } - } - - log.Debugf("config-file=%s tree-root=%s", f.ConfigFile, f.TreeRoot) - - // ensure all path arguments exist and are contained within the tree root - for _, path := range f.Paths { - relPath, err := filepath.Rel(f.TreeRoot, path) - if err != nil { - return fmt.Errorf("failed to determine relative path for %s to the tree root %s: %w", path, f.TreeRoot, err) - } - if strings.Contains(relPath, "..") { - return fmt.Errorf("path %s is outside the tree root %s", path, f.TreeRoot) - } - if f.Stdin { - // skip checking if the file exists if we are processing from stdin - // the file path is just used for matching against glob rules - continue - } - // check the path exists - _, err = os.Stat(path) - if err != nil { - return err - } - } - - // read config - cfg, err := config.ReadFile(f.ConfigFile, f.Formatters) - if err != nil { - return fmt.Errorf("failed to read config file %v: %w", f.ConfigFile, err) - } - // compile global exclude globs - if f.globalExcludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil { + globalExcludes, err := format.CompileGlobs(cfg.Excludes) + if err != nil { return fmt.Errorf("failed to compile global excludes: %w", err) } // initialise formatters - f.formatters = make(map[string]*format.Formatter) + formatters := make(map[string]*format.Formatter) env := expand.ListEnviron(os.Environ()...) - for name, formatterCfg := range cfg.Formatters { - formatter, err := format.NewFormatter(name, f.TreeRoot, env, formatterCfg) + for name, formatterCfg := range cfg.FormatterConfigs { + formatter, err := format.NewFormatter(name, cfg.TreeRoot, env, formatterCfg) - if errors.Is(err, format.ErrCommandNotFound) && f.AllowMissingFormatter { + if errors.Is(err, format.ErrCommandNotFound) && cfg.AllowMissingFormatter { log.Debugf("formatter command not found: %v", name) continue } else if err != nil { @@ -175,15 +154,15 @@ func (f *Format) Run() (err error) { } // store formatter by name - f.formatters[name] = formatter + formatters[name] = formatter } // open the cache if configured - if !f.NoCache { - if err = cache.Open(f.TreeRoot, f.ClearCache, f.formatters); err != nil { + if !cfg.NoCache { + if err = cache.Open(cfg.TreeRoot, cfg.ClearCache, formatters); err != nil { // if we can't open the cache, we log a warning and fallback to no cache log.Warnf("failed to open cache: %v", err) - f.NoCache = true + cfg.NoCache = true } } @@ -203,68 +182,54 @@ func (f *Format) Run() (err error) { // create a channel for files needing to be processed // we use a multiple of batch size here as a rudimentary concurrency optimization based on the host machine - f.filesCh = make(chan *walk.File, BatchSize*runtime.NumCPU()) + filesCh := make(chan *walk.File, BatchSize*runtime.NumCPU()) // create a channel for files that have been formatted - f.formattedCh = make(chan *format.Task, cap(f.filesCh)) + formattedCh := make(chan *format.Task, cap(filesCh)) // create a channel for files that have been processed - f.processedCh = make(chan *format.Task, cap(f.filesCh)) + processedCh := make(chan *format.Task, cap(filesCh)) // start concurrent processing tasks in reverse order - eg.Go(f.updateCache(ctx)) - eg.Go(f.detectFormatted(ctx)) - eg.Go(f.applyFormatters(ctx)) - eg.Go(f.walkFilesystem(ctx)) + eg.Go(updateCache(ctx, cfg, processedCh)) + eg.Go(detectFormatted(ctx, cfg, formattedCh, processedCh)) + eg.Go(applyFormatters(ctx, cfg, globalExcludes, formatters, filesCh, formattedCh)) + eg.Go(walkFilesystem(ctx, cfg, paths, filesCh)) // wait for everything to complete return eg.Wait() } -func (f *Format) walkFilesystem(ctx context.Context) func() error { +func walkFilesystem( + ctx context.Context, + cfg *config.Config, + paths []string, + filesCh chan *walk.File, +) func() error { return func() error { // close the files channel when we're done walking the file system - defer close(f.filesCh) + defer close(filesCh) eg, ctx := errgroup.WithContext(ctx) pathsCh := make(chan string, BatchSize) // By default, we use the cli arg, but if the stdin flag has been set we force a filesystem walk // since we will only be processing one file from a temp directory - walkerType := f.Walk - - if f.Stdin { - walkerType = walk.Filesystem - - // check we have only received one path arg which we use for the file extension / matching to formatters - if len(f.Paths) != 1 { - return fmt.Errorf("exactly one path should be specified when using the --stdin flag") - } - - // read stdin into a temporary file with the same file extension - pattern := fmt.Sprintf("*%s", filepath.Ext(f.Paths[0])) - file, err := os.CreateTemp("", pattern) - if err != nil { - return fmt.Errorf("failed to create a temporary file for processing stdin: %w", err) - } - - if _, err = io.Copy(file, os.Stdin); err != nil { - return fmt.Errorf("failed to copy stdin into a temporary file") - } - - f.Paths[0] = file.Name() + walkerType, err := walk.TypeString(cfg.Walk) + if err != nil { + return fmt.Errorf("invalid walk type: %w", err) } walkPaths := func() error { defer close(pathsCh) var idx int - for idx < len(f.Paths) { + for idx < len(paths) { select { case <-ctx.Done(): return ctx.Err() default: - pathsCh <- f.Paths[idx] + pathsCh <- paths[idx] idx += 1 } } @@ -272,22 +237,22 @@ func (f *Format) walkFilesystem(ctx context.Context) func() error { return nil } - if len(f.Paths) > 0 { + if len(paths) > 0 { eg.Go(walkPaths) } else { // no explicit paths to process, so we only need to process root - pathsCh <- f.TreeRoot + pathsCh <- cfg.TreeRoot close(pathsCh) } // create a filesystem walker - walker, err := walk.New(walkerType, f.TreeRoot, pathsCh) + walker, err := walk.New(walkerType, cfg.TreeRoot, pathsCh) if err != nil { return fmt.Errorf("failed to create walker: %w", err) } // if no cache has been configured, or we are processing from stdin, we invoke the walker directly - if f.NoCache || f.Stdin { + if cfg.NoCache || cfg.Stdin { return walker.Walk(ctx, func(file *walk.File, err error) error { select { case <-ctx.Done(): @@ -295,7 +260,7 @@ func (f *Format) walkFilesystem(ctx context.Context) func() error { default: stats.Add(stats.Traversed, 1) stats.Add(stats.Emitted, 1) - f.filesCh <- file + filesCh <- file return nil } }) @@ -303,7 +268,7 @@ func (f *Format) walkFilesystem(ctx context.Context) func() error { // otherwise we pass the walker to the cache and have it generate files for processing based on whether or not // they have been added/changed since the last invocation - if err = cache.ChangeSet(ctx, walker, f.filesCh); err != nil { + if err = cache.ChangeSet(ctx, walker, filesCh); err != nil { return fmt.Errorf("failed to generate change set: %w", err) } return nil @@ -311,7 +276,14 @@ func (f *Format) walkFilesystem(ctx context.Context) func() error { } // applyFormatters -func (f *Format) applyFormatters(ctx context.Context) func() error { +func applyFormatters( + ctx context.Context, + cfg *config.Config, + globalExcludes []glob.Glob, + formatters map[string]*format.Formatter, + filesCh chan *walk.File, + formattedCh chan *format.Task, +) func() error { // create our own errgroup for concurrent formatting tasks. // we don't want a cancel clause, in order to let formatters run up to the end. fg := errgroup.Group{} @@ -353,7 +325,7 @@ func (f *Format) applyFormatters(ctx context.Context) func() error { // pass each file to the formatted channel for _, task := range tasks { task.Errors = formatErrors - f.formattedCh <- task + formattedCh <- task } return nil @@ -375,17 +347,22 @@ func (f *Format) applyFormatters(ctx context.Context) func() error { return func() error { defer func() { // close processed channel - close(f.formattedCh) + close(formattedCh) }() + unmatchedLevel, err := log.ParseLevel(cfg.OnUnmatched) + if err != nil { + return fmt.Errorf("invalid on-unmatched value: %w", err) + } + // iterate the files channel - for file := range f.filesCh { + for file := range filesCh { // first check if this file has been globally excluded - if format.PathMatches(file.RelPath, f.globalExcludes) { + if format.PathMatches(file.RelPath, globalExcludes) { log.Debugf("path matched global excludes: %s", file.RelPath) // mark it as processed and continue to the next - f.formattedCh <- &format.Task{ + formattedCh <- &format.Task{ File: file, } continue @@ -393,7 +370,7 @@ func (f *Format) applyFormatters(ctx context.Context) func() error { // check if any formatters are interested in this file var matches []*format.Formatter - for _, formatter := range f.formatters { + for _, formatter := range formatters { if formatter.Wants(file) { matches = append(matches, formatter) } @@ -401,12 +378,13 @@ func (f *Format) applyFormatters(ctx context.Context) func() error { // see if any formatters matched if len(matches) == 0 { - if f.OnUnmatched == log.FatalLevel { + + if unmatchedLevel == log.FatalLevel { return fmt.Errorf("no formatter for path: %s", file.RelPath) } - log.Logf(f.OnUnmatched, "no formatter for path: %s", file.RelPath) + log.Logf(unmatchedLevel, "no formatter for path: %s", file.RelPath) // mark it as processed and continue to the next - f.formattedCh <- &format.Task{ + formattedCh <- &format.Task{ File: file, } } else { @@ -431,11 +409,11 @@ func (f *Format) applyFormatters(ctx context.Context) func() error { } } -func (f *Format) detectFormatted(ctx context.Context) func() error { +func detectFormatted(ctx context.Context, cfg *config.Config, formattedCh chan *format.Task, processedCh chan *format.Task) func() error { return func() error { defer func() { // close formatted channel - close(f.processedCh) + close(processedCh) }() for { @@ -445,7 +423,7 @@ func (f *Format) detectFormatted(ctx context.Context) func() error { case <-ctx.Done(): return ctx.Err() // take the next task that has been processed - case task, ok := <-f.formattedCh: + case task, ok := <-formattedCh: if !ok { // channel has been closed, no further files to process return nil @@ -463,7 +441,7 @@ func (f *Format) detectFormatted(ctx context.Context) func() error { stats.Add(stats.Formatted, 1) logMethod := log.Debug - if f.FailOnChange { + if cfg.FailOnChange { // surface the changed file more obviously logMethod = log.Error } @@ -482,13 +460,13 @@ func (f *Format) detectFormatted(ctx context.Context) func() error { } // mark as processed - f.processedCh <- task + processedCh <- task } } } } -func (f *Format) updateCache(ctx context.Context) func() error { +func updateCache(ctx context.Context, cfg *config.Config, processedCh chan *format.Task) func() error { return func() error { // used to batch updates for more efficient txs batch := make([]*format.Task, 0, BatchSize) @@ -509,7 +487,7 @@ func (f *Format) updateCache(ctx context.Context) func() error { // if we are processing from stdin that means we are outputting to stdout, no caching involved // if f.NoCache is set that means either the user explicitly disabled the cache or we failed to open on - if f.Stdin || f.NoCache { + if cfg.Stdin || cfg.NoCache { // do nothing processBatch = func() error { return nil } } @@ -521,7 +499,7 @@ func (f *Format) updateCache(ctx context.Context) func() error { case <-ctx.Done(): return ctx.Err() // respond to formatted files - case task, ok := <-f.processedCh: + case task, ok := <-processedCh: if !ok { // channel has been closed, no further files to process break LOOP @@ -529,7 +507,7 @@ func (f *Format) updateCache(ctx context.Context) func() error { file := task.File - if f.Stdin { + if cfg.Stdin { // dump file into stdout f, err := os.Open(file.Path) if err != nil { @@ -566,70 +544,15 @@ func (f *Format) updateCache(ctx context.Context) func() error { } // if fail on change has been enabled, check that no files were actually formatted, throwing an error if so - if f.FailOnChange && stats.Value(stats.Formatted) != 0 { + if cfg.FailOnChange && stats.Value(stats.Formatted) != 0 { return ErrFailOnChange } // print stats to stdout unless we are processing stdin and printing the results to stdout - if !f.Stdin { + if !cfg.Stdin { stats.Print() } return nil } } - -func findUp(searchDir string, fileNames ...string) (path string, dir string, err error) { - for _, dir := range eachDir(searchDir) { - for _, f := range fileNames { - path := filepath.Join(dir, f) - if fileExists(path) { - return path, dir, nil - } - } - } - return "", "", fmt.Errorf("could not find %s in %s", fileNames, searchDir) -} - -func eachDir(path string) (paths []string) { - path, err := filepath.Abs(path) - if err != nil { - return - } - - paths = []string{path} - - if path == "/" { - return - } - - for i := len(path) - 1; i >= 0; i-- { - if path[i] == os.PathSeparator { - path = path[:i] - if path == "" { - path = "/" - } - paths = append(paths, path) - } - } - - return -} - -func fileExists(path string) bool { - // Some broken filesystems like SSHFS return file information on stat() but - // then cannot open the file. So we use os.Open. - f, err := os.Open(path) - if err != nil { - return false - } - defer f.Close() - - // Next, check that the file is a regular file. - fi, err := f.Stat() - if err != nil { - return false - } - - return fi.Mode().IsRegular() -} diff --git a/cmd/init/init.go b/cmd/init/init.go new file mode 100644 index 00000000..91c5dc11 --- /dev/null +++ b/cmd/init/init.go @@ -0,0 +1,20 @@ +package init + +import ( + _ "embed" + "fmt" + "os" +) + +// We embed the sample toml file for use with the init flag. +// +//go:embed init.toml +var initBytes []byte + +func Run() error { + if err := os.WriteFile("treefmt.toml", initBytes, 0o644); err != nil { + return fmt.Errorf("failed to write treefmt.toml: %w", err) + } + fmt.Printf("Generated treefmt.toml. Now it's your turn to edit it.\n") + return nil +} diff --git a/cmd/init/init.toml b/cmd/init/init.toml new file mode 100644 index 00000000..d332172f --- /dev/null +++ b/cmd/init/init.toml @@ -0,0 +1,11 @@ +# One CLI to format the code tree - https://git.numtide.com/numtide/treefmt + +[formatter.mylanguage] +# Formatter to run +command = "command-to-run" +# Command-line arguments for the command +options = [] +# Glob pattern of files to include +includes = [ "*." ] +# Glob patterns of files to exclude +excludes = [] \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..58064eb8 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/charmbracelet/log" + "github.com/numtide/treefmt/build" + "github.com/numtide/treefmt/cmd/format" + _init "github.com/numtide/treefmt/cmd/init" + "github.com/numtide/treefmt/config" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewRoot() *cobra.Command { + var ( + treefmtInit bool + configFile string + ) + + // create a viper instance for reading in config + v, err := config.NewViper() + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to create viper instance: %w", err)) + } + + // create out root command + cmd := &cobra.Command{ + Use: "treefmt ", + Short: "One CLI to format your repo", + Version: build.Version, + RunE: func(cmd *cobra.Command, args []string) error { + return runE(v, cmd, args) + }, + } + + // update version template + cmd.SetVersionTemplate("treefmt {{.Version}}") + + fs := cmd.Flags() + + // add our config flags to the command's flag set + config.SetFlags(fs) + + // xor tree-root and tree-root-file flags + cmd.MarkFlagsMutuallyExclusive("tree-root", "tree-root-file") + + cmd.HelpTemplate() + + // add a couple of special flags which don't have a corresponding entry in treefmt.toml + fs.StringVar(&configFile, "config-file", "", "Load the config file from the given path (defaults to searching upwards for treefmt.toml or .treefmt.toml).") + fs.BoolVarP(&treefmtInit, "init", "i", false, "Create a treefmt.toml file in the current directory.") + + // bind our command's flags to viper + if err := v.BindPFlags(fs); err != nil { + cobra.CheckErr(fmt.Errorf("failed to bind global config to viper: %w", err)) + } + + // bind prj_root to the tree-root flag, allowing viper to handle environment override for us + // conforms with https://github.com/numtide/prj-spec/blob/main/PRJ_SPEC.md + cobra.CheckErr(v.BindPFlag("prj_root", fs.Lookup("tree-root"))) + + return cmd +} + +func runE(v *viper.Viper, cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + // change working directory if required + workingDir, err := filepath.Abs(v.GetString("working-dir")) + if err != nil { + return fmt.Errorf("failed to get absolute path for working directory: %w", err) + } else if err = os.Chdir(workingDir); err != nil { + return fmt.Errorf("failed to change working directory: %w", err) + } + + // check if we are running the init command + if init, err := flags.GetBool("init"); err != nil { + return fmt.Errorf("failed to read init flag: %w", err) + } else if init { + return _init.Run() + } + + // otherwise attempt to load the config file + + // use the path specified by the flag + configFile, err := flags.GetString("config-file") + if err != nil { + return fmt.Errorf("failed to read config-file flag: %w", err) + } + + // fallback to env + if configFile == "" { + configFile = os.Getenv("TREEFMT_CONFIG") + } + + // find the config file if one was not specified + if configFile == "" { + if configFile, _, err = config.FindUp(workingDir, "treefmt.toml", ".treefmt.toml"); err != nil { + return fmt.Errorf("failed to find treefmt config file: %w", err) + } + } + + // read in the config + v.SetConfigFile(configFile) + if err := v.ReadInConfig(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to read config file '%s': %w", configFile, err)) + } + + // configure logging + log.SetOutput(os.Stderr) + log.SetReportTimestamp(false) + + switch v.GetInt("verbose") { + case 0: + log.SetLevel(log.WarnLevel) + case 1: + log.SetLevel(log.InfoLevel) + default: + log.SetLevel(log.DebugLevel) + } + + // format + return format.Run(v, cmd, args) +} diff --git a/cli/format_test.go b/cmd/root_test.go similarity index 80% rename from cli/format_test.go rename to cmd/root_test.go index 6ce199b6..0da24793 100644 --- a/cli/format_test.go +++ b/cmd/root_test.go @@ -1,8 +1,9 @@ -package cli +package cmd import ( "bufio" "fmt" + "io" "os" "os/exec" "path" @@ -12,7 +13,14 @@ import ( "time" "github.com/numtide/treefmt/config" + + "github.com/charmbracelet/log" + "github.com/numtide/treefmt/stats" + + format2 "github.com/numtide/treefmt/cmd/format" + "github.com/numtide/treefmt/format" + "github.com/numtide/treefmt/test" "github.com/go-git/go-billy/v5/osfs" @@ -78,7 +86,8 @@ func TestOnUnmatched(t *testing.T) { as.NoError(err) checkOutput("INFO", out) - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv", "-u", "debug") + t.Setenv("TREEFMT_ON_UNMATCHED", "debug") + out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv") as.NoError(err) checkOutput("DEBU", out) } @@ -101,6 +110,13 @@ func TestCpuProfile(t *testing.T) { as.FileExists(filepath.Join(tempDir, "cpu.pprof")) _, err = os.Stat(filepath.Join(tempDir, "cpu.pprof")) as.NoError(err) + + t.Setenv("TREEFMT_CPU_PROFILE", "env.pprof") + _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter") + as.NoError(err) + as.FileExists(filepath.Join(tempDir, "env.pprof")) + _, err = os.Stat(filepath.Join(tempDir, "env.pprof")) + as.NoError(err) } func TestAllowMissingFormatter(t *testing.T) { @@ -109,26 +125,30 @@ func TestAllowMissingFormatter(t *testing.T) { tempDir := test.TempExamples(t) configPath := tempDir + "/treefmt.toml" - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "foo-fmt": { Command: "foo-fmt", }, }, }) - _, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) + _, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-vv") as.ErrorIs(err, format.ErrCommandNotFound) _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter") as.NoError(err) + + t.Setenv("TREEFMT_ALLOW_MISSING_FORMATTER", "true") + _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + as.NoError(err) } func TestSpecifyingFormatters(t *testing.T) { as := require.New(t) - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "elm": { Command: "touch", Options: []string{"-m"}, @@ -182,7 +202,8 @@ func TestSpecifyingFormatters(t *testing.T) { _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo") as.Errorf(err, "formatter not found in config: foo") - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "bar,foo") + t.Setenv("TREEFMT_FORMATTERS", "bar,foo") + _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.Errorf(err, "formatter not found in config: bar") } @@ -193,8 +214,8 @@ func TestIncludesAndExcludes(t *testing.T) { configPath := tempDir + "/touch.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -208,7 +229,7 @@ func TestIncludesAndExcludes(t *testing.T) { assertStats(t, as, 32, 32, 32, 0) // globally exclude nix files - cfg.Global.Excludes = []string{"*.nix"} + cfg.Excludes = []string{"*.nix"} test.WriteConfig(t, configPath, cfg) _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) @@ -216,14 +237,14 @@ func TestIncludesAndExcludes(t *testing.T) { assertStats(t, as, 32, 32, 31, 0) // add haskell files to the global exclude - cfg.Global.Excludes = []string{"*.nix", "*.hs"} + cfg.Excludes = []string{"*.nix", "*.hs"} test.WriteConfig(t, configPath, cfg) _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) assertStats(t, as, 32, 32, 25, 0) - echo := cfg.Formatters["echo"] + echo := cfg.FormatterConfigs["echo"] // remove python files from the echo formatter echo.Excludes = []string{"*.py"} @@ -233,14 +254,16 @@ func TestIncludesAndExcludes(t *testing.T) { as.NoError(err) assertStats(t, as, 32, 32, 23, 0) - // remove go files from the echo formatter - echo.Excludes = []string{"*.py", "*.go"} + // remove go files from the echo formatter via env + t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "*.py,*.go") test.WriteConfig(t, configPath, cfg) _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) assertStats(t, as, 32, 32, 22, 0) + t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "") // reset + // adjust the includes for echo to only include elm files echo.Includes = []string{"*.elm"} @@ -249,8 +272,8 @@ func TestIncludesAndExcludes(t *testing.T) { as.NoError(err) assertStats(t, as, 32, 32, 1, 0) - // add js files to echo formatter - echo.Includes = []string{"*.elm", "*.js"} + // add js files to echo formatter via env + t.Setenv("TREEFMT_FORMATTER_ECHO_INCLUDES", "*.elm,*.js") test.WriteConfig(t, configPath, cfg) _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) @@ -258,6 +281,29 @@ func TestIncludesAndExcludes(t *testing.T) { assertStats(t, as, 32, 32, 2, 0) } +func TestPrjRootEnvVariable(t *testing.T) { + as := require.New(t) + + tempDir := test.TempExamples(t) + configPath := tempDir + "/treefmt.toml" + + // test without any excludes + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ + "echo": { + Command: "echo", + Includes: []string{"*"}, + }, + }, + } + + test.WriteConfig(t, configPath, cfg) + t.Setenv("PRJ_ROOT", tempDir) + _, err := cmd(t, "--config-file", configPath) + as.NoError(err) + assertStats(t, as, 32, 32, 32, 0) +} + func TestCache(t *testing.T) { as := require.New(t) @@ -265,8 +311,8 @@ func TestCache(t *testing.T) { configPath := tempDir + "/touch.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -325,8 +371,8 @@ func TestChangeWorkingDirectory(t *testing.T) { configPath := tempDir + "/treefmt.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, @@ -341,6 +387,12 @@ func TestChangeWorkingDirectory(t *testing.T) { _, err = cmd(t, "-C", tempDir) as.NoError(err) assertStats(t, as, 32, 32, 32, 0) + + // use env + t.Setenv("TREEFMT_WORKING_DIR", tempDir) + _, err = cmd(t, "-c") + as.NoError(err) + assertStats(t, as, 32, 32, 32, 0) } func TestFailOnChange(t *testing.T) { @@ -350,8 +402,8 @@ func TestFailOnChange(t *testing.T) { configPath := tempDir + "/touch.toml" // test without any excludes - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "touch": { Command: "touch", Includes: []string{"*"}, @@ -361,15 +413,16 @@ func TestFailOnChange(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir) - as.ErrorIs(err, ErrFailOnChange) + as.ErrorIs(err, format2.ErrFailOnChange) // we have second precision mod time tracking time.Sleep(time.Second) // test with no cache + t.Setenv("TREEFMT_FAIL_ON_CHANGE", "true") test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir, "--no-cache") - as.ErrorIs(err, ErrFailOnChange) + _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") + as.ErrorIs(err, format2.ErrFailOnChange) } func TestBustCacheOnFormatterChange(t *testing.T) { @@ -394,8 +447,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) { as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH"))) // start with 2 formatters - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "python": { Command: "black", Includes: []string{"*.py"}, @@ -439,7 +492,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) { assertStats(t, as, 32, 0, 0, 0) // add go formatter - cfg.Formatters["go"] = &config.Formatter{ + cfg.FormatterConfigs["go"] = &config.Formatter{ Command: "gofmt", Options: []string{"-w"}, Includes: []string{"*.go"}, @@ -456,7 +509,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) { assertStats(t, as, 32, 0, 0, 0) // remove python formatter - delete(cfg.Formatters, "python") + delete(cfg.FormatterConfigs, "python") test.WriteConfig(t, configPath, cfg) _, err = cmd(t, args...) @@ -469,7 +522,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) { assertStats(t, as, 32, 0, 0, 0) // remove elm formatter - delete(cfg.Formatters, "elm") + delete(cfg.FormatterConfigs, "elm") test.WriteConfig(t, configPath, cfg) _, err = cmd(t, args...) @@ -489,14 +542,15 @@ func TestGitWorktree(t *testing.T) { configPath := filepath.Join(tempDir, "/treefmt.toml") // basic config - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, }, }, } + test.WriteConfig(t, configPath, cfg) // init a git repo @@ -564,7 +618,7 @@ func TestGitWorktree(t *testing.T) { // try with a bad path _, err = cmd(t, "-C", tempDir, "-c", "haskell", "foo") - as.ErrorContains(err, fmt.Sprintf("stat %s: no such file or directory", filepath.Join(tempDir, "foo"))) + as.ErrorContains(err, "path foo not found within the tree root") assertStats(t, as, 0, 0, 0, 0) // try with a path not in the git index, e.g. it is skipped @@ -596,14 +650,15 @@ func TestPathsArg(t *testing.T) { as.NoError(os.Chdir(tempDir)) // basic config - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "echo": { Command: "echo", Includes: []string{"*"}, }, }, } + test.WriteConfig(t, configPath, cfg) // without any path args @@ -618,15 +673,15 @@ func TestPathsArg(t *testing.T) { // specify a bad path _, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Bar.hs") - as.ErrorContains(err, "no such file or directory") + as.ErrorContains(err, "path haskell/Nested/Bar.hs not found within the tree root") // specify a path outside the tree root externalPath := filepath.Join(cwd, "go.mod") _, err = cmd(t, "-c", externalPath) - as.ErrorContains(err, fmt.Sprintf("%s is outside the tree root %s", externalPath, tempDir)) + as.ErrorContains(err, fmt.Sprintf("path %s not found within the tree root", externalPath)) } -func TestStdIn(t *testing.T) { +func TestStdin(t *testing.T) { as := require.New(t) // capture current cwd, so we can replace it after the test is finished @@ -700,8 +755,8 @@ func TestDeterministicOrderingInPipeline(t *testing.T) { tempDir := test.TempExamples(t) configPath := tempDir + "/treefmt.toml" - test.WriteConfig(t, configPath, config.Config{ - Formatters: map[string]*config.Formatter{ + test.WriteConfig(t, configPath, &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ // a and b have no priority set, which means they default to 0 and should execute first // a and b should execute in lexicographical order // c should execute first since it has a priority of 1 @@ -723,7 +778,6 @@ func TestDeterministicOrderingInPipeline(t *testing.T) { }, }, }) - _, err = cmd(t, "-C", tempDir) as.NoError(err) @@ -778,8 +832,8 @@ func TestRunInSubdir(t *testing.T) { as.NoError(os.Chdir(filepath.Join(tempDir, "elm"))) // basic config - cfg := config.Config{ - Formatters: map[string]*config.Formatter{ + cfg := &config.Config{ + FormatterConfigs: map[string]*config.Formatter{ "echo": { Command: "./echo", Includes: []string{"*"}, @@ -794,7 +848,66 @@ func TestRunInSubdir(t *testing.T) { assertStats(t, as, 32, 32, 32, 0) // specify some explicit paths, relative to the tree root - _, err = cmd(t, "-c", "elm.json", "../haskell/Nested/Foo.hs") + _, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") as.NoError(err) assertStats(t, as, 2, 2, 2, 0) } + +func cmd(t *testing.T, args ...string) ([]byte, error) { + t.Helper() + + tempDir := t.TempDir() + tempOut := test.TempFile(t, tempDir, "combined_output", nil) + + // capture standard outputs before swapping them + stdout := os.Stdout + stderr := os.Stderr + + // swap them temporarily + os.Stdout = tempOut + os.Stderr = tempOut + + log.SetOutput(tempOut) + + defer func() { + // swap outputs back + os.Stdout = stdout + os.Stderr = stderr + log.SetOutput(stderr) + }() + + // run the command + root := NewRoot() + + if args == nil { + // we must pass an empty array otherwise cobra with use os.Args[1:] + args = []string{} + } + root.SetArgs(args) + root.SetOut(tempOut) + root.SetErr(tempOut) + + if err := root.Execute(); err != nil { + return nil, err + } + + // reset and read the temporary output + if _, err := tempOut.Seek(0, 0); err != nil { + return nil, fmt.Errorf("failed to reset temp output for reading: %w", err) + } + + out, err := io.ReadAll(tempOut) + if err != nil { + return nil, fmt.Errorf("failed to read temp output: %w", err) + } + + return out, nil +} + +func assertStats(t *testing.T, as *require.Assertions, traversed int32, emitted int32, matched int32, formatted int32) { + t.Helper() + as.Equal(traversed, stats.Value(stats.Traversed), "stats.traversed") + as.Equal(emitted, stats.Value(stats.Emitted), "stats.emitted") + as.Equal(matched, stats.Value(stats.Matched), "stats.matched") + as.Equal(formatted, stats.Value(stats.Formatted), "stats.formatted") +} diff --git a/config/config.go b/config/config.go index 553aee62..3f3c9e83 100644 --- a/config/config.go +++ b/config/config.go @@ -2,32 +2,210 @@ package config import ( "fmt" + "os" + "path/filepath" + "strings" - "github.com/BurntSushi/toml" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) +// configReset is used to null out attempts to set certain values in the config file +var configReset = map[string]any{ + "clear-cache": false, + "no-cache": false, + "stdin": false, + "working-dir": ".", +} + // Config is used to represent the list of configured Formatters. type Config struct { + AllowMissingFormatter bool `mapstructure:"allow-missing-formatter" toml:"allow-missing-formatter,omitempty"` + CI bool `mapstructure:"ci" toml:"ci,omitempty"` + ClearCache bool `mapstructure:"clear-cache" toml:"-"` // not allowed in config + CpuProfile string `mapstructure:"cpu-profile" toml:"cpu-profile,omitempty"` + Excludes []string `mapstructure:"excludes" toml:"excludes,omitempty"` + FailOnChange bool `mapstructure:"fail-on-change" toml:"fail-on-change,omitempty"` + Formatters []string `mapstructure:"formatters" toml:"formatters,omitempty"` + NoCache bool `mapstructure:"no-cache" toml:"-"` // not allowed in config + OnUnmatched string `mapstructure:"on-unmatched" toml:"on-unmatched,omitempty"` + TreeRoot string `mapstructure:"tree-root" toml:"tree-root,omitempty"` + TreeRootFile string `mapstructure:"tree-root-file" toml:"tree-root-file,omitempty"` + Verbosity uint8 `mapstructure:"verbose" toml:"verbose,omitempty"` + Walk string `mapstructure:"walk" toml:"walk,omitempty"` + WorkingDirectory string `mapstructure:"working-dir" toml:"-"` + Stdin bool `mapstructure:"stdin" toml:"-"` // not allowed in config + + FormatterConfigs map[string]*Formatter `mapstructure:"formatter" toml:"formatter,omitempty"` + Global struct { - // Excludes is an optional list of glob patterns used to exclude certain files from all formatters. - Excludes []string `toml:"excludes"` - } `toml:"global"` - Formatters map[string]*Formatter `toml:"formatter"` + // Deprecated: Use Excludes + Excludes []string `mapstructure:"excludes" toml:"excludes,omitempty"` + } `mapstructure:"global" toml:"global,omitempty"` +} + +type Formatter struct { + // Command is the command to invoke when applying this Formatter. + Command string `mapstructure:"command" toml:"command"` + // Options are an optional list of args to be passed to Command. + Options []string `mapstructure:"options,omitempty" toml:"options,omitempty"` + // Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path. + Includes []string `mapstructure:"includes,omitempty" toml:"includes,omitempty"` + // Excludes is an optional list of glob patterns used to exclude certain files from this Formatter. + Excludes []string `mapstructure:"excludes,omitempty" toml:"excludes,omitempty"` + // Indicates the order of precedence when executing this Formatter in a sequence of Formatters. + Priority int `mapstructure:"priority,omitempty" toml:"priority,omitempty"` +} + +// SetFlags appends our flags to the provided flag set. +// We have a flag matching most entries in Config, taking care to ensure the name matches the field name defined in the +// mapstructure tag. +// We can rely on a flag's default value being provided in the event the same value was not specified in the config file. +func SetFlags(fs *pflag.FlagSet) { + fs.Bool( + "allow-missing-formatter", false, + "Do not exit with error if a configured formatter is missing. (env $TREEFMT_ALLOW_MISSING_FORMATTER)", + ) + fs.Bool( + "ci", false, + "Runs treefmt in a CI mode, enabling --no-cache, --fail-on-change and adjusting some other settings "+ + "best suited to a CI use case. (env $TREEFMT_CI)", + ) + fs.BoolP( + "clear-cache", "c", false, + "Reset the evaluation cache. Use in case the cache is not precise enough. (env $TREEFMT_CLEAR_CACHE)", + ) + fs.String( + "cpu-profile", "", + "The file into which a cpu profile will be written. (env $TREEFMT_CPU_PROFILE)", + ) + fs.StringSlice( + "excludes", nil, + "Exclude files or directories matching the specified globs. (env $TREEFMT_EXCLUDES)", + ) + fs.Bool( + "fail-on-change", false, + "Exit with error if any changes were made. Useful for CI. (env $TREEFMT_FAIL_ON_CHANGE)", + ) + fs.StringSliceP( + "formatters", "f", nil, + "Specify formatters to apply. Defaults to all configured formatters. (env $TREEFMT_FORMATTERS)", + ) + fs.Bool( + "no-cache", false, + "Ignore the evaluation cache entirely. Useful for CI. (env $TREEFMT_NO_CACHE)", + ) + fs.StringP( + "on-unmatched", "u", "warn", + "Log paths that did not match any formatters at the specified log level. Possible values are "+ + ". (env $TREEFMT_ON_UNMATCHED)", + ) + fs.Bool( + "stdin", false, + "Format the context passed in via stdin.", + ) + fs.String( + "tree-root", "", + "The root directory from which treefmt will start walking the filesystem (defaults to the directory "+ + "containing the config file). (env $TREEFMT_TREE_ROOT)", + ) + fs.String( + "tree-root-file", "", + "File to search for to find the tree root (if --tree-root is not passed). (env $TREEFMT_TREE_ROOT_FILE)", + ) + fs.CountP( + "verbose", "v", + "Set the verbosity of logs e.g. -vv. (env $TREEFMT_VERBOSE)", + ) + fs.String( + "walk", "auto", + "The method used to traverse the files within the tree root. Currently supports 'auto', 'git' or "+ + "'filesystem'. (env $TREEFMT_WALK)", + ) + fs.StringP( + "working-dir", "C", ".", + "Run as if treefmt was started in the specified working directory instead of the current working "+ + "directory. (env $TREEFMT_WORKING_DIR)", + ) } -// ReadFile reads from path and unmarshals toml into a Config instance. -func ReadFile(path string, names []string) (cfg *Config, err error) { - if _, err = toml.DecodeFile(path, &cfg); err != nil { - return nil, fmt.Errorf("failed to decode config file: %w", err) +// NewViper creates a Viper instance pre-configured with the following options: +// * TOML config type +// * automatic env enabled +// * `TREEFMT_` env prefix for environment variables +// * replacement of `-` and `.` with `_` when mapping from flags to env e.g. `global.excludes` => `TREEFMT_GLOBAL_EXCLUDES` +func NewViper() (*viper.Viper, error) { + v := viper.New() + + // Enforce toml (may open this up to other formats in the future) + v.SetConfigType("toml") + + // Allow env overrides for config and flags. + v.SetEnvPrefix("treefmt") + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + + // unset some env variables that we don't want automatically applied + if err := os.Unsetenv("TREEFMT_STDIN"); err != nil { + return nil, fmt.Errorf("failed to unset TREEFMT_STDIN: %w", err) + } + + return v, nil +} + +// FromViper takes a viper instance and produces a Config instance. +func FromViper(v *viper.Viper) (*Config, error) { + // reset certain values which are not allowed to be specified in the config file + if err := v.MergeConfigMap(configReset); err != nil { + return nil, fmt.Errorf("failed to overwrite config values: %w", err) + } + + // read config from viper + var err error + cfg := &Config{} + + if err = v.Unmarshal(cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + // resolve the working directory to an absolute path + cfg.WorkingDirectory, err = filepath.Abs(cfg.WorkingDirectory) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path for working directory: %w", err) + } + + // determine the tree root + if cfg.TreeRoot == "" { + // if none was specified, we first try with tree-root-file + if cfg.TreeRootFile != "" { + // search the tree root using the --tree-root-file if specified + _, cfg.TreeRoot, err = FindUp(cfg.WorkingDirectory, cfg.TreeRootFile) + if err != nil { + return nil, fmt.Errorf("failed to find tree-root based on tree-root-file: %w", err) + } + } else { + // otherwise fallback to the directory containing the config file + cfg.TreeRoot = filepath.Dir(v.ConfigFileUsed()) + } + } + + // resolve tree root to an absolute path + if cfg.TreeRoot, err = filepath.Abs(cfg.TreeRoot); err != nil { + return nil, fmt.Errorf("failed to get absolute path for tree root: %w", err) + } + + // prefer top level excludes, falling back to global.excludes for backwards compatibility + if len(cfg.Excludes) == 0 { + cfg.Excludes = cfg.Global.Excludes } // filter formatters based on provided names - if len(names) > 0 { + if len(cfg.Formatters) > 0 { filtered := make(map[string]*Formatter) // check if the provided names exist in the config - for _, name := range names { - formatterCfg, ok := cfg.Formatters[name] + for _, name := range cfg.Formatters { + formatterCfg, ok := cfg.FormatterConfigs[name] if !ok { return nil, fmt.Errorf("formatter %v not found in config", name) } @@ -35,8 +213,74 @@ func ReadFile(path string, names []string) (cfg *Config, err error) { } // updated formatters - cfg.Formatters = filtered + cfg.FormatterConfigs = filtered + } + + // ci mode + if cfg.CI { + cfg.NoCache = true + cfg.FailOnChange = true + + // ensure at least info level logging + if cfg.Verbosity < 1 { + cfg.Verbosity = 1 + } + } + + return cfg, nil +} + +func FindUp(searchDir string, fileNames ...string) (path string, dir string, err error) { + for _, dir := range eachDir(searchDir) { + for _, f := range fileNames { + path := filepath.Join(dir, f) + if fileExists(path) { + return path, dir, nil + } + } + } + return "", "", fmt.Errorf("could not find %s in %s", fileNames, searchDir) +} + +func eachDir(path string) (paths []string) { + path, err := filepath.Abs(path) + if err != nil { + return + } + + paths = []string{path} + + if path == "/" { + return + } + + for i := len(path) - 1; i >= 0; i-- { + if path[i] == os.PathSeparator { + path = path[:i] + if path == "" { + path = "/" + } + paths = append(paths, path) + } } return } + +func fileExists(path string) bool { + // Some broken filesystems like SSHFS return file information on stat() but + // then cannot open the file. So we use os.Open. + f, err := os.Open(path) + if err != nil { + return false + } + defer f.Close() + + // Next, check that the file is a regular file. + fi, err := f.Stat() + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} diff --git a/config/config_test.go b/config/config_test.go index 1a149b63..81211490 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,23 +1,553 @@ package config import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" "testing" + "github.com/BurntSushi/toml" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) -func TestReadConfigFile(t *testing.T) { +func newViper(t *testing.T) (*viper.Viper, *pflag.FlagSet) { + t.Helper() + v, err := NewViper() + if err != nil { + t.Fatal(err) + } + + tempDir := t.TempDir() + v.SetConfigFile(filepath.Join(tempDir, "treefmt.toml")) + + flags := pflag.NewFlagSet("test", pflag.ContinueOnError) + SetFlags(flags) + + if err := v.BindPFlags(flags); err != nil { + t.Fatal(err) + } + return v, flags +} + +func readValue(t *testing.T, v *viper.Viper, cfg map[string]any, test func(*Config)) { + t.Helper() + + // serialise the config and read it into viper + buf := bytes.NewBuffer(nil) + encoder := toml.NewEncoder(buf) + if err := encoder.Encode(cfg); err != nil { + t.Fatal(fmt.Errorf("failed to marshal config: %w", err)) + } else if err = v.ReadConfig(bufio.NewReader(buf)); err != nil { + t.Fatal(fmt.Errorf("failed to read config: %w", err)) + } + + // + decodedCfg, err := FromViper(v) + if err != nil { + t.Fatal(fmt.Errorf("failed to unmarshal config from viper: %w", err)) + } + + test(decodedCfg) +} + +func TestAllowMissingFormatter(t *testing.T) { as := require.New(t) - cfg, err := ReadFile("../test/examples/treefmt.toml", nil) - as.NoError(err, "failed to read config file") + cfg := make(map[string]any) + v, flags := newViper(t) - as.NotNil(cfg) + checkValue := func(expected bool) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.AllowMissingFormatter) + }) + } + + // default with no flag, env or config + checkValue(false) + + // set config value + cfg["allow-missing-formatter"] = true + checkValue(true) + + // env override + t.Setenv("TREEFMT_ALLOW_MISSING_FORMATTER", "false") + checkValue(false) + + // flag override + as.NoError(flags.Set("allow-missing-formatter", "true")) + checkValue(true) +} + +func TestCI(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValues := func(ci bool, noCache bool, failOnChange bool, verbosity uint8) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(ci, cfg.CI) + as.Equal(noCache, cfg.NoCache) + as.Equal(failOnChange, cfg.FailOnChange) + as.Equal(verbosity, cfg.Verbosity) + }) + } + + // default with no flag, env or config + checkValues(false, false, false, 0) + + // set config value + cfg["ci"] = true + checkValues(true, true, true, 1) + + // env override + t.Setenv("TREEFMT_CI", "false") + checkValues(false, false, false, 0) + + // flag override + as.NoError(flags.Set("ci", "true")) + checkValues(true, true, true, 1) + + // increase verbosity above 1 and check it isn't reset + cfg["verbose"] = 2 + checkValues(true, true, true, 2) +} + +func TestClearCache(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected bool) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.ClearCache) + }) + } + + // default with no flag, env or config + checkValue(false) + + // set config value and check that it has no effect + // you are not allowed to set clear-cache in config + cfg["clear-cache"] = true + checkValue(false) + + // env override + t.Setenv("TREEFMT_CLEAR_CACHE", "false") + checkValue(false) + + // flag override + as.NoError(flags.Set("clear-cache", "true")) + checkValue(true) +} + +func TestCpuProfile(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.CpuProfile) + }) + } + + // default with no flag, env or config + checkValue("") + + // set config value + cfg["cpu-profile"] = "/foo/bar" + checkValue("/foo/bar") + + // env override + t.Setenv("TREEFMT_CPU_PROFILE", "/fizz/buzz") + checkValue("/fizz/buzz") + + // flag override + as.NoError(flags.Set("cpu-profile", "/bla/bla")) + checkValue("/bla/bla") +} + +func TestExcludes(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected []string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.Excludes) + }) + } + + // default with no env or config + checkValue(nil) + + // set config value + cfg["excludes"] = []string{"foo", "bar"} + checkValue([]string{"foo", "bar"}) + + // test global.excludes fallback + delete(cfg, "excludes") + cfg["global"] = map[string]any{ + "excludes": []string{"fizz", "buzz"}, + } + checkValue([]string{"fizz", "buzz"}) + + // env override + t.Setenv("TREEFMT_EXCLUDES", "foo,bar") + checkValue([]string{"foo", "bar"}) + + // flag override + as.NoError(flags.Set("excludes", "bleep,bloop")) + checkValue([]string{"bleep", "bloop"}) +} + +func TestFailOnChange(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected bool) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.FailOnChange) + }) + } + + // default with no flag, env or config + checkValue(false) + + // set config value + cfg["fail-on-change"] = true + checkValue(true) + + // env override + t.Setenv("TREEFMT_FAIL_ON_CHANGE", "false") + checkValue(false) + + // flag override + as.NoError(flags.Set("fail-on-change", "true")) + checkValue(true) +} + +func TestFormatters(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected []string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.Formatters) + }) + } + + // default with no env or config + checkValue([]string{}) + + // set config value + cfg["formatter"] = map[string]any{ + "echo": map[string]any{ + "command": "echo", + }, + "touch": map[string]any{ + "command": "touch", + }, + "date": map[string]any{ + "command": "date", + }, + } + cfg["formatters"] = []string{"echo", "touch"} + checkValue([]string{"echo", "touch"}) + + // env override + t.Setenv("TREEFMT_FORMATTERS", "echo,date") + checkValue([]string{"echo", "date"}) + + // flag override + as.NoError(flags.Set("formatters", "date,touch")) + checkValue([]string{"date", "touch"}) + + // bad formatter name + as.NoError(flags.Set("formatters", "foo,echo,date")) + _, err := FromViper(v) + as.ErrorContains(err, "formatter foo not found in config") +} + +func TestNoCache(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) - as.Equal([]string{"*.toml"}, cfg.Global.Excludes) + checkValue := func(expected bool) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.NoCache) + }) + } + + // default with no flag, env or config + checkValue(false) + + // set config value and check that it has no effect + // you are not allowed to set no-cache in config + cfg["no-cache"] = true + checkValue(false) + + // env override + t.Setenv("TREEFMT_NO_CACHE", "false") + checkValue(false) + + // flag override + as.NoError(flags.Set("no-cache", "true")) + checkValue(true) +} + +func TestOnUnmatched(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.OnUnmatched) + }) + } + + // default with no flag, env or config + checkValue("warn") + + // set config value + cfg["on-unmatched"] = "error" + checkValue("error") + + // env override + t.Setenv("TREEFMT_ON_UNMATCHED", "debug") + checkValue("debug") + + // flag override + as.NoError(flags.Set("on-unmatched", "fatal")) + checkValue("fatal") +} + +func TestTreeRoot(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.TreeRoot) + }) + } + + // default with no flag, env or config + // should match the absolute path of the directory in which the config file is located + checkValue(filepath.Dir(v.ConfigFileUsed())) + + // set config value + cfg["tree-root"] = "/foo/bar" + checkValue("/foo/bar") + + // env override + t.Setenv("TREEFMT_TREE_ROOT", "/fizz/buzz") + checkValue("/fizz/buzz") + + // flag override + as.NoError(flags.Set("tree-root", "/flip/flop")) + checkValue("/flip/flop") +} + +func TestTreeRootFile(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + // create a directory structure with config files at various levels + tempDir := t.TempDir() + as.NoError(os.MkdirAll(filepath.Join(tempDir, "foo", "bar"), 0o755)) + as.NoError(os.WriteFile(filepath.Join(tempDir, "foo", "bar", "a.txt"), []byte{}, 0o644)) + as.NoError(os.WriteFile(filepath.Join(tempDir, "foo", "go.mod"), []byte{}, 0o644)) + as.NoError(os.MkdirAll(filepath.Join(tempDir, ".git"), 0o755)) + as.NoError(os.WriteFile(filepath.Join(tempDir, ".git", "config"), []byte{}, 0o644)) + + checkValue := func(treeRoot string, treeRootFile string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(treeRoot, cfg.TreeRoot) + as.Equal(treeRootFile, cfg.TreeRootFile) + }) + } + + // default with no flag, env or config + // should match the absolute path of the directory in which the config file is located + checkValue(filepath.Dir(v.ConfigFileUsed()), "") + + workDir := filepath.Join(tempDir, "foo", "bar") + t.Setenv("TREEFMT_WORKING_DIR", workDir) + + // set config value + // should match the lowest directory + cfg["tree-root-file"] = "a.txt" + checkValue(workDir, "a.txt") + + // env override + // should match the directory above + t.Setenv("TREEFMT_TREE_ROOT_FILE", "go.mod") + checkValue(filepath.Join(tempDir, "foo"), "go.mod") + + // flag override + // should match the root of the temp directory structure + as.NoError(flags.Set("tree-root-file", ".git/config")) + checkValue(tempDir, ".git/config") +} + +func TestVerbosity(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, _ := newViper(t) + + checkValue := func(expected uint8) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.Verbosity) + }) + } + + // default with no flag, env or config + checkValue(0) + + // set config value + cfg["verbose"] = 1 + checkValue(1) + + // env override + t.Setenv("TREEFMT_VERBOSE", "2") + checkValue(2) + + // flag override + // todo unsure how to set a count flag via the flags api + // as.NoError(flags.Set("verbose", "v")) + // checkValue(1) +} + +func TestWalk(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.Walk) + }) + } + + // default with no flag, env or config + checkValue("auto") + + // set config value + cfg["walk"] = "git" + checkValue("git") + + // env override + t.Setenv("TREEFMT_WALK", "filesystem") + checkValue("filesystem") + + // flag override + as.NoError(flags.Set("walk", "auto")) + checkValue("auto") +} + +func TestWorkingDirectory(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValue := func(expected string) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.WorkingDirectory) + }) + } + + cwd, err := os.Getwd() + as.NoError(err, "failed to get current working directory") + cwd, err = filepath.Abs(cwd) + as.NoError(err, "failed to get absolute path of current working directory") + + // default with no flag, env or config + // current working directory by default + checkValue(cwd) + + // set config value and check that it has no effect + // you are not allowed to set working-dir in config + cfg["working-dir"] = "/foo/bar/baz/../fizz" + checkValue(cwd) + + // env override + t.Setenv("TREEFMT_WORKING_DIR", "/fizz/buzz/..") + checkValue("/fizz") + + // flag override + as.NoError(flags.Set("working-dir", "/flip/flop")) + checkValue("/flip/flop") +} + +func TestStdin(t *testing.T) { + as := require.New(t) + + cfg := make(map[string]any) + v, flags := newViper(t) + + checkValues := func(stdin bool) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(stdin, cfg.Stdin) + }) + } + + // default with no flag, env or config + checkValues(false) + + // set config value and check that it has no effect + // you are not allowed to set stdin in config + cfg["stdin"] = true + checkValues(false) + + // env override + t.Setenv("TREEFMT_STDIN", "false") + checkValues(false) + + // flag override + as.NoError(flags.Set("stdin", "true")) + checkValues(true) +} + +func TestSampleConfigFile(t *testing.T) { + as := require.New(t) + + v := viper.New() + v.SetConfigFile("../test/examples/treefmt.toml") + as.NoError(v.ReadInConfig(), "failed to read config file") + + cfg, err := FromViper(v) + as.NoError(err, "failed to unmarshal config from viper") + + as.NotNil(cfg) + as.Equal([]string{"*.toml"}, cfg.Excludes) // python - python, ok := cfg.Formatters["python"] + python, ok := cfg.FormatterConfigs["python"] as.True(ok, "python formatter not found") as.Equal("black", python.Command) as.Nil(python.Options) @@ -25,7 +555,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(python.Excludes) // elm - elm, ok := cfg.Formatters["elm"] + elm, ok := cfg.FormatterConfigs["elm"] as.True(ok, "elm formatter not found") as.Equal("elm-format", elm.Command) as.Equal([]string{"--yes"}, elm.Options) @@ -33,7 +563,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(elm.Excludes) // go - golang, ok := cfg.Formatters["go"] + golang, ok := cfg.FormatterConfigs["go"] as.True(ok, "go formatter not found") as.Equal("gofmt", golang.Command) as.Equal([]string{"-w"}, golang.Options) @@ -41,7 +571,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(golang.Excludes) // haskell - haskell, ok := cfg.Formatters["haskell"] + haskell, ok := cfg.FormatterConfigs["haskell"] as.True(ok, "haskell formatter not found") as.Equal("ormolu", haskell.Command) as.Equal([]string{ @@ -55,7 +585,7 @@ func TestReadConfigFile(t *testing.T) { as.Equal([]string{"examples/haskell/"}, haskell.Excludes) // alejandra - alejandra, ok := cfg.Formatters["alejandra"] + alejandra, ok := cfg.FormatterConfigs["alejandra"] as.True(ok, "alejandra formatter not found") as.Equal("alejandra", alejandra.Command) as.Nil(alejandra.Options) @@ -64,7 +594,7 @@ func TestReadConfigFile(t *testing.T) { as.Equal(1, alejandra.Priority) // deadnix - deadnix, ok := cfg.Formatters["deadnix"] + deadnix, ok := cfg.FormatterConfigs["deadnix"] as.True(ok, "deadnix formatter not found") as.Equal("deadnix", deadnix.Command) as.Equal([]string{"-e"}, deadnix.Options) @@ -73,7 +603,7 @@ func TestReadConfigFile(t *testing.T) { as.Equal(2, deadnix.Priority) // ruby - ruby, ok := cfg.Formatters["ruby"] + ruby, ok := cfg.FormatterConfigs["ruby"] as.True(ok, "ruby formatter not found") as.Equal("rufo", ruby.Command) as.Equal([]string{"-x"}, ruby.Options) @@ -81,7 +611,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(ruby.Excludes) // prettier - prettier, ok := cfg.Formatters["prettier"] + prettier, ok := cfg.FormatterConfigs["prettier"] as.True(ok, "prettier formatter not found") as.Equal("prettier", prettier.Command) as.Equal([]string{"--write", "--tab-width", "4"}, prettier.Options) @@ -100,7 +630,7 @@ func TestReadConfigFile(t *testing.T) { as.Equal([]string{"CHANGELOG.md"}, prettier.Excludes) // rust - rust, ok := cfg.Formatters["rust"] + rust, ok := cfg.FormatterConfigs["rust"] as.True(ok, "rust formatter not found") as.Equal("rustfmt", rust.Command) as.Equal([]string{"--edition", "2018"}, rust.Options) @@ -108,7 +638,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(rust.Excludes) // shellcheck - shellcheck, ok := cfg.Formatters["shellcheck"] + shellcheck, ok := cfg.FormatterConfigs["shellcheck"] as.True(ok, "shellcheck formatter not found") as.Equal("shellcheck", shellcheck.Command) as.Equal(1, shellcheck.Priority) @@ -117,7 +647,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(shellcheck.Excludes) // shfmt - shfmt, ok := cfg.Formatters["shfmt"] + shfmt, ok := cfg.FormatterConfigs["shfmt"] as.True(ok, "shfmt formatter not found") as.Equal("shfmt", shfmt.Command) as.Equal(2, shfmt.Priority) @@ -126,7 +656,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(shfmt.Excludes) // opentofu - opentofu, ok := cfg.Formatters["opentofu"] + opentofu, ok := cfg.FormatterConfigs["opentofu"] as.True(ok, "opentofu formatter not found") as.Equal("tofu", opentofu.Command) as.Equal([]string{"fmt"}, opentofu.Options) @@ -134,7 +664,7 @@ func TestReadConfigFile(t *testing.T) { as.Nil(opentofu.Excludes) // missing - foo, ok := cfg.Formatters["foo-fmt"] + foo, ok := cfg.FormatterConfigs["foo-fmt"] as.True(ok, "foo formatter not found") as.Equal("foo-fmt", foo.Command) } diff --git a/config/formatter.go b/config/formatter.go deleted file mode 100644 index 1a780db1..00000000 --- a/config/formatter.go +++ /dev/null @@ -1,14 +0,0 @@ -package config - -type Formatter struct { - // Command is the command to invoke when applying this Formatter. - Command string `toml:"command"` - // Options are an optional list of args to be passed to Command. - Options []string `toml:"options,omitempty"` - // Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path. - Includes []string `toml:"includes,omitempty"` - // Excludes is an optional list of glob patterns used to exclude certain files from this Formatter. - Excludes []string `toml:"excludes,omitempty"` - // Indicates the order of precedence when executing this Formatter in a sequence of Formatters. - Priority int `toml:"priority,omitempty"` -} diff --git a/go.mod b/go.mod index 2d0740bd..4018bb47 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.22 require ( github.com/BurntSushi/toml v1.4.0 github.com/adrg/xdg v0.5.0 - github.com/alecthomas/kong v1.2.1 github.com/charmbracelet/log v0.4.0 github.com/go-git/go-billy/v5 v5.5.1-0.20240930170605-5f263c979534 github.com/go-git/go-git/v5 v5.12.1-0.20240930111449-d1843220b6ab github.com/gobwas/glob v0.2.3 github.com/otiai10/copy v1.14.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/vmihailenco/msgpack/v5 v5.4.1 go.etcd.io/bbolt v1.3.11 @@ -26,31 +28,47 @@ require ( github.com/charmbracelet/lipgloss v0.10.0 // indirect github.com/cloudflare/circl v1.3.8 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7360e3b1..6d72d5c5 100644 --- a/go.sum +++ b/go.sum @@ -9,16 +9,6 @@ github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0k github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= -github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= -github.com/alecthomas/kong v1.2.0 h1:rzOKVDXrKg6hpQi+99VFbgkiXLCRbnYp18PAlK6wYas= -github.com/alecthomas/kong v1.2.0/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= -github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= -github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -33,45 +23,31 @@ github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v0.0.0-20240618083138-03be62527ccb h1:2SoxRauy2IqekRMggrQk3yNI5X6omSnk6ugVbFywwXs= github.com/elazarl/goproxy v0.0.0-20240618083138-03be62527ccb/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.1-0.20240819193939-9b484184bdcc h1:fpw3vn8skBvfPwsKRq6K2o/55ZcwAid/9lubG/NyNNE= -github.com/go-git/go-billy/v5 v5.5.1-0.20240819193939-9b484184bdcc/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-billy/v5 v5.5.1-0.20240828070317-59c50b000c7a h1:CDPmu0p7gv6zJn35T/RtZyIq98I2SwHtLrp697pM3KI= -github.com/go-git/go-billy/v5 v5.5.1-0.20240828070317-59c50b000c7a/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-billy/v5 v5.5.1-0.20240902165505-04d471ab6285 h1:M1YAPXgyNZxbsGCVYIdOXrRBgbw+3+X5xhOXkNEehzw= -github.com/go-git/go-billy/v5 v5.5.1-0.20240902165505-04d471ab6285/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-billy/v5 v5.5.1-0.20240917100134-b0c83cae0621 h1:iyMznNr6ULbB8jeiQ66tv2qZfGUef+61o7qa0BKfoho= -github.com/go-git/go-billy/v5 v5.5.1-0.20240917100134-b0c83cae0621/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-billy/v5 v5.5.1-0.20240924073428-9745bbbd3431 h1:sQW0J3LNJrRunpY1fO7QO4m8dr6N3QTHuRh+WXbWyqA= -github.com/go-git/go-billy/v5 v5.5.1-0.20240924073428-9745bbbd3431/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-billy/v5 v5.5.1-0.20240927131424-c1ee0b97d109 h1:7oA/JFyGfyGz60ykn+9oU+prmtdDA7hVFsaJpxwY5pc= -github.com/go-git/go-billy/v5 v5.5.1-0.20240927131424-c1ee0b97d109/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-billy/v5 v5.5.1-0.20240930170605-5f263c979534 h1:ReIiJ3+RmLoagnYcjfgxfxAaIG+zkzttS56LvUsnKN8= github.com/go-git/go-billy/v5 v5.5.1-0.20240930170605-5f263c979534/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.1-0.20240821195137-5c762aefcd8d h1:+KOoJFltZdLrtMrrOqaTYr8LWc7q296l6Y/+/bS9At0= -github.com/go-git/go-git/v5 v5.12.1-0.20240821195137-5c762aefcd8d/go.mod h1:tTeL/MQl8Pjm1QfKA/x/F0E04y9g5EynnXfV52kvvTw= -github.com/go-git/go-git/v5 v5.12.1-0.20240905150439-cef892e5701b h1:Y9dDjdayADW+IO4Yrwa0Pd7uLrEdac7mtfLAH69Ho2U= -github.com/go-git/go-git/v5 v5.12.1-0.20240905150439-cef892e5701b/go.mod h1:50xCkQWA/V4E2fDE+DpgupVhAqJhPv74BLsWyoOm1lc= -github.com/go-git/go-git/v5 v5.12.1-0.20240906142134-9cf0e3ee57dd h1:EaDJxDdERXsQegyT0DqsrDTo/OBnAmmrBIiq3OnHcdU= -github.com/go-git/go-git/v5 v5.12.1-0.20240906142134-9cf0e3ee57dd/go.mod h1:bN6A1YeroE4hsEk6jE8Tk507NxnKZNJLVABgVuChAFg= -github.com/go-git/go-git/v5 v5.12.1-0.20240925075259-8a7ce8143681 h1:2gWyKkIaiSvaBN+THAUg43AyUGz0RLuCUa7mCWTY93g= -github.com/go-git/go-git/v5 v5.12.1-0.20240925075259-8a7ce8143681/go.mod h1:bN6A1YeroE4hsEk6jE8Tk507NxnKZNJLVABgVuChAFg= github.com/go-git/go-git/v5 v5.12.1-0.20240930111449-d1843220b6ab h1:90RNld1ZF+pwfooOog4MslWouh9+IxERrqKxpHbJAdg= github.com/go-git/go-git/v5 v5.12.1-0.20240930111449-d1843220b6ab/go.mod h1:bN6A1YeroE4hsEk6jE8Tk507NxnKZNJLVABgVuChAFg= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= @@ -84,8 +60,10 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -99,11 +77,15 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -116,28 +98,57 @@ github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -147,13 +158,15 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= @@ -167,8 +180,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -189,8 +200,6 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -198,8 +207,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -209,8 +216,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -220,10 +227,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mvdan.cc/sh/v3 v3.9.0 h1:it14fyjCdQUk4jf/aYxLO3FG8jFarR9GzMCtnlvvD7c= diff --git a/init.toml b/init.toml index f4458fe8..1e746a3c 100644 --- a/init.toml +++ b/init.toml @@ -1,5 +1,43 @@ # One CLI to format the code tree - https://github.com/numtide/treefmt +# Do not exit with error if a configured formatter is missing (env $TREEFMT_ALLOW_MISSING_FORMATTER) +# allow-missing-formatter = true + +## Runs treefmt in a CI mode, enabling --no-cache, --fail-on-change and adjusting some other settings best suited to a +## CI use case (env $TREEFMT_CI) +# ci = true + +# The file into which a cpu profile will be written (env $TREEFMT_CPU_PROFILE) +# cpu-profile = true + +# Exclude files or directories matching the specified globs (env $TREEFMT_EXCLUDES) +# excludes = ["*.md", "*.gif"] + +# Exit with error if any changes were made. Useful for CI (env $TREEFMT_FAIL_ON_CHANGE) +# fail-on-change = true + +# Specify formatters to apply. Defaults to all configured formatters. (env $TREEFMT_FORMATTERS) +# formatters = ["gofmt", "prettier"] + +# Log paths that did not match any formatters at the specified log level. +# Possible values are . (env $TREEFMT_ON_UNMATCHED) +# on-unmatched = "info" + +# The root directory from which treefmt will start walking the filesystem (defaults to the directory containing the +# config file). (env $TREEFMT_TREE_ROOT) +# tree-root = "/tmp/foo" + +# File to search for to find the tree root (if --tree-root is not passed). (env $TREEFMT_TREE_ROOT_FILE) +# tree-root-file = ".git/config" + +# Set the verbosity of logs e.g. +# 0 = warn, 1 = info, 2 = debug (env $TREEFMT_VERBOSE) +# verbose = 2 + +# The method used to traverse the files within the tree root. +# Currently supports 'auto', 'git' or 'filesystem'. (env $TREEFMT_WALK) +# walk = "filesystem" + [formatter.mylanguage] # Formatter to run command = "command-to-run" diff --git a/main.go b/main.go index c3f8ed33..1e6f0732 100644 --- a/main.go +++ b/main.go @@ -1,40 +1,14 @@ package main import ( - _ "embed" - "fmt" "os" - "github.com/alecthomas/kong" - "github.com/numtide/treefmt/build" - "github.com/numtide/treefmt/cli" + "github.com/numtide/treefmt/cmd" ) -// We embed the sample toml file for use with the init flag. -// -//go:embed init.toml -var initBytes []byte - 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 - } else if arg == "--init" || arg == "-i" { - if err := os.WriteFile("treefmt.toml", initBytes, 0o644); err != nil { - fmt.Printf("Failed to write treefmt.toml: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Generated treefmt.toml. Now it's your turn to edit it.\n") - return - } + // todo how are exit codes thrown by commands? + if err := cmd.NewRoot().Execute(); err != nil { + os.Exit(1) } - - ctx := kong.Parse(cli.New(), cli.NewOptions()...) - ctx.FatalIfErrorf(ctx.Run()) } diff --git a/nix/devshells/default.nix b/nix/devshells/default.nix index 7d12631e..f9034d8f 100644 --- a/nix/devshells/default.nix +++ b/nix/devshells/default.nix @@ -13,13 +13,16 @@ perSystem.self.treefmt.overrideAttrs (old: { nativeBuildInputs = old.nativeBuildInputs - ++ [ - pkgs.goreleaser - pkgs.golangci-lint - pkgs.delve - pkgs.pprof - pkgs.graphviz - ] + ++ (with pkgs; [ + goreleaser + golangci-lint + delve + pprof + graphviz + cobra-cli + enumer + perSystem.gomod2nix.default + ]) ++ # include formatters for development and testing (import ../packages/treefmt/formatters.nix pkgs); diff --git a/nix/packages/treefmt/gomod2nix.toml b/nix/packages/treefmt/gomod2nix.toml index 0a72186c..9e07d269 100644 --- a/nix/packages/treefmt/gomod2nix.toml +++ b/nix/packages/treefmt/gomod2nix.toml @@ -1,4 +1,3 @@ -# Generated with `nix develop .#renovate -c gomod2nix:update` schema = 3 [mod] @@ -36,11 +35,14 @@ schema = 3 version = "v0.2.5" hash = "sha256-Hb9fRUHnMJJwy7XuHRG2l0YiTKh/5jUz2YJVdYScIfE=" [mod."github.com/davecgh/go-spew"] - version = "v1.1.1" - hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI=" + version = "v1.1.2-0.20180830191138-d8f796af33cc" + hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc=" [mod."github.com/emirpasic/gods"] version = "v1.18.1" hash = "sha256-hGDKddjLj+5dn2woHtXKUdd49/3xdsqnhx7VEdCu1m4=" + [mod."github.com/fsnotify/fsnotify"] + version = "v1.7.0" + hash = "sha256-MdT2rQyQHspPJcx6n9ozkLbsktIOJutOqDuKpNAtoZY=" [mod."github.com/go-git/gcfg"] version = "v1.5.1-0.20230307220236-3a3c6141e376" hash = "sha256-f4k0gSYuo0/q3WOoTxl2eFaj7WZpdz29ih6CKc8Ude8=" @@ -59,6 +61,12 @@ schema = 3 [mod."github.com/golang/groupcache"] version = "v0.0.0-20210331224755-41bb18bfe9da" hash = "sha256-7Gs7CS9gEYZkbu5P4hqPGBpeGZWC64VDwraSKFF+VR0=" + [mod."github.com/hashicorp/hcl"] + version = "v1.0.0" + hash = "sha256-xsRCmYyBfglMxeWUvTZqkaRLSW+V2FvNodEDjTGg1WA=" + [mod."github.com/inconshreveable/mousetrap"] + version = "v1.1.0" + hash = "sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE=" [mod."github.com/jbenet/go-context"] version = "v0.0.0-20150711004518-d14ea06fba99" hash = "sha256-VANNCWNNpARH/ILQV9sCQsBWgyL2iFT+4AHZREpxIWE=" @@ -68,12 +76,18 @@ schema = 3 [mod."github.com/lucasb-eyer/go-colorful"] version = "v1.2.0" hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" + [mod."github.com/magiconair/properties"] + version = "v1.8.7" + hash = "sha256-XQ2bnc2s7/IH3WxEO4GishZurMyKwEclZy1DXg+2xXc=" [mod."github.com/mattn/go-isatty"] version = "v0.0.20" hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" [mod."github.com/mattn/go-runewidth"] version = "v0.0.15" hash = "sha256-WP39EU2UrQbByYfnwrkBDoKN7xzXsBssDq3pNryBGm0=" + [mod."github.com/mitchellh/mapstructure"] + version = "v1.5.0" + hash = "sha256-ztVhGQXs67MF8UadVvG72G3ly0ypQW0IRDdOOkjYwoE=" [mod."github.com/muesli/cancelreader"] version = "v0.2.2" hash = "sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ=" @@ -86,24 +100,54 @@ schema = 3 [mod."github.com/otiai10/copy"] version = "v1.14.0" hash = "sha256-xsaL1ddkPS544y0Jv7u/INUALBYmYq29ddWvysLXk4A=" + [mod."github.com/pelletier/go-toml/v2"] + version = "v2.2.2" + hash = "sha256-ukxk1Cfm6cQW18g/aa19tLcUu5BnF7VmfAvrDHAOl6A=" [mod."github.com/pjbgf/sha1cd"] version = "v0.3.0" hash = "sha256-kX9BdLh2dxtGNaDvc24NORO+C0AZ7JzbrXrtecCdB7w=" [mod."github.com/pmezard/go-difflib"] - version = "v1.0.0" - hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA=" + version = "v1.0.1-0.20181226105442-5d4384ee4fb2" + hash = "sha256-XA4Oj1gdmdV/F/+8kMI+DBxKPthZ768hbKsO3d9Gx90=" [mod."github.com/rivo/uniseg"] version = "v0.4.7" hash = "sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo=" + [mod."github.com/sagikazarmark/locafero"] + version = "v0.4.0" + hash = "sha256-7I1Oatc7GAaHgAqBFO6Tv4IbzFiYeU9bJAfJhXuWaXk=" + [mod."github.com/sagikazarmark/slog-shim"] + version = "v0.1.0" + hash = "sha256-F92BQXXmn3mCwu3mBaGh+joTRItQDSDhsjU6SofkYdA=" [mod."github.com/sergi/go-diff"] version = "v1.3.2-0.20230802210424-5b0b94c5c0d3" hash = "sha256-UcLU83CPMbSoKI8RLvLJ7nvGaE2xRSL1RjoHCVkMzUM=" [mod."github.com/skeema/knownhosts"] version = "v1.3.0" hash = "sha256-piR5IdfqxK9nxyErJ+IRDLnkaeNQwX93ztTFZyPm5MQ=" + [mod."github.com/sourcegraph/conc"] + version = "v0.3.0" + hash = "sha256-mIdMs9MLAOBKf3/0quf1iI3v8uNWydy7ae5MFa+F2Ko=" + [mod."github.com/spf13/afero"] + version = "v1.11.0" + hash = "sha256-+rV3cDZr13N8E0rJ7iHmwsKYKH+EhV+IXBut+JbBiIE=" + [mod."github.com/spf13/cast"] + version = "v1.6.0" + hash = "sha256-hxioqRZfXE0AE5099wmn3YG0AZF8Wda2EB4c7zHF6zI=" + [mod."github.com/spf13/cobra"] + version = "v1.8.1" + hash = "sha256-yDF6yAHycV1IZOrt3/hofR+QINe+B2yqkcIaVov3Ky8=" + [mod."github.com/spf13/pflag"] + version = "v1.0.5" + hash = "sha256-w9LLYzxxP74WHT4ouBspH/iQZXjuAh2WQCHsuvyEjAw=" + [mod."github.com/spf13/viper"] + version = "v1.19.0" + hash = "sha256-MZ8EAvdgpGbw6kmUz8UOaAAAMdPPGd14TrCBAY+A1T4=" [mod."github.com/stretchr/testify"] version = "v1.9.0" hash = "sha256-uUp/On+1nK+lARkTVtb5RxlW15zxtw2kaAFuIASA+J0=" + [mod."github.com/subosito/gotenv"] + version = "v1.6.0" + hash = "sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A=" [mod."github.com/vmihailenco/msgpack/v5"] version = "v5.4.1" hash = "sha256-pDplX6xU6UpNLcFbO1pRREW5vCnSPvSU+ojAwFDv3Hk=" @@ -116,6 +160,12 @@ schema = 3 [mod."go.etcd.io/bbolt"] version = "v1.3.11" hash = "sha256-SVWYZtE9TBgAo8xJSmo9DtSwuNa056N3zGvPLDJgiA8=" + [mod."go.uber.org/atomic"] + version = "v1.9.0" + hash = "sha256-D8OtLaViqPShz1w8ijhIHmjw9xVaRu0qD2hXKj63r4Q=" + [mod."go.uber.org/multierr"] + version = "v1.9.0" + hash = "sha256-tlDRooh/V4HDhZohsUrxot/Y6uVInVBtRWCZbj/tPds=" [mod."golang.org/x/crypto"] version = "v0.27.0" hash = "sha256-8HP4+gr4DbXI22GhdgZmCWr1ijtI9HNLsTcE0kltY9o=" @@ -134,6 +184,12 @@ schema = 3 [mod."golang.org/x/term"] version = "v0.24.0" hash = "sha256-PfC5psjzEWKRm1DlnBXX0ntw9OskJFrq1RRjyBa1lOk=" + [mod."golang.org/x/text"] + version = "v0.18.0" + hash = "sha256-aNvJW4gQs+MTfdz6DZqyyHQS2GJ9W8L8qKPVODPn4+k=" + [mod."gopkg.in/ini.v1"] + version = "v1.67.0" + hash = "sha256-V10ahGNGT+NLRdKUyRg1dos5RxLBXBk1xutcnquc/+4=" [mod."gopkg.in/warnings.v0"] version = "v0.1.2" hash = "sha256-ATVL9yEmgYbkJ1DkltDGRn/auGAjqGOfjQyBYyUo8s8=" diff --git a/test/examples/treefmt.toml b/test/examples/treefmt.toml index 7c171939..e70f30fc 100644 --- a/test/examples/treefmt.toml +++ b/test/examples/treefmt.toml @@ -1,6 +1,5 @@ # One CLI to format the code tree - https://github.com/numtide/treefmt -[global] excludes = ["*.toml"] [formatter.python] diff --git a/test/temp.go b/test/temp.go index 59a2bfaf..081dcea8 100644 --- a/test/temp.go +++ b/test/temp.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func WriteConfig(t *testing.T, path string, cfg config.Config) { +func WriteConfig(t *testing.T, path string, cfg *config.Config) { t.Helper() f, err := os.Create(path) if err != nil { diff --git a/walk/type_enum.go b/walk/type_enum.go new file mode 100644 index 00000000..f888fee8 --- /dev/null +++ b/walk/type_enum.go @@ -0,0 +1,94 @@ +// Code generated by "enumer -type=Type -text -transform=snake -output=./type_enum.go"; DO NOT EDIT. + +package walk + +import ( + "fmt" + "strings" +) + +const _TypeName = "autogitfilesystem" + +var _TypeIndex = [...]uint8{0, 4, 7, 17} + +const _TypeLowerName = "autogitfilesystem" + +func (i Type) String() string { + if i < 0 || i >= Type(len(_TypeIndex)-1) { + return fmt.Sprintf("Type(%d)", i) + } + return _TypeName[_TypeIndex[i]:_TypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _TypeNoOp() { + var x [1]struct{} + _ = x[Auto-(0)] + _ = x[Git-(1)] + _ = x[Filesystem-(2)] +} + +var _TypeValues = []Type{Auto, Git, Filesystem} + +var _TypeNameToValueMap = map[string]Type{ + _TypeName[0:4]: Auto, + _TypeLowerName[0:4]: Auto, + _TypeName[4:7]: Git, + _TypeLowerName[4:7]: Git, + _TypeName[7:17]: Filesystem, + _TypeLowerName[7:17]: Filesystem, +} + +var _TypeNames = []string{ + _TypeName[0:4], + _TypeName[4:7], + _TypeName[7:17], +} + +// TypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func TypeString(s string) (Type, error) { + if val, ok := _TypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _TypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Type values", s) +} + +// TypeValues returns all values of the enum +func TypeValues() []Type { + return _TypeValues +} + +// TypeStrings returns a slice of all String values of the enum +func TypeStrings() []string { + strs := make([]string, len(_TypeNames)) + copy(strs, _TypeNames) + return strs +} + +// IsAType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Type) IsAType() bool { + for _, v := range _TypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for Type +func (i Type) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for Type +func (i *Type) UnmarshalText(text []byte) error { + var err error + *i, err = TypeString(string(text)) + return err +} diff --git a/walk/walker.go b/walk/walker.go index e3afb77f..072f21d2 100644 --- a/walk/walker.go +++ b/walk/walker.go @@ -8,12 +8,13 @@ import ( "time" ) -type Type string +//go:generate enumer -type=Type -text -transform=snake -output=./type_enum.go +type Type int const ( - Git Type = "git" - Auto Type = "auto" - Filesystem Type = "filesystem" + Auto Type = iota + Git Type = iota + Filesystem ) type File struct { @@ -35,7 +36,7 @@ func (f File) HasChanged() (bool, fs.FileInfo, error) { } // POSIX specifies EPOCH time for Mod time, but some filesystems give more precision. - // Some formatters mess with the mod time (e.g., dos2unix) but not to the same precision, + // Some formatters mess with the mod time (e.g. dos2unix) but not to the same precision, // triggering false positives. // We truncate everything below a second. if f.Info.ModTime().Truncate(time.Second) != current.ModTime().Truncate(time.Second) {