diff --git a/internal/config/load.go b/internal/config/load.go index b8dd373b..aa03aa43 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -63,21 +63,27 @@ func Load(fs afero.Fs, repo *git.Repository) (*Config, error) { } func read(fs afero.Fs, path string, name string) (*viper.Viper, error) { + v := newViper(fs, path) + v.SetConfigName(name) + + if err := v.ReadInConfig(); err != nil { + return nil, err + } + + return v, nil +} + +func newViper(fs afero.Fs, path string) *viper.Viper { v := viper.New() v.SetFs(fs) v.AddConfigPath(path) - v.SetConfigName(name) // Allow overwriting settings with ENV variables v.SetEnvPrefix("LEFTHOOK") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() - if err := v.ReadInConfig(); err != nil { - return nil, err - } - - return v, nil + return v } func readOne(fs afero.Fs, path string, names []string) (*viper.Viper, error) { @@ -109,7 +115,7 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { return nil, err } - if err := extend(extends, repo.RootPath); err != nil { + if err := extend(fs, extends, repo.RootPath); err != nil { return nil, err } @@ -125,7 +131,7 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { // Local extends need to be re-applied only if they have different settings localExtends := extends.GetStringSlice("extends") if !slices.Equal(globalExtends, localExtends) { - if err = extend(extends, repo.RootPath); err != nil { + if err = extend(fs, extends, repo.RootPath); err != nil { return nil, err } } @@ -190,7 +196,7 @@ func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { return err } - if err = extend(v, filepath.Dir(configPath)); err != nil { + if err = extend(fs, v, filepath.Dir(configPath)); err != nil { return err } } @@ -206,12 +212,34 @@ func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { } // extend merges all files listed in 'extends' option into the config. -func extend(v *viper.Viper, root string) error { - for i, path := range v.GetStringSlice("extends") { +func extend(fs afero.Fs, v *viper.Viper, root string) error { + return extendRecursive(fs, v, root, make(map[string]struct{})) +} + +// extendRecursive merges extends. +// If extends contain other extends they get merged too. +func extendRecursive(fs afero.Fs, v *viper.Viper, root string, extends map[string]struct{}) error { + for _, path := range v.GetStringSlice("extends") { + if _, contains := extends[path]; contains { + return fmt.Errorf("possible recursion in extends: path %s is specified multiple times", path) + } + extends[path] = struct{}{} + if !filepath.IsAbs(path) { path = filepath.Join(root, path) } - if err := merge(fmt.Sprintf("extend_%d", i), path, v); err != nil { + + extendV := newViper(fs, root) + extendV.SetConfigFile(path) + if err := extendV.ReadInConfig(); err != nil { + return err + } + + if err := extendRecursive(fs, extendV, root, extends); err != nil { + return err + } + + if err := v.MergeConfigMap(extendV.AllSettings()); err != nil { return err } } @@ -222,9 +250,7 @@ func extend(v *viper.Viper, root string) error { // merge merges the configuration using viper builtin MergeInConfig. func merge(name, path string, v *viper.Viper) error { v.SetConfigName(name) - if len(path) > 0 { - v.SetConfigFile(path) - } + v.SetConfigFile(path) return v.MergeInConfig() } diff --git a/testdata/many_extends_levels.txt b/testdata/many_extends_levels.txt new file mode 100644 index 00000000..1edc6c58 --- /dev/null +++ b/testdata/many_extends_levels.txt @@ -0,0 +1,80 @@ +[windows] skip + +exec git init +exec lefthook dump +cmp stdout dump.yml +! stderr . + +-- lefthook.yml -- +extends: + - extends/e1.yml + +pre-commit: + commands: + echo: + run: echo 0 + +-- extends/e1.yml -- +extends: + - extends/e2.yml + +pre-commit: + commands: + echo: + run: echo 1 + skip: true + +e1: + commands: + echo: + run: e1 + +-- extends/e2.yml -- +extends: + - extends/e3.yml + +pre-commit: + commands: + echo: + run: echo 2 + tags: ["backend"] + +e2: + commands: + echo: + run: e2 + +-- extends/e3.yml -- +pre-commit: + commands: + echo: + glob: 3 + +e3: + commands: + echo: + run: e3 + +-- dump.yml -- +e1: + commands: + echo: + run: e1 +e2: + commands: + echo: + run: e2 +e3: + commands: + echo: + run: e3 +extends: + - extends/e3.yml +pre-commit: + commands: + echo: + run: echo 2 + skip: true + tags: + - backend + glob: "3"