From bd32d36a33f3eca49000402c91112ef8956d30f8 Mon Sep 17 00:00:00 2001 From: zimbatm Date: Thu, 30 May 2024 15:43:26 +0200 Subject: [PATCH] cli: search for the tree root by default Restore the treefmt 1.x behaviour where it would search for the tree root by recursively searching for the treefmt.toml file up the filesystem, starting from the current directory. The `--tree-root-file` option will be useful to remove this bash wrapper: https://github.com/numtide/treefmt-nix/blob/2fba33a182602b9d49f0b2440513e5ee091d838b/module-options.nix#L116-L135 Fixes #308 --- cli/cli.go | 5 +-- cli/format.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 9ddec90..524b737 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -13,10 +13,11 @@ type Format struct { 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" default:"./treefmt.toml" help:"The config file to use."` + ConfigFile string `type:"path" help:"Load the config file from the given path (defaults to finding treefmt.toml up)."` 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" default:"." help:"The root directory from which treefmt will start walking the filesystem."` + TreeRoot string `type:"path" help:"The root directory from which treefmt will start walking the filesystem (defaults to the directory containing the config file)."` + TreeRootFile string `type:"path" 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."` diff --git a/cli/format.go b/cli/format.go index a9fe764..e41fbe5 100644 --- a/cli/format.go +++ b/cli/format.go @@ -69,6 +69,40 @@ func (f *Format) Run() (err error) { } }() + // find the config file unless specified + if Cli.ConfigFile == "" { + pwd, err := os.Getwd() + if err != nil { + return err + } + Cli.ConfigFile, _, err = findUp(pwd, "treefmt.toml") + if err != nil { + return err + } + } + + // search for the project root unless specified + if Cli.TreeRoot == "" { + // use the location of the treefmt.toml file by default + dir := filepath.Dir(Cli.ConfigFile) + + // search using the --tree-root-file if specified + if Cli.TreeRootFile != "" { + pwd, err := os.Getwd() + if err != nil { + return err + } + _, dir, err = findUp(pwd, Cli.TreeRootFile) + if err != nil { + return err + } + } + + Cli.TreeRoot = dir + } + + log.Debugf("config-file=%s tree-root=%s", Cli.ConfigFile, Cli.TreeRoot) + // read config cfg, err := config.ReadFile(Cli.ConfigFile, Cli.Formatters) if err != nil { @@ -384,3 +418,56 @@ func applyFormatters(ctx context.Context) func() error { return nil } } + +func findUp(searchDir string, fileName string) (path string, dir string, err error) { + for _, dir := range eachDir(searchDir) { + path := filepath.Join(dir, fileName) + if fileExists(path) { + return path, dir, nil + } + } + return "", "", fmt.Errorf("could not find %s in %s", fileName, 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() +}