From bac4a0d102e1142406d3a7d15106e5ba108bfcf8 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 --- 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 | 45 ++ cmd/root.go | 127 ++++++ cli/format_test.go => cmd/root_test.go | 203 +++++++-- config/config.go | 271 +++++++++++- config/config_test.go | 566 ++++++++++++++++++++++++- config/formatter.go | 14 - go.mod | 24 +- go.sum | 98 +++-- init.toml | 11 - 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 +- 21 files changed, 1537 insertions(+), 587 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 delete mode 100644 init.toml create mode 100644 walk/type_enum.go 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..af4bf343 --- /dev/null +++ b/cmd/init/init.toml @@ -0,0 +1,45 @@ +# 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 + +# The file into which a cpu profile will be written (env $TREEFMT_CPU_PROFILE) +# cpu-profile = ./cpu.pprof + +# 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" +# 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..2ea1e775 100644 --- a/config/config.go +++ b/config/config.go @@ -2,32 +2,211 @@ 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{ + "ci": false, + "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"` + Verbose 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 +214,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.Verbose < 1 { + cfg.Verbose = 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..ecbfd3cd 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 *Config, 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 := &Config{} + 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.AllowMissingFormatter = 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 := &Config{} + 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.Verbose) + }) + } + + // default with no flag, env or config + checkValues(false, false, false, 0) + + // set config value and check that it has no effect + // you are not allowed to set ci in config + cfg.CI = true + checkValues(false, false, false, 0) + + // 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 := &Config{} + 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.ClearCache = 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 := &Config{} + 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.CpuProfile = "/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 := &Config{} + 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 + cfg.Excludes = nil + cfg.Global.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 := &Config{} + 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.FailOnChange = 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 := &Config{} + 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.FormatterConfigs = map[string]*Formatter{ + "echo": { + Command: "echo", + }, + "touch": { + Command: "touch", + }, + "date": { + 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 := &Config{} + 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.NoCache = 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 := &Config{} + 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.OnUnmatched = "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 := &Config{} + 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.TreeRoot = "/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 := &Config{} + 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.TreeRootFile = "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 := &Config{} + v, _ := newViper(t) + + checkValue := func(expected uint8) { + readValue(t, v, cfg, func(cfg *Config) { + as.Equal(expected, cfg.Verbose) + }) + } + + // 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 := &Config{} + 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 := &Config{} + 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.WorkingDirectory = "/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 := &Config{} + 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 deleted file mode 100644 index f4458fe8..00000000 --- a/init.toml +++ /dev/null @@ -1,11 +0,0 @@ -# One CLI to format the code tree - https://github.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/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) {