diff --git a/cmd/uninstall.go b/cmd/uninstall.go index e1d67a25..d49c5a6c 100644 --- a/cmd/uninstall.go +++ b/cmd/uninstall.go @@ -29,7 +29,7 @@ func newUninstallCmd(opts *lefthook.Options) *cobra.Command { uninstallCmd.Flags().BoolVarP( &args.RemoveConfig, "remove-configs", "c", false, - "remove lefthook.yml and lefthook-local.yml", + "remove lefthook.yml, lefthook-local.yml, .lefthook.yml, and .lefthook-local.yml", ) return &uninstallCmd diff --git a/internal/config/load.go b/internal/config/load.go index ae9ea4aa..f05f3e17 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -34,13 +34,8 @@ func (err NotFoundError) Error() string { // Loads configs from the given directory with extensions. func Load(fs afero.Fs, repo *git.Repository) (*Config, error) { - global, err := read(fs, repo.RootPath, "lefthook") + global, err := readOne(fs, repo.RootPath, []string{"lefthook", ".lefthook"}) if err != nil { - var notFoundErr viper.ConfigFileNotFoundError - if ok := errors.As(err, ¬FoundErr); ok { - return nil, NotFoundError{err.Error()} - } - return nil, err } @@ -81,9 +76,28 @@ func read(fs afero.Fs, path string, name string) (*viper.Viper, error) { return v, nil } -// mergeAll merges remotes and extends from .lefthook and .lefthook-local. +func readOne(fs afero.Fs, path string, names []string) (*viper.Viper, error) { + for _, name := range names { + v, err := read(fs, path, name) + if err != nil { + var notFoundErr viper.ConfigFileNotFoundError + if ok := errors.As(err, ¬FoundErr); ok { + continue + } else { + return nil, err + } + } + + return v, nil + } + + return nil, NotFoundError{fmt.Sprintf("No config files with names %q could not be found in \"%s\"", names, path)} +} + +// mergeAll merges (.lefthook or lefthook) and (extended config) and (remote) +// and (.lefthook-local or .lefthook-local) configs. func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { - extends, err := read(fs, repo.RootPath, "lefthook") + extends, err := readOne(fs, repo.RootPath, []string{"lefthook", ".lefthook"}) if err != nil { return nil, err } @@ -96,7 +110,7 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { return nil, err } - if err := merge("lefthook-local", "", extends); err == nil { + if err := mergeOne([]string{"lefthook-local", ".lefthook-local"}, "", extends); err == nil { if err = extend(extends, repo.RootPath); err != nil { return nil, err } @@ -173,6 +187,22 @@ func merge(name, path string, v *viper.Viper) error { return nil } +func mergeOne(names []string, path string, v *viper.Viper) error { + for _, name := range names { + err := merge(name, path, v) + if err == nil { + break + } else { + var notFoundErr viper.ConfigFileNotFoundError + if ok := errors.As(err, ¬FoundErr); !ok { + return err + } + } + } + + return nil +} + func unmarshalConfigs(base, extra *viper.Viper, c *Config) error { c.Hooks = make(map[string]*Hook) diff --git a/internal/config/load_test.go b/internal/config/load_test.go index c65ec338..3c37fff6 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -23,9 +23,93 @@ func TestLoad(t *testing.T) { name string global, local, remote string remoteConfigPath string - extends map[string]string + otherFiles map[string]string result *Config }{ + { + name: "with global, dot", + otherFiles: map[string]string{ + ".lefthook.yml": ` +pre-commit: + commands: + tests: + run: yarn test +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Hooks: map[string]*Hook{ + "pre-commit": { + Parallel: false, + Commands: map[string]*Command{ + "tests": { + Run: "yarn test", + }, + }, + }, + }, + }, + }, + { + name: "with global, nodot", + otherFiles: map[string]string{ + "lefthook.yml": ` +pre-commit: + commands: + tests: + run: yarn test +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Hooks: map[string]*Hook{ + "pre-commit": { + Parallel: false, + Commands: map[string]*Command{ + "tests": { + Run: "yarn test", + }, + }, + }, + }, + }, + }, + { + name: "with global, nodot has priority", + otherFiles: map[string]string{ + ".lefthook.yml": ` +pre-commit: + commands: + tests: + run: yarn test1 +`, + "lefthook.yml": ` +pre-commit: + commands: + tests: + run: yarn test2 +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Hooks: map[string]*Hook{ + "pre-commit": { + Parallel: false, + Commands: map[string]*Command{ + "tests": { + Run: "yarn test2", + }, + }, + }, + }, + }, + }, { name: "simple", global: ` @@ -144,6 +228,114 @@ pre-push: }, }, }, + { + name: "with overrides, dot", + otherFiles: map[string]string{ + ".lefthook.yml": ` +pre-push: + scripts: + "global-extend": + runner: bash +`, + ".lefthook-local.yml": ` +pre-push: + scripts: + "local-extend": + runner: bash +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Hooks: map[string]*Hook{ + "pre-push": { + Scripts: map[string]*Script{ + "global-extend": { + Runner: "bash", + }, + "local-extend": { + Runner: "bash", + }, + }, + }, + }, + }, + }, + { + name: "with overrides, dot, nodot", + otherFiles: map[string]string{ + "lefthook.yml": ` +pre-push: + scripts: + "global-extend": + runner: bash +`, + ".lefthook-local.yml": ` +pre-push: + scripts: + "local-extend": + runner: bash +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Hooks: map[string]*Hook{ + "pre-push": { + Scripts: map[string]*Script{ + "global-extend": { + Runner: "bash", + }, + "local-extend": { + Runner: "bash", + }, + }, + }, + }, + }, + }, + { + name: "with overrides, nodot has priority", + otherFiles: map[string]string{ + "lefthook.yml": ` +pre-push: + scripts: + "global-extend": + runner: bash +`, + ".lefthook-local.yml": ` +pre-push: + scripts: + "local-extend": + runner: bash1 +`, + "lefthook-local.yml": ` +pre-push: + scripts: + "local-extend": + runner: bash2 +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Colors: nil, + Hooks: map[string]*Hook{ + "pre-push": { + Scripts: map[string]*Script{ + "global-extend": { + Runner: "bash", + }, + "local-extend": { + Runner: "bash2", + }, + }, + }, + }, + }, + }, { name: "with extra hooks", global: ` @@ -356,7 +548,7 @@ pre-push: run: echo remote `, remoteConfigPath: filepath.Join(root, ".git", "info", "lefthook-remotes", "lefthook", "examples", "config.yml"), - extends: map[string]string{ + otherFiles: map[string]string{ "global-extend.yml": ` pre-push: scripts: @@ -422,12 +614,16 @@ pre-push: } t.Run(fmt.Sprintf("%d: %s", i, tt.name), func(t *testing.T) { - if err := fs.WriteFile(filepath.Join(root, "lefthook.yml"), []byte(tt.global), 0o644); err != nil { - t.Errorf("unexpected error: %s", err) + if tt.global != "" { + if err := fs.WriteFile(filepath.Join(root, "lefthook.yml"), []byte(tt.global), 0o644); err != nil { + t.Errorf("unexpected error: %s", err) + } } - if err := fs.WriteFile(filepath.Join(root, "lefthook-local.yml"), []byte(tt.local), 0o644); err != nil { - t.Errorf("unexpected error: %s", err) + if tt.local != "" { + if err := fs.WriteFile(filepath.Join(root, "lefthook-local.yml"), []byte(tt.local), 0o644); err != nil { + t.Errorf("unexpected error: %s", err) + } } if len(tt.remoteConfigPath) > 0 { @@ -440,7 +636,7 @@ pre-push: } } - for name, content := range tt.extends { + for name, content := range tt.otherFiles { path := filepath.Join( root, filepath.Join(strings.Split(name, "/")...), diff --git a/internal/lefthook/install.go b/internal/lefthook/install.go index 32504ab2..c2a0896a 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -30,7 +30,7 @@ const ( var ( lefthookChecksumRegexp = regexp.MustCompile(`(\w+)\s+(\d+)`) - configGlob = glob.MustCompile("lefthook.{yml,yaml,json,toml}") + configGlob = glob.MustCompile("{.,}lefthook.{yml,yaml,json,toml}") errNoConfig = fmt.Errorf("no lefthook config found") ) diff --git a/internal/lefthook/uninstall.go b/internal/lefthook/uninstall.go index 21e8aa43..b90322cd 100644 --- a/internal/lefthook/uninstall.go +++ b/internal/lefthook/uninstall.go @@ -34,7 +34,9 @@ func (l *Lefthook) Uninstall(args *UninstallArgs) error { if args.RemoveConfig { for _, glob := range []string{ + ".lefthook.y*ml", "lefthook.y*ml", + ".lefthook-local.y*ml", "lefthook-local.y*ml", } { l.removeFile(filepath.Join(l.repo.RootPath, glob))