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

This makes it possible to use date and time in initial values like this:

```yaml
initialValue: 'ruudk/{{ runCommand "date +\"%Y/%-m\"" }}/'
```
![Screenshot 2025-04-02 at 19 41
40@2x](https://github.com/user-attachments/assets/7f5e79c6-c8c9-4047-83da-ee61240ded27)

I want to use this to configure my BranchPrefix like this:

```yaml
git:
  branchPrefix: 'ruudk/{{ runCommand "date +\"%Y/%-m\"" }}/'
```
![Screenshot 2025-04-02 at 19 43
06@2x](https://github.com/user-attachments/assets/c0c517d5-8bc3-4e83-944d-2bf591ac1521)
This commit is contained in:
Stefan Haller 2025-04-09 11:11:07 +02:00 committed by GitHub
commit da0105c16b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 142 additions and 5 deletions

View file

@ -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:

View file

@ -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)

View file

@ -1,6 +1,11 @@
package git_commands
import "github.com/mgutz/str"
import (
"fmt"
"strings"
"github.com/mgutz/str"
)
type CustomCommands struct {
*GitCommon
@ -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
}

View file

@ -3,6 +3,7 @@ package helpers
import (
"fmt"
"strings"
"text/template"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
@ -318,7 +319,15 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
)
if suggestedBranchName == "" {
suggestedBranchName = self.c.UserConfig().Git.BranchPrefix
var err error
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 {

View file

@ -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) }

View file

@ -1,4 +1,4 @@
package commit
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"

View file

@ -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")
})
},
})

View file

@ -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()
},
})

View file

@ -50,6 +50,8 @@ var tests = []*components.IntegrationTest{
branch.NewBranchAutostash,
branch.NewBranchFromRemoteTrackingDifferentName,
branch.NewBranchFromRemoteTrackingSameName,
branch.NewBranchWithPrefix,
branch.NewBranchWithPrefixUsingRunCommand,
branch.OpenPullRequestInvalidTargetRemoteName,
branch.OpenPullRequestNoUpstream,
branch.OpenPullRequestSelectRemoteAndTargetBranch,
@ -118,7 +120,6 @@ var tests = []*components.IntegrationTest{
commit.History,
commit.HistoryComplex,
commit.NewBranch,
commit.NewBranchWithPrefix,
commit.PasteCommitMessage,
commit.PasteCommitMessageOverExisting,
commit.PreserveCommitMessage,
@ -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,