diff --git a/cli/cli.go b/cli/cli.go index 9ddec900..524b737e 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 a9fe7640..e41fbe50 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() +}