Skip to content

Commit da0105c

Browse files
authored
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)
2 parents e127a92 + 1282048 commit da0105c

File tree

9 files changed

+142
-5
lines changed

9 files changed

+142
-5
lines changed

docs/Config.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,15 @@ git:
10271027
branchPrefix: "firstlast/"
10281028
```
10291029

1030+
It's possible to use a dynamic prefix by using the `runCommand` function:
1031+
1032+
```yaml
1033+
git:
1034+
branchPrefix: "firstlast/{{ runCommand "date +\"%Y/%-m\"" }}/"
1035+
```
1036+
1037+
This would produce something like: `firstlast/2025/4/`
1038+
10301039
## Custom git log command
10311040

10321041
You can override the `git log` command that's used to render the log of the selected branch like so:

docs/Custom_Command_Keybindings.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,24 @@ We don't support accessing all elements of a range selection yet. We might add t
320320
command: "git format-patch {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}}"
321321
```
322322

323+
We support the following functions:
324+
325+
### Quoting
326+
327+
Quote wraps a string in quotes with necessary escaping for the current platform.
328+
329+
```
330+
git {{.SelectedFile.Name | quote}}
331+
```
332+
333+
### Running a command
334+
335+
Runs a command and returns the output. If the command outputs more than a single line, it will produce an error.
336+
337+
```
338+
initialValue: "username/{{ runCommand "date +\"%Y/%-m\"" }}/"
339+
```
340+
323341
## Keybinding collisions
324342
325343
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)

pkg/commands/git_commands/custom.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package git_commands
22

3-
import "github.com/mgutz/str"
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/mgutz/str"
8+
)
49

510
type CustomCommands struct {
611
*GitCommon
@@ -18,3 +23,18 @@ func NewCustomCommands(gitCommon *GitCommon) *CustomCommands {
1823
func (self *CustomCommands) RunWithOutput(cmdStr string) (string, error) {
1924
return self.cmd.New(str.ToArgv(cmdStr)).RunWithOutput()
2025
}
26+
27+
// A function that can be used as a "runCommand" entry in the template.FuncMap of templates.
28+
func (self *CustomCommands) TemplateFunctionRunCommand(cmdStr string) (string, error) {
29+
output, err := self.RunWithOutput(cmdStr)
30+
if err != nil {
31+
return "", err
32+
}
33+
output = strings.TrimRight(output, "\r\n")
34+
35+
if strings.Contains(output, "\r\n") {
36+
return "", fmt.Errorf("command output contains newlines: %s", output)
37+
}
38+
39+
return output, nil
40+
}

pkg/gui/controllers/helpers/refs_helper.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package helpers
33
import (
44
"fmt"
55
"strings"
6+
"text/template"
67

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

320321
if suggestedBranchName == "" {
321-
suggestedBranchName = self.c.UserConfig().Git.BranchPrefix
322+
var err error
323+
324+
suggestedBranchName, err = utils.ResolveTemplate(self.c.UserConfig().Git.BranchPrefix, nil, template.FuncMap{
325+
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
326+
})
327+
if err != nil {
328+
return err
329+
}
330+
suggestedBranchName = strings.ReplaceAll(suggestedBranchName, "\t", " ")
322331
}
323332

324333
refresh := func() error {

pkg/gui/services/custom_commands/handler_creator.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ func (self *HandlerCreator) getResolveTemplateFn(form map[string]string, promptR
246246
}
247247

248248
funcs := template.FuncMap{
249-
"quote": self.c.OS().Quote,
249+
"quote": self.c.OS().Quote,
250+
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
250251
}
251252

252253
return func(templateStr string) (string, error) { return utils.ResolveTemplate(templateStr, objects, funcs) }

pkg/integration/tests/commit/new_branch_with_prefix.go renamed to pkg/integration/tests/branch/new_branch_with_prefix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package commit
1+
package branch
22

33
import (
44
"github.com/jesseduffield/lazygit/pkg/config"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package branch
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var NewBranchWithPrefixUsingRunCommand = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Creating a new branch with a branch prefix using a runCommand",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(cfg *config.AppConfig) {
13+
cfg.GetUserConfig().Git.BranchPrefix = "myprefix/{{ runCommand \"echo dynamic\" }}/"
14+
},
15+
SetupRepo: func(shell *Shell) {
16+
shell.
17+
EmptyCommit("commit 1")
18+
},
19+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
20+
t.Views().Commits().
21+
Focus().
22+
Lines(
23+
Contains("commit 1").IsSelected(),
24+
).
25+
SelectNextItem().
26+
Press(keys.Universal.New).
27+
Tap(func() {
28+
t.ExpectPopup().Prompt().
29+
Title(Contains("New branch name")).
30+
InitialText(Equals("myprefix/dynamic/")).
31+
Type("my-branch").
32+
Confirm()
33+
t.Git().CurrentBranchName("myprefix/dynamic/my-branch")
34+
})
35+
},
36+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package custom_commands
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var RunCommand = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Using a custom command that uses runCommand template function in a prompt step",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupRepo: func(shell *Shell) {
13+
shell.EmptyCommit("blah")
14+
},
15+
SetupConfig: func(cfg *config.AppConfig) {
16+
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
17+
{
18+
Key: "a",
19+
Context: "localBranches",
20+
Command: `git checkout {{.Form.Branch}}`,
21+
Prompts: []config.CustomCommandPrompt{
22+
{
23+
Key: "Branch",
24+
Type: "input",
25+
Title: "Enter a branch name",
26+
InitialValue: "myprefix/{{ runCommand \"echo dynamic\" }}/",
27+
},
28+
},
29+
},
30+
}
31+
},
32+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
33+
t.Views().Branches().
34+
Focus().
35+
Press("a")
36+
37+
t.ExpectPopup().Prompt().
38+
Title(Equals("Enter a branch name")).
39+
InitialText(Contains("myprefix/dynamic/")).
40+
Confirm()
41+
},
42+
})

pkg/integration/tests/test_list.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ var tests = []*components.IntegrationTest{
5050
branch.NewBranchAutostash,
5151
branch.NewBranchFromRemoteTrackingDifferentName,
5252
branch.NewBranchFromRemoteTrackingSameName,
53+
branch.NewBranchWithPrefix,
54+
branch.NewBranchWithPrefixUsingRunCommand,
5355
branch.OpenPullRequestInvalidTargetRemoteName,
5456
branch.OpenPullRequestNoUpstream,
5557
branch.OpenPullRequestSelectRemoteAndTargetBranch,
@@ -118,7 +120,6 @@ var tests = []*components.IntegrationTest{
118120
commit.History,
119121
commit.HistoryComplex,
120122
commit.NewBranch,
121-
commit.NewBranchWithPrefix,
122123
commit.PasteCommitMessage,
123124
commit.PasteCommitMessageOverExisting,
124125
commit.PreserveCommitMessage,
@@ -154,6 +155,7 @@ var tests = []*components.IntegrationTest{
154155
custom_commands.MenuFromCommandsOutput,
155156
custom_commands.MultipleContexts,
156157
custom_commands.MultiplePrompts,
158+
custom_commands.RunCommand,
157159
custom_commands.SelectedCommit,
158160
custom_commands.SelectedCommitRange,
159161
custom_commands.SelectedPath,

0 commit comments

Comments
 (0)