summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorMark Kopenga <mkopenga@gmail.com>2021-07-22 19:54:14 +0200
committerGitHub <noreply@github.com>2021-07-22 19:54:14 +0200
commitc005b0d92bcb03403582a36674d3ad8801b7806a (patch)
treeeab0691d59061147fb1cac03a3779da5f343a7f3 /pkg
parent1573a449f84846657b7ac9e07756caa9db548b0e (diff)
parent713fae3e32a0482314a9489d3169ca62af2417de (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.go5
-rw-r--r--pkg/gui/custom_commands.go220
-rw-r--r--pkg/gui/dummies.go21
-rw-r--r--pkg/gui/gui_test.go49
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))
+ })
+ }
+}