From 74ed1ac584edb108291a76494ed1a08b9674eb24 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 15 Jul 2024 13:48:00 +0200 Subject: [PATCH] Support per-repo config files For now we only support .git/lazygit.yml; in the future we would also like to support ./.lazygit.yml, but that one will need a trust prompt as it could be versioned, which adds quite a bit of complexity, so we leave that for later. We do, however, support config files in parent directories (all the way up to the root directory). This makes it possible to add a config file that applies to multiple repos at once. Useful if you want to set different options for all your work repos vs. all your open-source repos, for instance. --- pkg/config/app_config.go | 76 +++++++++++++++++++++++++++------------- pkg/gui/gui.go | 44 +++++++++++++++++++++++ 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index e30ff4a26..b2cc1eafe 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -15,16 +15,17 @@ import ( // AppConfig contains the base configuration fields required for lazygit. type AppConfig struct { - debug bool `long:"debug" env:"DEBUG" default:"false"` - version string `long:"version" env:"VERSION" default:"unversioned"` - buildDate string `long:"build-date" env:"BUILD_DATE"` - name string `long:"name" env:"NAME" default:"lazygit"` - buildSource string `long:"build-source" env:"BUILD_SOURCE" default:""` - userConfig *UserConfig - userConfigFiles []*ConfigFile - userConfigDir string - tempDir string - appState *AppState + debug bool `long:"debug" env:"DEBUG" default:"false"` + version string `long:"version" env:"VERSION" default:"unversioned"` + buildDate string `long:"build-date" env:"BUILD_DATE"` + name string `long:"name" env:"NAME" default:"lazygit"` + buildSource string `long:"build-source" env:"BUILD_SOURCE" default:""` + userConfig *UserConfig + globalUserConfigFiles []*ConfigFile + userConfigFiles []*ConfigFile + userConfigDir string + tempDir string + appState *AppState } type AppConfigurer interface { @@ -38,6 +39,7 @@ type AppConfigurer interface { GetUserConfig() *UserConfig GetUserConfigPaths() []string GetUserConfigDir() string + ReloadUserConfigForRepo(repoConfigFiles []*ConfigFile) error GetTempDir() string GetAppState() *AppState @@ -49,11 +51,13 @@ type ConfigFilePolicy int const ( ConfigFilePolicyCreateIfMissing ConfigFilePolicy = iota ConfigFilePolicyErrorIfMissing + ConfigFilePolicySkipIfMissing ) type ConfigFile struct { Path string Policy ConfigFilePolicy + exists bool } // NewAppConfig makes a new app config @@ -108,16 +112,17 @@ func NewAppConfig( } appConfig := &AppConfig{ - name: name, - version: version, - buildDate: date, - debug: debuggingFlag, - buildSource: buildSource, - userConfig: userConfig, - userConfigFiles: configFiles, - userConfigDir: configDir, - tempDir: tempDir, - appState: appState, + name: name, + version: version, + buildDate: date, + debug: debuggingFlag, + buildSource: buildSource, + userConfig: userConfig, + globalUserConfigFiles: configFiles, + userConfigFiles: configFiles, + userConfigDir: configDir, + tempDir: tempDir, + appState: appState, } return appConfig, nil @@ -141,7 +146,10 @@ func loadUserConfigWithDefaults(configFiles []*ConfigFile) (*UserConfig, error) func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, error) { for _, configFile := range configFiles { path := configFile.Path - if _, err := os.Stat(path); err != nil { + _, err := os.Stat(path) + if err == nil { + configFile.exists = true + } else { if !os.IsNotExist(err) { return nil, err } @@ -150,6 +158,10 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e case ConfigFilePolicyErrorIfMissing: return nil, err + case ConfigFilePolicySkipIfMissing: + configFile.exists = false + continue + case ConfigFilePolicyCreateIfMissing: file, err := os.Create(path) if err != nil { @@ -160,6 +172,8 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e return nil, err } file.Close() + + configFile.exists = true } } @@ -260,8 +274,8 @@ func (c *AppConfig) GetAppState() *AppState { } func (c *AppConfig) GetUserConfigPaths() []string { - return lo.Map(c.userConfigFiles, func(f *ConfigFile, _ int) string { - return f.Path + return lo.FilterMap(c.userConfigFiles, func(f *ConfigFile, _ int) (string, bool) { + return f.Path, f.exists }) } @@ -269,6 +283,18 @@ func (c *AppConfig) GetUserConfigDir() string { return c.userConfigDir } +func (c *AppConfig) ReloadUserConfigForRepo(repoConfigFiles []*ConfigFile) error { + configFiles := append(c.globalUserConfigFiles, repoConfigFiles...) + userConfig, err := loadUserConfigWithDefaults(configFiles) + if err != nil { + return err + } + + c.userConfig = userConfig + c.userConfigFiles = configFiles + return nil +} + func (c *AppConfig) GetTempDir() string { return c.tempDir } @@ -365,7 +391,7 @@ func loadAppState() (*AppState, error) { // SaveGlobalUserConfig saves the UserConfig back to disk. This is only used in // integration tests, so we are a bit sloppy with error handling. func (c *AppConfig) SaveGlobalUserConfig() { - if len(c.userConfigFiles) != 1 { + if len(c.globalUserConfigFiles) != 1 { panic("expected exactly one global user config file") } @@ -374,7 +400,7 @@ func (c *AppConfig) SaveGlobalUserConfig() { log.Fatalf("error marshalling user config: %v", err) } - err = os.WriteFile(c.userConfigFiles[0].Path, yamlContent, 0o644) + err = os.WriteFile(c.globalUserConfigFiles[0].Path, yamlContent, 0o644) if err != nil { log.Fatalf("error saving user config: %v", err) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index cd2cdf983..015aec1fa 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sort" "strings" "sync" @@ -307,6 +308,16 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context return err } + err = gui.Config.ReloadUserConfigForRepo(gui.getPerRepoConfigFiles()) + if err != nil { + return err + } + + err = gui.onUserConfigLoaded() + if err != nil { + return err + } + contextToPush := gui.resetState(startArgs) gui.resetHelpersAndControllers() @@ -342,6 +353,39 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context return nil } +func (gui *Gui) getPerRepoConfigFiles() []*config.ConfigFile { + repoConfigFiles := []*config.ConfigFile{ + // TODO: add filepath.Join(gui.git.RepoPaths.RepoPath(), ".lazygit.yml"), + // with trust prompt + { + Path: filepath.Join(gui.git.RepoPaths.RepoGitDirPath(), "lazygit.yml"), + Policy: config.ConfigFilePolicySkipIfMissing, + }, + } + + prevDir := gui.c.Git().RepoPaths.RepoPath() + dir := filepath.Dir(prevDir) + for dir != prevDir { + repoConfigFiles = utils.Prepend(repoConfigFiles, &config.ConfigFile{ + Path: filepath.Join(dir, ".lazygit.yml"), + Policy: config.ConfigFilePolicySkipIfMissing, + }) + prevDir = dir + dir = filepath.Dir(dir) + } + return repoConfigFiles +} + +func (gui *Gui) onUserConfigLoaded() error { + userConfig := gui.Config.GetUserConfig() + gui.Common.SetUserConfig(userConfig) + + gui.setColorScheme() + gui.configureViewProperties() + + return nil +} + // resetState reuses the repo state from our repo state map, if the repo was // open before; otherwise it creates a new one. func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {