Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate deprecated AllBranchesLogCmd to AllBranchesLogCmds #1

Closed
wants to merge 10 commits into from
6 changes: 3 additions & 3 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,9 @@ git:
# Command used when displaying the current branch git log in the main window
branchLogCmd: git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --

# Command used to display git log of all branches in the main window.
# Deprecated: Use `allBranchesLogCmds` instead.
allBranchesLogCmd: git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium
# Commands used to display git log of all branches in the main window, they will be cycled in order of appearance
allBranchesLogCmds:
- git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium

# If true, do not spawn a separate process when using GPG
overrideGpg: false
Expand Down
23 changes: 22 additions & 1 deletion docs/Custom_Command_Keybindings.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Custom Command Keybindings

You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
You can add custom command keybindings in your config.yml (accessible by pressing 'e' on the status panel from within lazygit) like so:

```yml
customCommands:
Expand Down Expand Up @@ -324,6 +324,27 @@ We don't support accessing all elements of a range selection yet. We might add t

If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)

## Menus of custom commands

For custom commands that are not used very frequently it may be preferable to hide them in a menu; you can assign a key to open the menu, and the commands will appear inside. This has the advantage that you don't have to come up with individual unique keybindings for all those commands that you don't use often; the keybindings for the commands in the menu only need to be unique within the menu. Here is an example:

```yml
customCommands:
- key: X
description: "Copy/paste commits across repos"
commandMenu:
- key: c
command: 'git format-patch --stdout {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy'
context: commits, subCommits
description: "Copy selected commits to clipboard"
- key: v
command: 'pbpaste | git am'
context: "commits"
description: "Paste selected commits from clipboard"
```

If you use the commandMenu property, none of the other properties except key and description can be used.

## Debugging

If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
Expand Down
6 changes: 1 addition & 5 deletions pkg/commands/git_commands/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,7 @@ func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {

func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
// Only choose between non-empty, non-identical commands
candidates := lo.Uniq(lo.WithoutEmpty(append([]string{
self.UserConfig().Git.AllBranchesLogCmd,
},
self.UserConfig().Git.AllBranchesLogCmds...,
)))
candidates := lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially move this logic into the migration, but I didn't feel like doing so. It would mean we would technically want to edit the AllBranchesLogCmds, even if there was no AllBranchesLogCmd, which just adds another code path for little gain.


n := len(candidates)

Expand Down
43 changes: 43 additions & 0 deletions pkg/config/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}

err = migrateAllBranchesLogCmd(&rootNode)
if err != nil {
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}

// Add more migrations here...

if !reflect.DeepEqual(rootNode, originalCopy) {
Expand Down Expand Up @@ -341,6 +346,44 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
})
}

// This migration is special because users have already defined
// a single elemnt at `allBranchesLogCmd` and the sequence at `allBranchesLogCmds`.
// Some users have explicitly set `allBranchesLogCmd` to be an empty string in order
// to remove it, so in that case we just delete the element, and add nothing to the list
func migrateAllBranchesLogCmd(rootNode *yaml.Node) error {
return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error {
cmdKeyNode, cmdValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmd")
// Nothing to do if they do not have the deprecated item
if cmdKeyNode == nil {
return nil
}

cmdsKeyNode, cmdsValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmds")
if cmdsKeyNode == nil {
// Create dummy node and attach it onto the root git node
cmdsKeyNode = &yaml.Node{Kind: yaml.ScalarNode, Value: "allBranchesLogCmds"}
cmdsValueNode = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{}}
gitNode.Content = append(gitNode.Content,
cmdsKeyNode,
cmdsValueNode,
)
}

if cmdValueNode.Value != "" {
if cmdsValueNode.Kind != yaml.SequenceNode {
return fmt.Errorf("You should have an allBranchesLogCmds defined as a sequence!")
}
// Prepending the individual element to make it show up first in the list, which was prior behavior
cmdsValueNode.Content = append([]*yaml.Node{{Kind: yaml.ScalarNode, Value: cmdValueNode.Value}}, cmdsValueNode.Content...)
}

// Clear out the existing allBranchesLogCmd, now that we have migrated it into the list
_, _ = yaml_utils.RemoveKey(gitNode, "allBranchesLogCmd")

return nil
})
}

func (c *AppConfig) GetDebug() bool {
return c.debug
}
Expand Down
79 changes: 79 additions & 0 deletions pkg/config/app_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,3 +695,82 @@ func BenchmarkMigrationOnLargeConfiguration(b *testing.B) {
_, _ = computeMigratedConfig("path doesn't matter", largeConfiguration)
}
}

func TestAllBranchesLogCmdMigrations(t *testing.T) {
scenarios := []struct {
name string
input string
expected string
}{
{
name: "Incomplete Configuration Passes uneventfully",
input: "git:",
expected: "git:",
}, {
name: "Single Cmd with no Cmds",
input: `git:
allBranchesLogCmd: git log --graph --oneline
`,
expected: `git:
allBranchesLogCmds:
- git log --graph --oneline
`,
}, {
name: "Cmd with one existing Cmds",
input: `git:
allBranchesLogCmd: git log --graph --oneline
allBranchesLogCmds:
- git log --graph --oneline --pretty
`,
expected: `git:
allBranchesLogCmds:
- git log --graph --oneline
- git log --graph --oneline --pretty
`,
}, {
name: "Only Cmds set have no changes",
input: `git:
allBranchesLogCmds:
- git log
`,
expected: `git:
allBranchesLogCmds:
- git log
`,
}, {
name: "Removes Empty Cmd When at end of yaml",
input: `git:
allBranchesLogCmds:
- git log --graph --oneline
allBranchesLogCmd:
`,
expected: `git:
allBranchesLogCmds:
- git log --graph --oneline
`,
}, {
name: "Removes Empty Cmd With Keys Afterwards",
input: `git:
allBranchesLogCmds:
- git log --graph --oneline
allBranchesLogCmd:
foo: bar
`,
expected: `git:
allBranchesLogCmds:
- git log --graph --oneline
foo: bar
`,
},
}

for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
if err != nil {
t.Error(err)
}
assert.Equal(t, s.expected, string(actual))
})
}
}
28 changes: 20 additions & 8 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,6 @@ type GitConfig struct {
AutoStageResolvedConflicts bool `yaml:"autoStageResolvedConflicts"`
// Command used when displaying the current branch git log in the main window
BranchLogCmd string `yaml:"branchLogCmd"`
// Command used to display git log of all branches in the main window.
// Deprecated: Use `allBranchesLogCmds` instead.
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
// Commands used to display git log of all branches in the main window, they will be cycled in order of appearance
AllBranchesLogCmds []string `yaml:"allBranchesLogCmds"`
// If true, do not spawn a separate process when using GPG
Expand Down Expand Up @@ -614,26 +611,41 @@ type CustomCommandAfterHook struct {
type CustomCommand struct {
// The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md
Key string `yaml:"key"`
// Instead of defining a single custom command, create a menu of custom commands. Useful for grouping related commands together under a single keybinding, and for keeping them out of the global keybindings menu.
// When using this, all other fields except Key and Description are ignored and must be empty.
CommandMenu []CustomCommand `yaml:"commandMenu"`
// The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for "commits, subCommits" or "files, commitFiles".
Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"`
// The command to run (using Go template syntax for placeholder values)
Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"`
// If true, run the command in a subprocess (e.g. if the command requires user input)
Subprocess bool `yaml:"subprocess"`
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
Subprocess *bool `yaml:"subprocess"`
// A list of prompts that will request user input before running the final command
Prompts []CustomCommandPrompt `yaml:"prompts"`
// Text to display while waiting for command to finish
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`
// Label for the custom command when displayed in the keybindings menu
Description string `yaml:"description"`
// If true, stream the command's output to the Command Log panel
Stream bool `yaml:"stream"`
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
Stream *bool `yaml:"stream"`
// If true, show the command's output in a popup within Lazygit
ShowOutput bool `yaml:"showOutput"`
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
ShowOutput *bool `yaml:"showOutput"`
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
OutputTitle string `yaml:"outputTitle"`
// Actions to take after the command has completed
After CustomCommandAfterHook `yaml:"after"`
// [dev] Pointer so that we can tell whether it appears in the config file
After *CustomCommandAfterHook `yaml:"after"`
}

func (c *CustomCommand) GetDescription() string {
if c.Description != "" {
return c.Description
}

return c.Command
}

type CustomCommandPrompt struct {
Expand Down Expand Up @@ -786,7 +798,7 @@ func GetDefaultConfig() *UserConfig {
FetchAll: true,
AutoStageResolvedConflicts: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
AllBranchesLogCmds: []string{"git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"},
DisableForcePushing: false,
CommitPrefixes: map[string][]CommitPrefixConfig(nil),
BranchPrefix: "",
Expand Down
17 changes: 17 additions & 0 deletions pkg/config/user_config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ func validateCustomCommands(customCommands []CustomCommand) error {
if err := validateCustomCommandKey(customCommand.Key); err != nil {
return err
}

if len(customCommand.CommandMenu) > 0 &&
(len(customCommand.Context) > 0 ||
len(customCommand.Command) > 0 ||
customCommand.Subprocess != nil ||
len(customCommand.Prompts) > 0 ||
len(customCommand.LoadingText) > 0 ||
customCommand.Stream != nil ||
customCommand.ShowOutput != nil ||
len(customCommand.OutputTitle) > 0 ||
customCommand.After != nil) {
commandRef := ""
if len(customCommand.Key) > 0 {
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
}
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
}
}
return nil
}
52 changes: 52 additions & 0 deletions pkg/config/user_config_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,58 @@ func TestUserConfigValidate_enums(t *testing.T) {
{value: "invalid_value", valid: false},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Description: "My Custom Commands",
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: true},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Context: "global",
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: false},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
falseVal := false
config.CustomCommands = []CustomCommand{
{
Key: "X",
Subprocess: &falseVal,
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: false},
},
},
}

for _, s := range scenarios {
Expand Down
Loading
Loading