Skip to content

Add runCommand function to Go template syntax + add support for templates in git branchPrefix setting #4438

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

Merged
merged 2 commits into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,15 @@ git:
branchPrefix: "firstlast/"
```

It's possible to use a dynamic prefix by using the `runCommand` function:

```yaml
git:
branchPrefix: "firstlast/{{ runCommand "date +\"%Y/%-m\"" }}/"
```

This would produce something like: `firstlast/2025/4/`

## Custom git log command

You can override the `git log` command that's used to render the log of the selected branch like so:
Expand Down
18 changes: 18 additions & 0 deletions docs/Custom_Command_Keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,24 @@ We don't support accessing all elements of a range selection yet. We might add t
command: "git format-patch {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}}"
```

We support the following functions:

### Quoting

Quote wraps a string in quotes with necessary escaping for the current platform.

```
git {{.SelectedFile.Name | quote}}
```

### Running a command

Runs a command and returns the output. If the command outputs more than a single line, it will produce an error.

```
initialValue: "username/{{ runCommand "date +\"%Y/%-m\"" }}/"
```

## Keybinding collisions

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)
Expand Down
22 changes: 21 additions & 1 deletion pkg/commands/git_commands/custom.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package git_commands

import "github.com/mgutz/str"
import (
"fmt"
"strings"

"github.com/mgutz/str"
)

type CustomCommands struct {
*GitCommon
Expand All @@ -18,3 +23,18 @@ func NewCustomCommands(gitCommon *GitCommon) *CustomCommands {
func (self *CustomCommands) RunWithOutput(cmdStr string) (string, error) {
return self.cmd.New(str.ToArgv(cmdStr)).RunWithOutput()
}

// A function that can be used as a "runCommand" entry in the template.FuncMap of templates.
func (self *CustomCommands) TemplateFunctionRunCommand(cmdStr string) (string, error) {
output, err := self.RunWithOutput(cmdStr)
if err != nil {
return "", err
}
output = strings.TrimRight(output, "\r\n")

if strings.Contains(output, "\r\n") {
return "", fmt.Errorf("command output contains newlines: %s", output)
}

return output, nil
}
11 changes: 10 additions & 1 deletion pkg/gui/controllers/helpers/refs_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package helpers
import (
"fmt"
"strings"
"text/template"

"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
Expand Down Expand Up @@ -318,7 +319,15 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
)

if suggestedBranchName == "" {
suggestedBranchName = self.c.UserConfig().Git.BranchPrefix
var err error
Copy link
Owner

Choose a reason for hiding this comment

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

Does this need to be declared separately?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I think so. Without this you'd have to say := below, but that makes suggestedBranchName a new variable in that scope, so that doesn't work.

Copy link
Owner

Choose a reason for hiding this comment

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

ah yes, makes sense


suggestedBranchName, err = utils.ResolveTemplate(self.c.UserConfig().Git.BranchPrefix, nil, template.FuncMap{
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
})
if err != nil {
return err
}
suggestedBranchName = strings.ReplaceAll(suggestedBranchName, "\t", " ")
}

refresh := func() error {
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/services/custom_commands/handler_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ func (self *HandlerCreator) getResolveTemplateFn(form map[string]string, promptR
}

funcs := template.FuncMap{
"quote": self.c.OS().Quote,
"quote": self.c.OS().Quote,
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
}

return func(templateStr string) (string, error) { return utils.ResolveTemplate(templateStr, objects, funcs) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package commit
package branch

import (
"github.com/jesseduffield/lazygit/pkg/config"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package branch

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var NewBranchWithPrefixUsingRunCommand = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Creating a new branch with a branch prefix using a runCommand",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.BranchPrefix = "myprefix/{{ runCommand \"echo dynamic\" }}/"
},
SetupRepo: func(shell *Shell) {
shell.
EmptyCommit("commit 1")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit 1").IsSelected(),
).
SelectNextItem().
Press(keys.Universal.New).
Tap(func() {
t.ExpectPopup().Prompt().
Title(Contains("New branch name")).
InitialText(Equals("myprefix/dynamic/")).
Type("my-branch").
Confirm()
t.Git().CurrentBranchName("myprefix/dynamic/my-branch")
})
},
})
42 changes: 42 additions & 0 deletions pkg/integration/tests/custom_commands/run_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package custom_commands

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var RunCommand = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Using a custom command that uses runCommand template function in a prompt step",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("blah")
},
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{
Key: "a",
Context: "localBranches",
Command: `git checkout {{.Form.Branch}}`,
Prompts: []config.CustomCommandPrompt{
{
Key: "Branch",
Type: "input",
Title: "Enter a branch name",
InitialValue: "myprefix/{{ runCommand \"echo dynamic\" }}/",
},
},
},
}
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Press("a")

t.ExpectPopup().Prompt().
Title(Equals("Enter a branch name")).
InitialText(Contains("myprefix/dynamic/")).
Confirm()
},
})
4 changes: 3 additions & 1 deletion pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ var tests = []*components.IntegrationTest{
branch.NewBranchAutostash,
branch.NewBranchFromRemoteTrackingDifferentName,
branch.NewBranchFromRemoteTrackingSameName,
branch.NewBranchWithPrefix,
branch.NewBranchWithPrefixUsingRunCommand,
branch.OpenPullRequestInvalidTargetRemoteName,
branch.OpenPullRequestNoUpstream,
branch.OpenPullRequestSelectRemoteAndTargetBranch,
Expand Down Expand Up @@ -118,7 +120,6 @@ var tests = []*components.IntegrationTest{
commit.History,
commit.HistoryComplex,
commit.NewBranch,
commit.NewBranchWithPrefix,
commit.PasteCommitMessage,
commit.PasteCommitMessageOverExisting,
commit.PreserveCommitMessage,
Expand Down Expand Up @@ -154,6 +155,7 @@ var tests = []*components.IntegrationTest{
custom_commands.MenuFromCommandsOutput,
custom_commands.MultipleContexts,
custom_commands.MultiplePrompts,
custom_commands.RunCommand,
custom_commands.SelectedCommit,
custom_commands.SelectedCommitRange,
custom_commands.SelectedPath,
Expand Down