diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 81d2792ff..3f836a21b 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -191,6 +191,51 @@ func validateCustomCommandKey(key Keybinding) error { return nil } +// ValidCustomCommandContexts lists the names a custom command's 'context' may +// use. It mirrors context.AllContextKeys in the gui package, which this package +// can't import; a test over there keeps the two in sync. +var ValidCustomCommandContexts = []string{ + "global", + "status", + "files", + "localBranches", + "remotes", + "worktrees", + "remoteBranches", + "tags", + "commits", + "reflogCommits", + "subCommits", + "commitFiles", + "stash", + "normal", + "normalSecondary", + "staging", + "stagingSecondary", + "patchBuilding", + "patchBuildingSecondary", + "mergeConflicts", + "menu", + "confirmation", + "prompt", + "search", + "commitMessage", + "submodules", + "suggestions", + "cmdLog", +} + +func validateCustomCommandContext(context string) error { + for _, name := range strings.Split(context, ",") { + name = strings.TrimSpace(name) + if !slices.Contains(ValidCustomCommandContexts, name) { + return fmt.Errorf("Unknown context '%s' for custom command. Allowed values: %s", + name, strings.Join(ValidCustomCommandContexts, ", ")) + } + } + return nil +} + func validateCustomCommands(customCommands []CustomCommand) error { for _, customCommand := range customCommands { if err := validateCustomCommandKey(customCommand.Key); err != nil { @@ -216,6 +261,15 @@ func validateCustomCommands(customCommands []CustomCommand) error { return err } } else { + // A command in a menu may leave the context out, in which case it is + // offered whatever is focused; a top-level one may not, but that is + // only noticed when the keybindings are built. + if customCommand.Context != "" { + if err := validateCustomCommandContext(customCommand.Context); err != nil { + return err + } + } + for _, prompt := range customCommand.Prompts { if err := validateCustomCommandPrompt(prompt); err != nil { return err diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index 370818718..7977e3e4c 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -225,6 +225,43 @@ func TestUserConfigValidate_enums(t *testing.T) { {value: "invalid_value", valid: false}, }, }, + { + name: "Custom command context", + setup: func(config *UserConfig, value string) { + config.CustomCommands = []CustomCommand{ + { + Context: value, + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "global", valid: true}, + {value: "commits", valid: true}, + {value: "commits, subCommits", valid: true}, + {value: "commits,subCommits", valid: true}, + {value: "invalid_value", valid: false}, + {value: "commits, invalid_value", valid: false}, + }, + }, + { + name: "Custom command context in a sub menu", + setup: func(config *UserConfig, value string) { + config.CustomCommands = []CustomCommand{ + { + Key: Keybinding{"X"}, + CommandMenu: []CustomCommand{ + {Key: Keybinding{"1"}, Command: "echo 'hello'", Context: value}, + }, + }, + } + }, + testCases: []testCase{ + {value: "", valid: true}, + {value: "commits", valid: true}, + {value: "invalid_value", valid: false}, + }, + }, { name: "Custom command sub menu", setup: func(config *UserConfig, _ string) { diff --git a/pkg/gui/context/context_test.go b/pkg/gui/context/context_test.go new file mode 100644 index 000000000..53f83c6ea --- /dev/null +++ b/pkg/gui/context/context_test.go @@ -0,0 +1,22 @@ +package context + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +// The config package validates a custom command's context against its own copy of +// these names, being unable to import this package. A name in one list but not the +// other would be either a context that validation rejects although you can bind to +// it, or one it accepts although binding to it exits lazygit. +func TestValidCustomCommandContextsMatchesAllContextKeys(t *testing.T) { + keys := lo.Map(AllContextKeys, func(key types.ContextKey, _ int) string { + return string(key) + }) + + assert.Equal(t, keys, config.ValidCustomCommandContexts) +}