diff --git a/docs-master/Config.md b/docs-master/Config.md index 21709da755a..af1f47a257a 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -615,6 +615,7 @@ keybinding: startSearch: / optionMenu: optionMenu-alt1: '?' + gitConfig: ; select: goInto: confirm: diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index bbbd3e2e8b7..34988bb200a 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -18,6 +18,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | | `` `` | View custom patch options | | | `` m `` | View merge/rebase options | View options to abort/continue/skip the current merge/rebase. | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 79cf196fe69..7677c881c62 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -18,6 +18,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | `` ( `` | リネーム検出の類似度しきい値を下げる | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | 差分コンテキストサイズを増やす | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | 差分コンテキストサイズを減らす | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | シェルコマンドを実行 | 実行するシェルコマンドを入力するプロンプトを表示します。 | | `` `` | カスタムパッチオプションを表示 | | | `` m `` | マージ/リベースオプションを表示 | 現在のマージ/リベースを中止/継続/スキップするオプションを表示します。 | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index 330c080eeab..b6d9f52235b 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -18,6 +18,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | Diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기 | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | Diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기 | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | | `` `` | 커스텀 Patch 옵션 보기 | | | `` m `` | View merge/rebase options | View options to abort/continue/skip the current merge/rebase. | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index f738be1b274..4e230334445 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -18,6 +18,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | | `` `` | Bekijk aangepaste patch opties | | | `` m `` | Bekijk merge/rebase opties | View options to abort/continue/skip the current merge/rebase. | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 7bf17f18baa..9864ff6eb8b 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -18,6 +18,7 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | Zwiększ rozmiar kontekstu w widoku różnic | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | Zmniejsz rozmiar kontekstu w widoku różnic | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | | `` `` | Wyświetl opcje niestandardowej łatki | | | `` m `` | Pokaż opcje scalania/rebase | Pokaż opcje do przerwania/kontynuowania/pominięcia bieżącego scalania/rebase. | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 54b1f1871db..4c61f09220c 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -18,6 +18,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Executar comando da shell | Traga um prompt onde você pode digitar um comando shell para executar. | | `` `` | Ver opções de patch personalizadas | | | `` m `` | Ver opções de mesclar/rebase | Ver opções para abortar/continuar/pular o merge/rebase atual. | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index cee2366b421..1d32b49ec6b 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -18,6 +18,7 @@ _Связки клавиш_ | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | Увеличить размер контекста, отображаемого вокруг изменений в просмотрщике сравнении | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | Уменьшите размер контекста, отображаемого вокруг изменений в просмотрщике сравнении | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | | `` `` | Просмотреть пользовательские параметры патча | | | `` m `` | Просмотреть параметры слияния/перебазирования | View options to abort/continue/skip the current merge/rebase. | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index c2cc9c1def0..d2a1bce7584 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -18,6 +18,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | `` ( `` | 降低重命名相似度阈值 | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | 扩大差异视图中显示的上下文范围 | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | 缩小差异视图中显示的上下文范围 | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | 执行 Shell 命令 | 调出可输入shell命令执行的提示符。 | | `` `` | 查看自定义补丁选项 | | | `` m `` | 查看合并/变基选项 | 查看当前合并或变基的中止、继续、跳过选项 | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 59d6d74c90d..8df857fedd3 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -18,6 +18,7 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.

The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. | | `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | Increase the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | | `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | Decrease the amount of the context shown around changes in the diff view.

The default can be changed in the config file with the key 'git.diffContextSize'. | +| `` ; `` | Git config | | | `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. | | `` `` | 檢視自訂補丁選項 | | | `` m `` | 查看合併/變基選項 | View options to abort/continue/skip the current merge/rebase. | diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 90514cbd62a..bf96d6e2a67 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -110,7 +110,7 @@ func NewGitCommandAux( // and allows for better namespacing when compared to having every method living // on the one struct. // common ones are: cmn, osCommand, dotGitDir, configCommands - configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo) + configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo, cmd) gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands, pagerConfig) diff --git a/pkg/commands/git_commands/config.go b/pkg/commands/git_commands/config.go index a9fd4e14772..2f1ddcd6ce0 100644 --- a/pkg/commands/git_commands/config.go +++ b/pkg/commands/git_commands/config.go @@ -1,9 +1,12 @@ package git_commands import ( + "strings" + gogit "github.com/jesseduffield/go-git/v5" "github.com/jesseduffield/go-git/v5/config" "github.com/jesseduffield/lazygit/pkg/commands/git_config" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/common" ) @@ -12,17 +15,20 @@ type ConfigCommands struct { gitConfig git_config.IGitConfig repo *gogit.Repository + cmd oscommands.ICmdObjBuilder } func NewConfigCommands( common *common.Common, gitConfig git_config.IGitConfig, repo *gogit.Repository, + cmd oscommands.ICmdObjBuilder, ) *ConfigCommands { return &ConfigCommands{ Common: common, gitConfig: gitConfig, repo: repo, + cmd: cmd, } } @@ -101,6 +107,72 @@ func (self *ConfigCommands) GetMergeFF() string { return self.gitConfig.Get("merge.ff") } +func (self *ConfigCommands) ListLocalConfig() map[string]string { + return self.listConfig("--local") +} + +func (self *ConfigCommands) ListGlobalConfig() map[string]string { + return self.listConfig("--global") +} + +func (self *ConfigCommands) ListSystemConfig() map[string]string { + return self.listConfig("--system") +} + +func (self *ConfigCommands) GetLocalConfigValue(key string) string { + return self.gitConfig.GetGeneral("--local --get " + key) +} + +func (self *ConfigCommands) GetGlobalConfigValue(key string) string { + return self.gitConfig.GetGeneral("--global --get " + key) +} + +func (self *ConfigCommands) SetLocalConfigValue(key string, value string) error { + return self.cmd.New(NewGitCmd("config").Arg("--local", key, value).ToArgv()).Run() +} + +func (self *ConfigCommands) SetGlobalConfigValue(key string, value string) error { + return self.cmd.New(NewGitCmd("config").Arg("--global", key, value).ToArgv()).Run() +} + +func (self *ConfigCommands) UnsetLocalConfigValue(key string) error { + return self.cmd.New(NewGitCmd("config").Arg("--local", "--unset", key).ToArgv()).Run() +} + +func (self *ConfigCommands) UnsetGlobalConfigValue(key string) error { + return self.cmd.New(NewGitCmd("config").Arg("--global", "--unset", key).ToArgv()).Run() +} + +func (self *ConfigCommands) listConfig(scope string) map[string]string { + cmdObj := self.cmd.New(NewGitCmd("config").Arg(scope, "--list", "--null").ToArgv()).DontLog() + stdout, _, err := cmdObj.RunWithOutputs() + if err != nil { + self.Log.Debugf("Error getting git config list for scope %s: %v", scope, err) + return map[string]string{} + } + + return parseGitConfigList(stdout) +} + +func parseGitConfigList(output string) map[string]string { + result := make(map[string]string) + entries := strings.Split(output, "\x00") + for _, entry := range entries { + if entry == "" { + continue + } + if parts := strings.SplitN(entry, "\n", 2); len(parts) == 2 { + result[parts[0]] = parts[1] + continue + } + + if parts := strings.SplitN(entry, "=", 2); len(parts) == 2 { + result[parts[0]] = parts[1] + } + } + return result +} + func (self *ConfigCommands) DropConfigCache() { self.gitConfig.DropCache() } diff --git a/pkg/commands/git_commands/deps_test.go b/pkg/commands/git_commands/deps_test.go index 3c4bd5a144d..0eacea1f4e2 100644 --- a/pkg/commands/git_commands/deps_test.go +++ b/pkg/commands/git_commands/deps_test.go @@ -76,7 +76,7 @@ func buildGitCommon(deps commonDeps) *GitCommon { } gitCommon.repo = buildRepo() - gitCommon.config = NewConfigCommands(gitCommon.Common, gitConfig, gitCommon.repo) + gitCommon.config = NewConfigCommands(gitCommon.Common, gitConfig, gitCommon.repo, gitCommon.cmd) getenv := deps.getenv if getenv == nil { diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 8205f7ef6cf..8a26e1660b2 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -693,11 +693,12 @@ func (c *AppConfig) SaveGlobalUserConfig() { // AppState stores data between runs of the app like when the last update check // was performed and which other repos have been checked out type AppState struct { - LastUpdateCheck int64 - RecentRepos []string - StartupPopupVersion int - DidShowHunkStagingHint bool - LastVersion string // this is the last version the user was using, for the purpose of showing release notes + LastUpdateCheck int64 + RecentRepos []string + StartupPopupVersion int + DidShowHunkStagingHint bool + LastVersion string // this is the last version the user was using, for the purpose of showing release notes + GitConfigExpandedSections []string `yaml:"gitConfigExpandedSections"` // these are for shell commands typed in directly, not for custom commands in the lazygit config. // For backwards compatibility we keep the old name in yaml files. diff --git a/pkg/config/app_config_test.go b/pkg/config/app_config_test.go index 1109256a9d3..c198f02b2c6 100644 --- a/pkg/config/app_config_test.go +++ b/pkg/config/app_config_test.go @@ -836,6 +836,7 @@ keybinding: startSearch: / optionMenu: optionMenu-alt1: '?' + gitConfig: ';' select: goInto: confirm: diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index bcb5c2f6f85..45f06b1f2e8 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -447,6 +447,7 @@ type KeybindingUniversalConfig struct { StartSearch string `yaml:"startSearch"` OptionMenu string `yaml:"optionMenu"` OptionMenuAlt1 string `yaml:"optionMenu-alt1"` + GitConfig string `yaml:"gitConfig"` Select string `yaml:"select"` GoInto string `yaml:"goInto"` Confirm string `yaml:"confirm"` @@ -909,6 +910,7 @@ func GetDefaultConfig() *UserConfig { StartSearch: "/", OptionMenu: "", OptionMenuAlt1: "?", + GitConfig: ";", Select: "", GoInto: "", Confirm: "", diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 09781261468..8b4e78fef9a 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -17,6 +17,7 @@ type MenuContext struct { *MenuViewModel *ListContextTrait + extraKeybindings []*types.Binding } var _ types.IListContext = (*MenuContext)(nil) @@ -216,14 +217,14 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin // allows assigning a keybinding to a menu item that overrides a non-essential binding such // as 'j', 'k', 'H', 'L', etc. This is safe to do because the essential bindings such as // confirm and return have already been removed from the menu items in this case. - return append(menuItemBindings, basicBindings...) + return append(append(menuItemBindings, self.extraKeybindings...), basicBindings...) } // For the keybindings menu we didn't remove the essential bindings from the menu items, because // it is important to see all bindings (as a cheat sheet for what the keys are when the menu is // not open). Therefore we want the essential bindings to have higher precedence than the menu // item bindings. - return append(basicBindings, menuItemBindings...) + return append(append(basicBindings, menuItemBindings...), self.extraKeybindings...) } func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error { @@ -261,3 +262,7 @@ func (self *MenuContext) FilterPrefix(tr *i18n.TranslationSet) string { return self.FilteredListViewModel.FilterPrefix(tr) } + +func (self *MenuContext) SetExtraKeybindings(bindings []*types.Binding) { + self.extraKeybindings = bindings +} diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 702ed826d11..fea474bb4db 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -89,6 +89,7 @@ func (gui *Gui) resetHelpersAndControllers() { func() *status.StatusManager { return gui.statusManager }, modeHelper, ) + gitConfigHelper := helpers.NewGitConfigHelper(helperCommon) gui.helpers = &helpers.Helpers{ Refs: refsHelper, @@ -115,6 +116,7 @@ func (gui *Gui) resetHelpersAndControllers() { Repos: reposHelper, RecordDirectory: recordDirectoryHelper, Update: helpers.NewUpdateHelper(helperCommon, gui.Updater), + GitConfig: gitConfigHelper, Window: windowHelper, View: viewHelper, Refresh: refreshHelper, diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index 1ae56068561..6e90a5e70a8 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -23,6 +23,14 @@ func NewGlobalController( func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.GitConfig), + Handler: opts.Guards.NoPopupPanel(self.c.Helpers().GitConfig.OpenMenu), + Description: self.c.Tr.GitConfigTitle, + DisplayOnScreen: true, + OpensMenu: true, + GetDisabledReason: self.gitConfigDisabledReason, + }, { Key: opts.GetKey(opts.Config.Universal.ExecuteShellCommand), Handler: self.shellCommand, @@ -216,6 +224,14 @@ func (self *GlobalController) optionsMenuDisabledReason() *types.DisabledReason return nil } +func (self *GlobalController) gitConfigDisabledReason() *types.DisabledReason { + ctx := self.c.Context().Current() + if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP { + return &types.DisabledReason{Text: ""} + } + return nil +} + func (self *GlobalController) createFilteringMenu() error { return (&FilteringMenuAction{c: self.c}).Call() } diff --git a/pkg/gui/controllers/helpers/git_config_helper.go b/pkg/gui/controllers/helpers/git_config_helper.go new file mode 100644 index 00000000000..32a752a5f5c --- /dev/null +++ b/pkg/gui/controllers/helpers/git_config_helper.go @@ -0,0 +1,508 @@ +package helpers + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) + +type GitConfigHelper struct { + c *HelperCommon + displayScope gitConfigDisplayScope + expandedSections map[string]bool +} + +func NewGitConfigHelper(c *HelperCommon) *GitConfigHelper { + expandedSections := map[string]bool{} + for _, section := range c.GetAppState().GitConfigExpandedSections { + expandedSections[section] = true + } + return &GitConfigHelper{ + c: c, + displayScope: gitConfigDisplayScopeGlobal, + expandedSections: expandedSections, + } +} + +func (self *GitConfigHelper) OpenMenu() error { + return self.openMenuWithSelection(self.currentMenuSelectionID()) +} + +func (self *GitConfigHelper) openMenuWithSelection(selectionID string) error { + localConfig := self.c.Git().Config.ListLocalConfig() + globalConfig := self.c.Git().Config.ListGlobalConfig() + systemConfig := self.c.Git().Config.ListSystemConfig() + + displayValues := self.displayScopeValues(localConfig, globalConfig, systemConfig) + keys := keysForScope(displayValues, defaultGitConfigKeysForScope(self.displayScope)) + configSection := &types.MenuSection{ + Title: utils.ResolvePlaceholderString( + self.c.Tr.GitConfigConfigSectionWithScope, + map[string]string{"scope": self.displayScopeLabel()}, + ), + Column: 0, + } + menuItems := []*types.MenuItem{} + + menuItems = append(menuItems, self.sectionMenuItems(configSection, keys, displayValues)...) + + self.c.Contexts().Menu.SetExtraKeybindings(self.scopeKeybindings()) + + if err := self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.GitConfigTitle, + Items: menuItems, + ColumnAlignment: []utils.Alignment{utils.AlignLeft, utils.AlignLeft}, + }); err != nil { + return err + } + self.restoreSelection(menuItems, selectionID) + return nil +} + +func (self *GitConfigHelper) openGitConfigEntryMenu(key string) error { + localValue := self.c.Git().Config.GetLocalConfigValue(key) + globalValue := self.c.Git().Config.GetGlobalConfigValue(key) + notSetReason := &types.DisabledReason{Text: self.c.Tr.GitConfigNotSet} + + menuItems := []*types.MenuItem{ + { + LabelColumns: []string{ + self.c.Tr.GitConfigSetLocal, + self.formatGitConfigValue(localValue, style.FgGreen), + }, + OnPress: func() error { + return self.promptSetGitConfigValue(key, gitConfigScopeLocal, localValue) + }, + }, + { + LabelColumns: []string{ + self.c.Tr.GitConfigSetGlobal, + self.formatGitConfigValue(globalValue, style.FgYellow), + }, + OnPress: func() error { + return self.promptSetGitConfigValue(key, gitConfigScopeGlobal, globalValue) + }, + }, + { + Label: self.c.Tr.GitConfigUnsetLocal, + OnPress: func() error { return self.unsetGitConfigValue(key, gitConfigScopeLocal) }, + DisabledReason: lo.Ternary(localValue == "", notSetReason, nil), + }, + { + Label: self.c.Tr.GitConfigUnsetGlobal, + OnPress: func() error { return self.unsetGitConfigValue(key, gitConfigScopeGlobal) }, + DisabledReason: lo.Ternary(globalValue == "", notSetReason, nil), + }, + } + + return self.c.Menu(types.CreateMenuOptions{ + Title: key, + Items: menuItems, + ColumnAlignment: []utils.Alignment{utils.AlignLeft, utils.AlignLeft}, + }) +} + +type gitConfigScope struct { + name string +} + +var ( + gitConfigScopeLocal = gitConfigScope{name: "local"} + gitConfigScopeGlobal = gitConfigScope{name: "global"} +) + +func (self *GitConfigHelper) promptSetGitConfigValue(key string, scope gitConfigScope, currentValue string) error { + title := utils.ResolvePlaceholderString( + self.c.Tr.GitConfigSetPromptTitle, + map[string]string{ + "key": key, + "scope": self.gitConfigScopeLabel(scope), + }, + ) + self.c.Prompt(types.PromptOpts{ + Title: title, + InitialContent: currentValue, + AllowEmptyInput: false, + PreserveWhitespace: true, + HandleConfirm: func(value string) error { + if err := self.setGitConfigValue(key, scope, value); err != nil { + return err + } + self.c.Toast(utils.ResolvePlaceholderString(self.c.Tr.GitConfigUpdatedToast, map[string]string{ + "key": key, + "scope": self.gitConfigScopeLabel(scope), + })) + return self.OpenMenu() + }, + }) + return nil +} + +func (self *GitConfigHelper) setGitConfigValue(key string, scope gitConfigScope, value string) error { + var err error + switch scope.name { + case "local": + err = self.c.Git().Config.SetLocalConfigValue(key, value) + case "global": + err = self.c.Git().Config.SetGlobalConfigValue(key, value) + default: + return errors.New("unknown git config scope") + } + + if err != nil { + return err + } + + self.c.Git().Config.DropConfigCache() + return nil +} + +func (self *GitConfigHelper) unsetGitConfigValue(key string, scope gitConfigScope) error { + var err error + switch scope.name { + case "local": + err = self.c.Git().Config.UnsetLocalConfigValue(key) + case "global": + err = self.c.Git().Config.UnsetGlobalConfigValue(key) + default: + return errors.New("unknown git config scope") + } + + if err != nil { + return err + } + + self.c.Git().Config.DropConfigCache() + self.c.Toast(utils.ResolvePlaceholderString(self.c.Tr.GitConfigUpdatedToast, map[string]string{ + "key": key, + "scope": self.gitConfigScopeLabel(scope), + })) + return self.OpenMenu() +} + +func (self *GitConfigHelper) formatGitConfigValue(value string, color style.TextStyle) string { + if value == "" { + return style.FgBlackLighter.Sprint(self.c.Tr.GitConfigNotSet) + } + return color.Sprint(value) +} + +func (self *GitConfigHelper) gitConfigScopeLabel(scope gitConfigScope) string { + switch scope.name { + case "local": + return self.c.Tr.GitConfigScopeLocal + case "global": + return self.c.Tr.GitConfigScopeGlobal + default: + return scope.name + } +} + +type gitConfigDisplayScope struct { + name string +} + +var ( + gitConfigDisplayScopeLocal = gitConfigDisplayScope{name: "local"} + gitConfigDisplayScopeGlobal = gitConfigDisplayScope{name: "global"} + gitConfigDisplayScopeSystem = gitConfigDisplayScope{name: "system"} +) + +func (self gitConfigDisplayScope) label(tr *i18n.TranslationSet) string { + switch self.name { + case "local": + return tr.GitConfigLocalColumn + case "global": + return tr.GitConfigGlobalColumn + case "system": + return tr.GitConfigSystemColumn + default: + return self.name + } +} + +func (self gitConfigDisplayScope) keybinding() types.Key { + switch self.name { + case "local": + return 'l' + case "global": + return 'g' + case "system": + return 's' + default: + return nil + } +} + +func (self *GitConfigHelper) displayScopeValues(localConfig map[string]string, globalConfig map[string]string, systemConfig map[string]string) map[string]string { + switch self.displayScope.name { + case "local": + return localConfig + case "global": + return globalConfig + case "system": + return systemConfig + default: + return map[string]string{} + } +} + +func (self *GitConfigHelper) sectionMenuItems(sectionHeader *types.MenuSection, keys []string, values map[string]string) []*types.MenuItem { + sections := map[string][]string{} + leafKeys := []string{} + for _, key := range keys { + parts := strings.SplitN(key, ".", 2) + if len(parts) == 1 { + leafKeys = append(leafKeys, key) + continue + } + sections[parts[0]] = append(sections[parts[0]], parts[1]) + } + + sectionNames := make([]string, 0, len(sections)) + for sectionName := range sections { + sectionNames = append(sectionNames, sectionName) + } + sort.Strings(sectionNames) + sort.Strings(leafKeys) + + menuItems := make([]*types.MenuItem, 0, len(keys)) + for _, section := range sectionNames { + isExpanded := self.isSectionExpanded(section) + prefix := "+ " + if isExpanded { + prefix = "- " + } + menuItems = append(menuItems, &types.MenuItem{ + Label: "section:" + section, + LabelColumns: []string{prefix + section}, + Section: sectionHeader, + OnPress: func() error { + self.toggleSection(section) + return self.openMenuWithSelection("section:" + section) + }, + }) + + if !isExpanded { + continue + } + + children := sections[section] + sort.Strings(children) + for i, child := range children { + isLast := i == len(children)-1 + branch := " |- " + if isLast { + branch = " `- " + } + fullKey := section + "." + child + menuItems = append(menuItems, &types.MenuItem{ + Label: "key:" + fullKey, + LabelColumns: []string{ + branch + child, + self.formatGitConfigValue(values[fullKey], self.displayScopeColor()), + }, + Section: sectionHeader, + OnPress: func() error { + return self.openGitConfigEntryMenu(fullKey) + }, + }) + } + } + + for _, key := range leafKeys { + menuItems = append(menuItems, &types.MenuItem{ + Label: "key:" + key, + LabelColumns: []string{ + key, + self.formatGitConfigValue(values[key], self.displayScopeColor()), + }, + Section: sectionHeader, + OnPress: func() error { + return self.openGitConfigEntryMenu(key) + }, + }) + } + + return menuItems +} + +func (self *GitConfigHelper) displayScopeColor() style.TextStyle { + switch self.displayScope.name { + case "local": + return style.FgGreen + case "global": + return style.FgYellow + case "system": + return style.FgBlue + default: + return style.FgDefault + } +} + +func (self *GitConfigHelper) isSectionExpanded(section string) bool { + expanded, ok := self.expandedSections[section] + return ok && expanded +} + +func (self *GitConfigHelper) toggleSection(section string) { + self.expandedSections[section] = !self.isSectionExpanded(section) + self.persistExpandedSections() +} + +func (self *GitConfigHelper) persistExpandedSections() { + expanded := []string{} + for section, isExpanded := range self.expandedSections { + if isExpanded { + expanded = append(expanded, section) + } + } + sort.Strings(expanded) + self.c.GetAppState().GitConfigExpandedSections = expanded + self.c.SaveAppStateAndLogError() +} + +func (self *GitConfigHelper) currentMenuSelectionID() string { + if self.c.Contexts().Menu == nil { + return "" + } + return self.c.Contexts().Menu.GetSelectedItemId() +} + +func (self *GitConfigHelper) restoreSelection(items []*types.MenuItem, selectionID string) { + if selectionID == "" { + return + } + for i, item := range items { + if item != nil && item.ID() == selectionID { + self.c.Contexts().Menu.GetList().SetSelection(i) + self.c.Contexts().Menu.FocusLine(true) + return + } + } +} + +func (self *GitConfigHelper) scopeKeybindings() []*types.Binding { + return []*types.Binding{ + { + Key: gitConfigDisplayScopeLocal.keybinding(), + Handler: func() error { return self.setDisplayScope(gitConfigDisplayScopeLocal) }, + }, + { + Key: gitConfigDisplayScopeGlobal.keybinding(), + Handler: func() error { return self.setDisplayScope(gitConfigDisplayScopeGlobal) }, + }, + { + Key: gitConfigDisplayScopeSystem.keybinding(), + Handler: func() error { return self.setDisplayScope(gitConfigDisplayScopeSystem) }, + }, + { + Key: gocui.KeyArrowLeft, + Handler: self.selectPreviousScope, + }, + { + Key: gocui.KeyArrowRight, + Handler: self.selectNextScope, + }, + } +} + +func (self *GitConfigHelper) setDisplayScope(scope gitConfigDisplayScope) error { + if self.displayScope.name == scope.name { + return nil + } + self.displayScope = scope + return self.openMenuWithSelection(self.currentMenuSelectionID()) +} + +func (self *GitConfigHelper) selectPreviousScope() error { + return self.selectScopeOffset(-1) +} + +func (self *GitConfigHelper) selectNextScope() error { + return self.selectScopeOffset(1) +} + +func (self *GitConfigHelper) selectScopeOffset(offset int) error { + scopes := []gitConfigDisplayScope{ + gitConfigDisplayScopeLocal, + gitConfigDisplayScopeGlobal, + gitConfigDisplayScopeSystem, + } + currentIdx := 0 + for i, scope := range scopes { + if scope.name == self.displayScope.name { + currentIdx = i + break + } + } + nextIdx := (currentIdx + offset) % len(scopes) + if nextIdx < 0 { + nextIdx += len(scopes) + } + return self.setDisplayScope(scopes[nextIdx]) +} + +func (self *GitConfigHelper) displayScopeLabel() string { + return fmt.Sprintf("< %s >", self.displayScope.label(self.c.Tr)) +} + +func keysForScope(values map[string]string, defaults []string) []string { + keySet := make(map[string]struct{}, len(values)+len(defaults)) + for key := range values { + keySet[key] = struct{}{} + } + for _, key := range defaults { + keySet[key] = struct{}{} + } + result := make([]string, 0, len(keySet)) + for key := range keySet { + result = append(result, key) + } + sort.Strings(result) + return result +} + +type defaultGitConfigKey struct { + key string + scopes map[string]bool +} + +func defaultGitConfigKeysForScope(scope gitConfigDisplayScope) []string { + keys := []defaultGitConfigKey{ + {key: "user.name", scopes: map[string]bool{"local": true, "global": true}}, + {key: "user.email", scopes: map[string]bool{"local": true, "global": true}}, + {key: "core.editor", scopes: map[string]bool{"local": true, "global": true, "system": true}}, + {key: "core.autocrlf", scopes: map[string]bool{"local": true, "global": true, "system": true}}, + {key: "core.ignorecase", scopes: map[string]bool{"local": true, "global": true, "system": true}}, + {key: "init.defaultBranch", scopes: map[string]bool{"global": true, "system": true}}, + {key: "pull.rebase", scopes: map[string]bool{"local": true, "global": true}}, + {key: "pull.ff", scopes: map[string]bool{"local": true, "global": true}}, + {key: "merge.ff", scopes: map[string]bool{"local": true, "global": true}}, + {key: "push.default", scopes: map[string]bool{"local": true, "global": true}}, + {key: "fetch.prune", scopes: map[string]bool{"local": true, "global": true}}, + {key: "commit.gpgSign", scopes: map[string]bool{"local": true, "global": true}}, + {key: "tag.gpgSign", scopes: map[string]bool{"local": true, "global": true}}, + {key: "gpg.program", scopes: map[string]bool{"global": true, "system": true}}, + {key: "rebase.autostash", scopes: map[string]bool{"local": true, "global": true}}, + {key: "rerere.enabled", scopes: map[string]bool{"local": true, "global": true}}, + {key: "diff.tool", scopes: map[string]bool{"local": true, "global": true}}, + {key: "merge.tool", scopes: map[string]bool{"local": true, "global": true}}, + } + result := []string{} + for _, entry := range keys { + if entry.scopes[scope.name] { + result = append(result, entry.key) + } + } + sort.Strings(result) + return result +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 4c9c79f3d81..14206c332c1 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -42,6 +42,7 @@ type Helpers struct { Repos *ReposHelper RecordDirectory *RecordDirectoryHelper Update *UpdateHelper + GitConfig *GitConfigHelper Window *WindowHelper View *ViewHelper Refresh *RefreshHelper @@ -79,6 +80,7 @@ func NewStubHelpers() *Helpers { Repos: &ReposHelper{}, RecordDirectory: &RecordDirectoryHelper{}, Update: &UpdateHelper{}, + GitConfig: &GitConfigHelper{}, Window: &WindowHelper{}, View: &ViewHelper{}, Refresh: &RefreshHelper{}, diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 8a681abbd57..9433117d4cd 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -11,6 +11,9 @@ import ( // note: items option is mutated by this function func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { + if opts.Title != gui.c.Tr.GitConfigTitle { + gui.State.Contexts.Menu.SetExtraKeybindings(nil) + } if !opts.HideCancel { // this is mutative but I'm okay with that for now opts.Items = append(opts.Items, &types.MenuItem{ diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 75391a2c1f2..34a7f359ab5 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -228,6 +228,22 @@ type TranslationSet struct { RenameStashPrompt string OpenConfig string EditConfig string + GitConfigTitle string + GitConfigKeyColumn string + GitConfigLocalColumn string + GitConfigGlobalColumn string + GitConfigSystemColumn string + GitConfigValueColumn string + GitConfigSetLocal string + GitConfigSetGlobal string + GitConfigUnsetLocal string + GitConfigUnsetGlobal string + GitConfigSetPromptTitle string + GitConfigUpdatedToast string + GitConfigNotSet string + GitConfigScopeLocal string + GitConfigScopeGlobal string + GitConfigConfigSectionWithScope string ForcePush string ForcePushPrompt string ForcePushDisabled string @@ -1322,6 +1338,22 @@ func EnglishTranslationSet() *TranslationSet { RenameStashPrompt: "Rename stash: {{.stashName}}", OpenConfig: "Open config file", EditConfig: "Edit config file", + GitConfigTitle: "Git config", + GitConfigKeyColumn: "Key", + GitConfigLocalColumn: "Local", + GitConfigGlobalColumn: "Global", + GitConfigSystemColumn: "System", + GitConfigValueColumn: "Value ({{.scope}})", + GitConfigSetLocal: "Set local value", + GitConfigSetGlobal: "Set global value", + GitConfigUnsetLocal: "Unset local value", + GitConfigUnsetGlobal: "Unset global value", + GitConfigSetPromptTitle: "Set {{.scope}} {{.key}}", + GitConfigUpdatedToast: "Updated {{.scope}} {{.key}}", + GitConfigNotSet: "not set", + GitConfigScopeLocal: "local", + GitConfigScopeGlobal: "global", + GitConfigConfigSectionWithScope: "Config {{.scope}}", ForcePush: "Force push", ForcePushPrompt: "Your branch has diverged from the remote branch. Press {{.cancelKey}} to cancel, or {{.confirmKey}} to force push.", ForcePushDisabled: "Your branch has diverged from the remote branch and you've disabled force pushing", diff --git a/pkg/integration/tests/ui/keybinding_suggestions_when_switching_repos.go b/pkg/integration/tests/ui/keybinding_suggestions_when_switching_repos.go index def9a53c742..5007ad0dfdf 100644 --- a/pkg/integration/tests/ui/keybinding_suggestions_when_switching_repos.go +++ b/pkg/integration/tests/ui/keybinding_suggestions_when_switching_repos.go @@ -31,12 +31,12 @@ var KeybindingSuggestionsWhenSwitchingRepos = NewIntegrationTest(NewIntegrationT t.Views().Files().Focus() t.Views().Options().Content( - Equals("Commit: c | Stash: s | Reset: D | Keybindings: ?")) + Equals("Commit: c | Stash: s | Reset: D | Git config: ; | Keybindings: ?")) switchToRepo("other") switchToRepo("repo") t.Views().Options().Content( - Equals("Commit: c | Stash: s | Reset: D | Keybindings: ?")) + Equals("Commit: c | Stash: s | Reset: D | Git config: ; | Keybindings: ?")) }, }) diff --git a/schema-master/config.json b/schema-master/config.json index c371f14e9f7..01beddc063a 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -1370,6 +1370,10 @@ "type": "string", "default": "?" }, + "gitConfig": { + "type": "string", + "default": ";" + }, "select": { "type": "string", "default": "\u003cspace\u003e"