diff options
author | Mark Kopenga <mkopenga@gmail.com> | 2021-07-22 19:54:14 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-22 19:54:14 +0200 |
commit | c005b0d92bcb03403582a36674d3ad8801b7806a (patch) | |
tree | eab0691d59061147fb1cac03a3779da5f343a7f3 /pkg | |
parent | 1573a449f84846657b7ac9e07756caa9db548b0e (diff) | |
parent | 713fae3e32a0482314a9489d3169ca62af2417de (diff) |
Merge pull request #1390 from FoamScience/menu_from_cmd
Generate menu options from a Git Command with a filter
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/config/user_config.go | 5 | ||||
-rw-r--r-- | pkg/gui/custom_commands.go | 220 | ||||
-rw-r--r-- | pkg/gui/dummies.go | 21 | ||||
-rw-r--r-- | pkg/gui/gui_test.go | 49 |
4 files changed, 234 insertions, 61 deletions
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 4855bf816..d373b74f1 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -280,6 +280,11 @@ type CustomCommandPrompt struct { // this only applies to menus Options []CustomCommandMenuOption + + // this only applies to menuFromCommand + Command string `yaml:"command"` + Filter string `yaml:"filter"` + Format string `yaml:"format"` } type CustomCommandMenuOption struct { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 06be97da0..752587e69 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -1,8 +1,13 @@ package gui import ( + "bytes" + "errors" "log" + "regexp" + "strconv" "strings" + "text/template" "github.com/fatih/color" "github.com/jesseduffield/gocui" @@ -49,6 +54,153 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s return utils.ResolveTemplate(templateStr, objects) } +func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + return gui.prompt(promptOpts{ + title: title, + initialContent: initialValue, + handleConfirm: func(str string) error { + promptResponses[responseIdx] = str + return wrappedF() + }, + }) +} + +func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { + // need to make a menu here some how + menuItems := make([]*menuItem, len(prompt.Options)) + for i, option := range prompt.Options { + option := option + + nameTemplate := option.Name + if nameTemplate == "" { + // this allows you to only pass values rather than bother with names/descriptions + nameTemplate = option.Value + } + name, err := gui.resolveTemplate(nameTemplate, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + description, err := gui.resolveTemplate(option.Description, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + value, err := gui.resolveTemplate(option.Value, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + menuItems[i] = &menuItem{ + displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)}, + onPress: func() error { + promptResponses[responseIdx] = value + return wrappedF() + }, + } + } + + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) +} + +func (gui *Gui) GenerateMenuCandidates(commandOutput string, filter string, format string) ([]string, error) { + candidates := []string{} + reg, err := regexp.Compile(filter) + if err != nil { + return candidates, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error())) + } + buff := bytes.NewBuffer(nil) + temp, err := template.New("format").Parse(format) + if err != nil { + return candidates, gui.surfaceError(errors.New("unable to parse format, error: " + err.Error())) + } + for _, str := range strings.Split(string(commandOutput), "\n") { + if str == "" { + continue + } + tmplData := map[string]string{} + out := reg.FindAllStringSubmatch(str, -1) + if len(out) > 0 { + for groupIdx, group := range reg.SubexpNames() { + // Record matched group with group ids + matchName := "group_" + strconv.Itoa(groupIdx) + tmplData[matchName] = out[0][groupIdx] + // Record last named group non-empty matches as group matches + if group != "" { + tmplData[group] = out[0][groupIdx] + } + } + } + err = temp.Execute(buff, tmplData) + if err != nil { + return candidates, gui.surfaceError(err) + } + + candidates = append(candidates, strings.TrimSpace(buff.String())) + buff.Reset() + } + return candidates, err +} + +func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { + // Collect cmd to run from config + cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + // Collect Filter regexp + filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + // Run and save output + message, err := gui.GitCommand.RunCommandWithOutput(cmdStr) + if err != nil { + return gui.surfaceError(err) + } + + // Need to make a menu out of what the cmd has displayed + candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.Format) + if err != nil { + return gui.surfaceError(err) + } + + menuItems := make([]*menuItem, len(candidates)) + for i := range candidates { + menuItems[i] = &menuItem{ + displayStrings: []string{candidates[i]}, + onPress: func() error { + promptResponses[responseIdx] = candidates[i] + return wrappedF() + }, + } + } + + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) +} + func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error { return func() error { promptResponses := make([]string, len(customCommand.Prompts)) @@ -89,72 +241,18 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand switch prompt.Type { case "input": f = func() error { - title, err := gui.resolveTemplate(prompt.Title, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - return gui.prompt(promptOpts{ - title: title, - initialContent: initialValue, - handleConfirm: func(str string) error { - promptResponses[idx] = str - - return wrappedF() - }, - }) + return gui.inputPrompt(prompt, promptResponses, idx, wrappedF) } case "menu": f = func() error { - // need to make a menu here some how - menuItems := make([]*menuItem, len(prompt.Options)) - for i, option := range prompt.Options { - option := option - - nameTemplate := option.Name - if nameTemplate == "" { - // this allows you to only pass values rather than bother with names/descriptions - nameTemplate = option.Value - } - name, err := gui.resolveTemplate(nameTemplate, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - description, err := gui.resolveTemplate(option.Description, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - value, err := gui.resolveTemplate(option.Value, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - menuItems[i] = &menuItem{ - displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)}, - onPress: func() error { - promptResponses[idx] = value - - return wrappedF() - }, - } - } - - title, err := gui.resolveTemplate(prompt.Title, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + return gui.menuPrompt(prompt, promptResponses, idx, wrappedF) + } + case "menuFromCommand": + f = func() error { + return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF) } default: - return gui.createErrorPanel("custom command prompt must have a type of 'input' or 'menu'") + return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'") } } diff --git a/pkg/gui/dummies.go b/pkg/gui/dummies.go new file mode 100644 index 000000000..d8a7cba29 --- /dev/null +++ b/pkg/gui/dummies.go @@ -0,0 +1,21 @@ +package gui + +import ( + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/updates" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// NewDummyGui creates a new dummy GUI for testing +func NewDummyUpdater() *updates.Updater { + DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), config.NewDummyAppConfig(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog())) + return DummyUpdater +} + +func NewDummyGui() *Gui { + DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig(), NewDummyUpdater(), "", false) + return DummyGui +} diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go index 8e37cecfe..ec1279608 100644 --- a/pkg/gui/gui_test.go +++ b/pkg/gui/gui_test.go @@ -80,3 +80,52 @@ func runCmdHeadless(cmd *exec.Cmd) error { return f.Close() } + +func TestGuiGenerateMenuCandidates(t *testing.T) { + type scenario struct { + testName string + cmdOut string + filter string + format string + test func([]string, error) + } + + scenarios := []scenario{ + { + "Extract remote branch name", + "upstream/pr-1", + "upstream/(?P<branch>.*)", + "{{ .branch }}", + func(actual []string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "pr-1", actual[0]) + }, + }, + { + "Multiple named groups", + "upstream/pr-1", + "(?P<remote>[a-z]*)/(?P<branch>.*)", + "{{ .branch }}|{{ .remote }}", + func(actual []string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "pr-1|upstream", actual[0]) + }, + }, + { + "Multiple named groups with group ids", + "upstream/pr-1", + "(?P<remote>[a-z]*)/(?P<branch>.*)", + "{{ .group_2 }}|{{ .group_1 }}", + func(actual []string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "pr-1|upstream", actual[0]) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.format)) + }) + } +} |