Skip to content

Commit

Permalink
Support per-repo config properly (#256)
Browse files Browse the repository at this point in the history
Previously, it finds a config in some config dirs or in
`.git/config.$EXT`. This changes the config loading code to do the
followings:

* Change the repository config path from `$GIT_DIR/config.yaml` to
  `$GIT_COMMON_DIR/av/config.yaml`. `$GIT_COMMON_DIR` is always (e.g.
  `.git` dir). `$GIT_DIR` can point to something else under certain
  situations.

* Loads both global configs (`~/.config/av/config.yaml`) and repository
  config (`.git/av/config.yaml`). The repository config overrides the
  global config.
  • Loading branch information
draftcode authored Apr 6, 2024
1 parent c1e5fc4 commit e772868
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 41 deletions.
25 changes: 12 additions & 13 deletions cmd/av/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -45,35 +46,33 @@ var rootCmd = &cobra.Command{
logrus.WithField("av_version", config.Version).Debug("enabled debug logging")
}

var configDirs []string
repoConfigDir := ""
repo, err := getRepo()
// If we weren't able to load the Git repo, that probably just means the
// command isn't being run from inside a repo. That's fine, we just
// don't need to bother reading repo-local config.
if err != nil {
logrus.WithError(err).Debug("unable to load Git repo (probably not inside a repo)")
} else {
gitDir, err := repo.Git("rev-parse", "--git-dir")
gitCommonDir, err := repo.Git("rev-parse", "--git-common-dir")
if err != nil {
logrus.WithError(err).Warning("failed to determine git root directory")
logrus.WithError(err).Warning("failed to determine $GIT_COMMON_DIR")
} else {
configDirs = append(configDirs, gitDir)
gitCommonDir, err = filepath.Abs(gitCommonDir)
if err != nil {
logrus.WithError(err).Warning("failed to determine $GIT_COMMON_DIR")
} else {
logrus.WithField("git_common_dir", gitCommonDir).Debug("loaded Git repo")
repoConfigDir = filepath.Join(gitCommonDir, "av")
}
}
logrus.WithField("git_dir", gitDir).Debug("loaded Git repo")
}

// Note: this only returns an error if config exists and it can't be
// read/parsed. It doesn't return an error if no config file exists.
didLoadConfig, err := config.Load(configDirs)
if err != nil {
if err := config.Load(repoConfigDir); err != nil {
return errors.Wrap(err, "failed to load configuration")
}
if didLoadConfig {
logrus.Debug("loaded configuration")
} else {
logrus.Debug("no configuration found")
}

return nil
},
}
Expand Down
69 changes: 41 additions & 28 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package config

import (
"os"
"path/filepath"

"emperror.dev/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)

Expand Down Expand Up @@ -62,52 +64,63 @@ var Av = struct {
}

// Load initializes the configuration values.
// It may optionally be called with a list of additional paths to check for the
// config file.
// Returns a boolean indicating whether or not a config file was loaded and an
// error if one occurred.
func Load(paths []string) (bool, error) {
loaded, err := loadFromFile(paths)
if err != nil {
return loaded, err
//
// This takes an optional repository config directory, which, when exists, overrides the default
// config.
func Load(repoConfigDir string) error {
if err := loadFromFile(repoConfigDir); err != nil {
return err
}
if err := loadFromEnv(); err != nil {
return loaded, err
return err
}
return loaded, err
return nil
}

func loadFromFile(paths []string) (bool, error) {
func loadFromFile(repoConfigDir string) error {
config := viper.New()

// Viper has support for various formats, so it supports kson, toml, yaml,
// and more (https://github.com/spf13/viper#reading-config-files).
// The base filename of the config files.
config.SetConfigName("config")

// Reasonable places to look for config files.
// With config.ReadInConfig, Viper looks for a file with `config.$EXT` where $EXT is
// viper.SupportedExts. It tries to find the file in the following directories in this
// order (e.g. $XDG_CONFIG_HOME/av/config.yaml first).
//
// Note that Viper will find only one file in these directories, so if there are multiple,
// only one is read.
config.AddConfigPath("$XDG_CONFIG_HOME/av")
config.AddConfigPath("$HOME/.config/av")
config.AddConfigPath("$HOME/.av")
config.AddConfigPath("$AV_HOME")
// Add additional custom paths.
// The primary use case for this is adding repository-specific
// configuration (e.g., $REPO/.git/av/config.json).
for _, path := range paths {
config.AddConfigPath(path)
if err := config.ReadInConfig(); err != nil {
// We can ignore config file not exist case.
if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return err
}
} else {
logrus.WithField("config_file", config.ConfigFileUsed()).Debug("loaded config file")
}

if err := config.ReadInConfig(); err != nil {
if errors.As(err, &viper.ConfigFileNotFoundError{}) {
return false, nil
// As stated above, Viper will read only one file from the above paths. However, we want to
// support per-repo configuration that overrides the global configuration. Here, we mimic
// the behavior of Viper by looking for the per-repo config file and merge it.
for _, ext := range viper.SupportedExts {
fp := filepath.Join(repoConfigDir, "config."+ext)
if stat, err := os.Stat(fp); err == nil {
if !stat.IsDir() {
config.SetConfigFile(fp)
if err := config.MergeInConfig(); err != nil {
return errors.Wrapf(err, "failed to read %s", fp)
}
logrus.WithField("config_file", fp).Debug("loaded config file")
break
}
}
return false, err
}

if err := config.Unmarshal(&Av); err != nil {
return true, errors.Wrap(err, "failed to read av configs")
return errors.Wrap(err, "failed to read av configs")
}

return false, nil
return nil
}

func loadFromEnv() error {
Expand Down

0 comments on commit e772868

Please sign in to comment.