feat: create config package

Move all config related code into a config package.

Signed-off-by: Brian McGee <brian@bmcgee.ie>
This commit is contained in:
Brian McGee 2024-01-12 20:12:30 +00:00
parent 15db7f459c
commit 3cbe494426
Signed by: brianmcgee
GPG Key ID: D49016E76AD1E8C0
7 changed files with 92 additions and 83 deletions

View File

@ -10,6 +10,8 @@ import (
"syscall" "syscall"
"time" "time"
"git.numtide.com/numtide/treefmt/internal/config"
"git.numtide.com/numtide/treefmt/internal/cache" "git.numtide.com/numtide/treefmt/internal/cache"
"git.numtide.com/numtide/treefmt/internal/format" "git.numtide.com/numtide/treefmt/internal/format"
@ -39,7 +41,7 @@ func (f *Format) Run() error {
defer cancel() defer cancel()
// read config // read config
cfg, err := format.ReadConfigFile(Cli.ConfigFile) cfg, err := config.ReadFile(Cli.ConfigFile)
if err != nil { if err != nil {
return fmt.Errorf("%w: failed to read config file", err) return fmt.Errorf("%w: failed to read config file", err)
} }
@ -68,8 +70,8 @@ func (f *Format) Run() error {
formatters := make(map[string]*format.Formatter) formatters := make(map[string]*format.Formatter)
// detect broken dependencies // detect broken dependencies
for name, config := range cfg.Formatters { for name, formatterCfg := range cfg.Formatters {
before := config.Before before := formatterCfg.Before
if before != "" { if before != "" {
// check child formatter exists // check child formatter exists
_, ok := cfg.Formatters[before] _, ok := cfg.Formatters[before]
@ -80,7 +82,7 @@ func (f *Format) Run() error {
} }
// dependency cycle detection // dependency cycle detection
for name, config := range cfg.Formatters { for name, formatterCfg := range cfg.Formatters {
var ok bool var ok bool
var history []string var history []string
childName := name childName := name
@ -88,23 +90,23 @@ func (f *Format) Run() error {
// add to history // add to history
history = append(history, childName) history = append(history, childName)
if config.Before == "" { if formatterCfg.Before == "" {
break break
} else if config.Before == name { } else if formatterCfg.Before == name {
return fmt.Errorf("formatter cycle detected %v", strings.Join(history, " -> ")) return fmt.Errorf("formatter cycle detected %v", strings.Join(history, " -> "))
} }
// load child config // load child config
childName = config.Before childName = formatterCfg.Before
config, ok = cfg.Formatters[config.Before] formatterCfg, ok = cfg.Formatters[formatterCfg.Before]
if !ok { if !ok {
return fmt.Errorf("formatter not found: %v", config.Before) return fmt.Errorf("formatter not found: %v", formatterCfg.Before)
} }
} }
} }
// init formatters // init formatters
for name, config := range cfg.Formatters { for name, formatterCfg := range cfg.Formatters {
if !includeFormatter(name) { if !includeFormatter(name) {
// remove this formatter // remove this formatter
delete(cfg.Formatters, name) delete(cfg.Formatters, name)
@ -112,8 +114,8 @@ func (f *Format) Run() error {
continue continue
} }
formatter, err := format.NewFormatter(name, config, globalExcludes) formatter, err := format.NewFormatter(name, formatterCfg, globalExcludes)
if errors.Is(err, format.ErrFormatterNotFound) && Cli.AllowMissingFormatter { if errors.Is(err, format.ErrCommandNotFound) && Cli.AllowMissingFormatter {
l.Debugf("formatter not found: %v", name) l.Debugf("formatter not found: %v", name)
continue continue
} else if err != nil { } else if err != nil {

View File

@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"git.numtide.com/numtide/treefmt/internal/config"
"git.numtide.com/numtide/treefmt/internal/test" "git.numtide.com/numtide/treefmt/internal/test"
"github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@ -24,8 +26,8 @@ func TestAllowMissingFormatter(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
configPath := tempDir + "/treefmt.toml" configPath := tempDir + "/treefmt.toml"
test.WriteConfig(t, configPath, format.Config{ test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"foo-fmt": { "foo-fmt": {
Command: "foo-fmt", Command: "foo-fmt",
}, },
@ -33,7 +35,7 @@ func TestAllowMissingFormatter(t *testing.T) {
}) })
_, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) _, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.ErrorIs(err, format.ErrFormatterNotFound) as.ErrorIs(err, format.ErrCommandNotFound)
_, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter") _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter")
as.NoError(err) as.NoError(err)
@ -45,8 +47,8 @@ func TestDependencyCycle(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
configPath := tempDir + "/treefmt.toml" configPath := tempDir + "/treefmt.toml"
test.WriteConfig(t, configPath, format.Config{ test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"a": {Command: "echo", Before: "b"}, "a": {Command: "echo", Before: "b"},
"b": {Command: "echo", Before: "c"}, "b": {Command: "echo", Before: "c"},
"c": {Command: "echo", Before: "a"}, "c": {Command: "echo", Before: "a"},
@ -66,8 +68,8 @@ func TestSpecifyingFormatters(t *testing.T) {
tempDir := test.TempExamples(t) tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml" configPath := tempDir + "/treefmt.toml"
test.WriteConfig(t, configPath, format.Config{ test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"elm": { "elm": {
Command: "echo", Command: "echo",
Includes: []string{"*.elm"}, Includes: []string{"*.elm"},
@ -115,8 +117,8 @@ func TestIncludesAndExcludes(t *testing.T) {
configPath := tempDir + "/echo.toml" configPath := tempDir + "/echo.toml"
// test without any excludes // test without any excludes
config := format.Config{ cfg := config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"echo": { "echo": {
Command: "echo", Command: "echo",
Includes: []string{"*"}, Includes: []string{"*"},
@ -124,33 +126,33 @@ func TestIncludesAndExcludes(t *testing.T) {
}, },
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) as.Contains(string(out), fmt.Sprintf("%d files changed", 29))
// globally exclude nix files // globally exclude nix files
config.Global.Excludes = []string{"*.nix"} cfg.Global.Excludes = []string{"*.nix"}
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 28)) as.Contains(string(out), fmt.Sprintf("%d files changed", 28))
// add haskell files to the global exclude // add haskell files to the global exclude
config.Global.Excludes = []string{"*.nix", "*.hs"} cfg.Global.Excludes = []string{"*.nix", "*.hs"}
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 22)) as.Contains(string(out), fmt.Sprintf("%d files changed", 22))
echo := config.Formatters["echo"] echo := cfg.Formatters["echo"]
// remove python files from the echo formatter // remove python files from the echo formatter
echo.Excludes = []string{"*.py"} echo.Excludes = []string{"*.py"}
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 20)) as.Contains(string(out), fmt.Sprintf("%d files changed", 20))
@ -158,7 +160,7 @@ func TestIncludesAndExcludes(t *testing.T) {
// remove go files from the echo formatter // remove go files from the echo formatter
echo.Excludes = []string{"*.py", "*.go"} echo.Excludes = []string{"*.py", "*.go"}
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 19)) as.Contains(string(out), fmt.Sprintf("%d files changed", 19))
@ -166,7 +168,7 @@ func TestIncludesAndExcludes(t *testing.T) {
// adjust the includes for echo to only include elm files // adjust the includes for echo to only include elm files
echo.Includes = []string{"*.elm"} echo.Includes = []string{"*.elm"}
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 1)) as.Contains(string(out), fmt.Sprintf("%d files changed", 1))
@ -174,7 +176,7 @@ func TestIncludesAndExcludes(t *testing.T) {
// add js files to echo formatter // add js files to echo formatter
echo.Includes = []string{"*.elm", "*.js"} echo.Includes = []string{"*.elm", "*.js"}
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) out, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 2)) as.Contains(string(out), fmt.Sprintf("%d files changed", 2))
@ -187,8 +189,8 @@ func TestCache(t *testing.T) {
configPath := tempDir + "/echo.toml" configPath := tempDir + "/echo.toml"
// test without any excludes // test without any excludes
config := format.Config{ cfg := config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"echo": { "echo": {
Command: "echo", Command: "echo",
Includes: []string{"*"}, Includes: []string{"*"},
@ -196,7 +198,7 @@ func TestCache(t *testing.T) {
}, },
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) out, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir)
as.NoError(err) as.NoError(err)
as.Contains(string(out), fmt.Sprintf("%d files changed", 29)) as.Contains(string(out), fmt.Sprintf("%d files changed", 29))
@ -222,8 +224,8 @@ func TestChangeWorkingDirectory(t *testing.T) {
configPath := tempDir + "/treefmt.toml" configPath := tempDir + "/treefmt.toml"
// test without any excludes // test without any excludes
config := format.Config{ cfg := config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"echo": { "echo": {
Command: "echo", Command: "echo",
Includes: []string{"*"}, Includes: []string{"*"},
@ -231,7 +233,7 @@ func TestChangeWorkingDirectory(t *testing.T) {
}, },
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
// by default, we look for ./treefmt.toml and use the cwd for the tree root // by default, we look for ./treefmt.toml and use the cwd for the tree root
// this should fail if the working directory hasn't been changed first // this should fail if the working directory hasn't been changed first
@ -247,8 +249,8 @@ func TestFailOnChange(t *testing.T) {
configPath := tempDir + "/echo.toml" configPath := tempDir + "/echo.toml"
// test without any excludes // test without any excludes
config := format.Config{ cfg := config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"echo": { "echo": {
Command: "echo", Command: "echo",
Includes: []string{"*"}, Includes: []string{"*"},
@ -256,7 +258,7 @@ func TestFailOnChange(t *testing.T) {
}, },
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
_, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir) _, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir)
as.ErrorIs(err, ErrFailOnChange) as.ErrorIs(err, ErrFailOnChange)
} }
@ -283,8 +285,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH"))) as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))
// start with 2 formatters // start with 2 formatters
config := format.Config{ cfg := config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"python": { "python": {
Command: "black", Command: "black",
Includes: []string{"*.py"}, Includes: []string{"*.py"},
@ -297,7 +299,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
}, },
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
args := []string{"--config-file", configPath, "--tree-root", tempDir} args := []string{"--config-file", configPath, "--tree-root", tempDir}
out, err := cmd(t, args...) out, err := cmd(t, args...)
as.NoError(err) as.NoError(err)
@ -328,12 +330,12 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
as.Contains(string(out), "0 files changed") as.Contains(string(out), "0 files changed")
// add go formatter // add go formatter
config.Formatters["go"] = &format.FormatterConfig{ cfg.Formatters["go"] = &config.Formatter{
Command: "gofmt", Command: "gofmt",
Options: []string{"-w"}, Options: []string{"-w"},
Includes: []string{"*.go"}, Includes: []string{"*.go"},
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
@ -345,8 +347,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
as.Contains(string(out), "0 files changed") as.Contains(string(out), "0 files changed")
// remove python formatter // remove python formatter
delete(config.Formatters, "python") delete(cfg.Formatters, "python")
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
@ -358,8 +360,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
as.Contains(string(out), "0 files changed") as.Contains(string(out), "0 files changed")
// remove elm formatter // remove elm formatter
delete(config.Formatters, "elm") delete(cfg.Formatters, "elm")
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
out, err = cmd(t, args...) out, err = cmd(t, args...)
as.NoError(err) as.NoError(err)
@ -378,15 +380,15 @@ func TestGitWorktree(t *testing.T) {
configPath := filepath.Join(tempDir, "/treefmt.toml") configPath := filepath.Join(tempDir, "/treefmt.toml")
// basic config // basic config
config := format.Config{ cfg := config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"echo": { "echo": {
Command: "echo", Command: "echo",
Includes: []string{"*"}, Includes: []string{"*"},
}, },
}, },
} }
test.WriteConfig(t, configPath, config) test.WriteConfig(t, configPath, cfg)
// init a git repo // init a git repo
repo, err := git.Init( repo, err := git.Init(
@ -433,8 +435,8 @@ func TestOrderingFormatters(t *testing.T) {
configPath := path.Join(tempDir, "treefmt.toml") configPath := path.Join(tempDir, "treefmt.toml")
// missing child // missing child
test.WriteConfig(t, configPath, format.Config{ test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"hs-a": { "hs-a": {
Command: "echo", Command: "echo",
Includes: []string{"*.hs"}, Includes: []string{"*.hs"},
@ -447,8 +449,8 @@ func TestOrderingFormatters(t *testing.T) {
as.ErrorContains(err, "formatter hs-a is before hs-b but config for hs-b was not found") as.ErrorContains(err, "formatter hs-a is before hs-b but config for hs-b was not found")
// multiple roots // multiple roots
test.WriteConfig(t, configPath, format.Config{ test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*format.FormatterConfig{ Formatters: map[string]*config.Formatter{
"hs-a": { "hs-a": {
Command: "echo", Command: "echo",
Includes: []string{"*.hs"}, Includes: []string{"*.hs"},

View File

@ -1,4 +1,4 @@
package format package config
import "github.com/BurntSushi/toml" import "github.com/BurntSushi/toml"
@ -8,11 +8,11 @@ type Config struct {
// Excludes is an optional list of glob patterns used to exclude certain files from all formatters. // Excludes is an optional list of glob patterns used to exclude certain files from all formatters.
Excludes []string Excludes []string
} }
Formatters map[string]*FormatterConfig `toml:"formatter"` Formatters map[string]*Formatter `toml:"formatter"`
} }
// ReadConfigFile reads from path and unmarshals toml into a Config instance. // ReadFile reads from path and unmarshals toml into a Config instance.
func ReadConfigFile(path string) (cfg *Config, err error) { func ReadFile(path string) (cfg *Config, err error) {
_, err = toml.DecodeFile(path, &cfg) _, err = toml.DecodeFile(path, &cfg)
return return
} }

View File

@ -1,4 +1,4 @@
package format package config
import ( import (
"testing" "testing"
@ -9,7 +9,7 @@ import (
func TestReadConfigFile(t *testing.T) { func TestReadConfigFile(t *testing.T) {
as := require.New(t) as := require.New(t)
cfg, err := ReadConfigFile("../../test/treefmt.toml") cfg, err := ReadFile("../../test/treefmt.toml")
as.NoError(err, "failed to read config file") as.NoError(err, "failed to read config file")
as.NotNil(cfg) as.NotNil(cfg)

View File

@ -0,0 +1,14 @@
package config
type Formatter struct {
// Command is the command invoke when applying this Formatter.
Command string
// Options are an optional list of args to be passed to Command.
Options []string
// Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path.
Includes []string
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.
Excludes []string
// Before is the name of another formatter which must process a path after this one
Before string
}

View File

@ -7,37 +7,27 @@ import (
"os/exec" "os/exec"
"time" "time"
"git.numtide.com/numtide/treefmt/internal/config"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gobwas/glob" "github.com/gobwas/glob"
) )
// ErrFormatterNotFound is returned when the Command for a Formatter is not available. // ErrCommandNotFound is returned when the Command for a Formatter is not available.
var ErrFormatterNotFound = errors.New("formatter not found") var ErrCommandNotFound = errors.New("formatter command not found in PATH")
type FormatterConfig struct {
// Command is the command invoke when applying this Formatter.
Command string
// Options are an optional list of args to be passed to Command.
Options []string
// Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path.
Includes []string
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.
Excludes []string
// Before is the name of another formatter which must process a path after this one
Before string
}
// Formatter represents a command which should be applied to a filesystem. // Formatter represents a command which should be applied to a filesystem.
type Formatter struct { type Formatter struct {
name string name string
config *FormatterConfig config *config.Formatter
log *log.Logger log *log.Logger
executable string // path to the executable described by Command executable string // path to the executable described by Command
before string before string
parent *Formatter
child *Formatter child *Formatter
parent *Formatter
// internal compiled versions of Includes and Excludes. // internal compiled versions of Includes and Excludes.
includes []glob.Glob includes []glob.Glob
@ -68,7 +58,7 @@ func (f *Formatter) Executable() string {
} }
// NewFormatter is used to create a new Formatter. // NewFormatter is used to create a new Formatter.
func NewFormatter(name string, config *FormatterConfig, globalExcludes []glob.Glob) (*Formatter, error) { func NewFormatter(name string, config *config.Formatter, globalExcludes []glob.Glob) (*Formatter, error) {
var err error var err error
f := Formatter{} f := Formatter{}
@ -80,7 +70,7 @@ func NewFormatter(name string, config *FormatterConfig, globalExcludes []glob.Gl
// test if the formatter is available // test if the formatter is available
executable, err := exec.LookPath(config.Command) executable, err := exec.LookPath(config.Command)
if errors.Is(err, exec.ErrNotFound) { if errors.Is(err, exec.ErrNotFound) {
return nil, ErrFormatterNotFound return nil, ErrCommandNotFound
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }

View File

@ -4,13 +4,14 @@ import (
"os" "os"
"testing" "testing"
"git.numtide.com/numtide/treefmt/internal/format" "git.numtide.com/numtide/treefmt/internal/config"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
cp "github.com/otiai10/copy" cp "github.com/otiai10/copy"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func WriteConfig(t *testing.T, path string, cfg format.Config) { func WriteConfig(t *testing.T, path string, cfg config.Config) {
t.Helper() t.Helper()
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {