Skip to content

Commit 0d155e1

Browse files
authored
feat: Support multiple commit prefixes (#4261)
- **PR Description** This implementation, unlike that proposed in #4253 keeps the yaml schema easy, and does a migration from the single elements to a sequence of elements. Addresses #4194
2 parents a7bfeca + 2fa4ee2 commit 0d155e1

File tree

14 files changed

+394
-64
lines changed

14 files changed

+394
-64
lines changed

docs/Config.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -341,14 +341,6 @@ git:
341341
# If true, do not allow force pushes
342342
disableForcePushing: false
343343

344-
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
345-
commitPrefix:
346-
# pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use "^\\w+\\/(\\w+-\\w+).*"
347-
pattern: ""
348-
349-
# Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use "[$1] "
350-
replace: ""
351-
352344
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-branch-name-prefix
353345
branchPrefix: ""
354346

@@ -922,27 +914,40 @@ Where:
922914
## Predefined commit message prefix
923915

924916
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate commit message with prefix that is parsed from the branch name.
917+
If you define multiple naming patterns, they will be attempted in order until one matches.
925918

926-
Example:
919+
Example hitting first match:
927920

928921
- Branch name: feature/AB-123
929-
- Commit message: [AB-123] Adding feature
922+
- Generated commit message prefix: [AB-123]
923+
924+
Example hitting second match:
925+
926+
- Branch name: CD-456_fix_problem
927+
- Generated commit message prefix: (CD-456)
930928

931929
```yaml
932930
git:
933931
commitPrefix:
934-
pattern: "^\\w+\\/(\\w+-\\w+).*"
935-
replace: '[$1] '
932+
- pattern: "^\\w+\\/(\\w+-\\w+).*"
933+
replace: '[$1] '
934+
- pattern: "^([^_]+)_.*" # Take all text prior to the first underscore
935+
replace: '($1) '
936936
```
937937

938-
If you want repository-specific prefixes, you can map them with `commitPrefixes`. If you have both `commitPrefixes` defined and an entry in `commitPrefixes` for the current repo, the `commitPrefixes` entry is given higher precedence. Repository folder names must be an exact match.
938+
If you want repository-specific prefixes, you can map them with `commitPrefixes`. If you have both entries in `commitPrefix` defined and an repository match in `commitPrefixes` for the current repo, the `commitPrefixes` entries will be attempted first. Repository folder names must be an exact match.
939939

940940
```yaml
941941
git:
942942
commitPrefixes:
943943
my_project: # This is repository folder name
944-
pattern: "^\\w+\\/(\\w+-\\w+).*"
945-
replace: '[$1] '
944+
- pattern: "^\\w+\\/(\\w+-\\w+).*"
945+
replace: '[$1] '
946+
commitPrefix:
947+
- pattern: "^(\\w+)-.*" # A more general match for any leading word
948+
replace : '[$1] '
949+
- pattern: ".*" # The final fallthrough regex that copies over the whole branch name
950+
replace : '[$0] '
946951
```
947952

948953
> [!IMPORTANT]

pkg/config/app_config.go

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
217217
// from one container to another, or changing the type of a key (e.g. from bool
218218
// to an enum).
219219
func migrateUserConfig(path string, content []byte) ([]byte, error) {
220+
changedContent, err := computeMigratedConfig(path, content)
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
// Write config back if changed
226+
if string(changedContent) != string(content) {
227+
fmt.Println("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file...")
228+
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
229+
return nil, fmt.Errorf("While attempting to write back fixed user config to %s, an error occurred: %s", path, err)
230+
}
231+
fmt.Printf("Success. New config written to %s\n", path)
232+
return changedContent, nil
233+
}
234+
235+
return content, nil
236+
}
237+
238+
// A pure function helper for testing purposes
239+
func computeMigratedConfig(path string, content []byte) ([]byte, error) {
220240
changedContent := content
221241

222242
pathsToReplace := []struct {
@@ -241,19 +261,18 @@ func migrateUserConfig(path string, content []byte) ([]byte, error) {
241261
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
242262
}
243263

244-
// Add more migrations here...
264+
changedContent, err = changeElementToSequence(changedContent, []string{"git", "commitPrefix"})
265+
if err != nil {
266+
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
267+
}
245268

246-
// Write config back if changed
247-
if string(changedContent) != string(content) {
248-
fmt.Println("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file...")
249-
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
250-
return nil, fmt.Errorf("While attempting to write back fixed user config to %s, an error occurred: %s", path, err)
251-
}
252-
fmt.Printf("Success. New config written to %s\n", path)
253-
return changedContent, nil
269+
changedContent, err = changeCommitPrefixesMap(changedContent)
270+
if err != nil {
271+
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
254272
}
273+
// Add more migrations here...
255274

256-
return content, nil
275+
return changedContent, nil
257276
}
258277

259278
func changeNullKeybindingsToDisabled(changedContent []byte) ([]byte, error) {
@@ -267,6 +286,46 @@ func changeNullKeybindingsToDisabled(changedContent []byte) ([]byte, error) {
267286
})
268287
}
269288

289+
func changeElementToSequence(changedContent []byte, path []string) ([]byte, error) {
290+
return yaml_utils.TransformNode(changedContent, path, func(node *yaml.Node) (bool, error) {
291+
if node.Kind == yaml.MappingNode {
292+
nodeContentCopy := node.Content
293+
node.Kind = yaml.SequenceNode
294+
node.Value = ""
295+
node.Tag = "!!seq"
296+
node.Content = []*yaml.Node{{
297+
Kind: yaml.MappingNode,
298+
Content: nodeContentCopy,
299+
}}
300+
301+
return true, nil
302+
}
303+
return false, nil
304+
})
305+
}
306+
307+
func changeCommitPrefixesMap(changedContent []byte) ([]byte, error) {
308+
return yaml_utils.TransformNode(changedContent, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) (bool, error) {
309+
if prefixesNode.Kind == yaml.MappingNode {
310+
for _, contentNode := range prefixesNode.Content {
311+
if contentNode.Kind == yaml.MappingNode {
312+
nodeContentCopy := contentNode.Content
313+
contentNode.Kind = yaml.SequenceNode
314+
contentNode.Value = ""
315+
contentNode.Tag = "!!seq"
316+
contentNode.Content = []*yaml.Node{{
317+
Kind: yaml.MappingNode,
318+
Content: nodeContentCopy,
319+
}}
320+
321+
}
322+
}
323+
return true, nil
324+
}
325+
return false, nil
326+
})
327+
}
328+
270329
func (c *AppConfig) GetDebug() bool {
271330
return c.debug
272331
}

pkg/config/app_config_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"gopkg.in/yaml.v3"
8+
)
9+
10+
func TestCommitPrefixMigrations(t *testing.T) {
11+
scenarios := []struct {
12+
name string
13+
input string
14+
expected string
15+
}{
16+
{
17+
"Empty String",
18+
"",
19+
"",
20+
}, {
21+
"Single CommitPrefix Rename",
22+
`
23+
git:
24+
commitPrefix:
25+
pattern: "^\\w+-\\w+.*"
26+
replace: '[JIRA $0] '`,
27+
`
28+
git:
29+
commitPrefix:
30+
- pattern: "^\\w+-\\w+.*"
31+
replace: '[JIRA $0] '`,
32+
}, {
33+
"Complicated CommitPrefixes Rename",
34+
`
35+
git:
36+
commitPrefixes:
37+
foo:
38+
pattern: "^\\w+-\\w+.*"
39+
replace: '[OTHER $0] '
40+
CrazyName!@#$^*&)_-)[[}{f{[]:
41+
pattern: "^foo.bar*"
42+
replace: '[FUN $0] '`,
43+
`
44+
git:
45+
commitPrefixes:
46+
foo:
47+
- pattern: "^\\w+-\\w+.*"
48+
replace: '[OTHER $0] '
49+
CrazyName!@#$^*&)_-)[[}{f{[]:
50+
- pattern: "^foo.bar*"
51+
replace: '[FUN $0] '`,
52+
}, {
53+
"Incomplete Configuration",
54+
"git:",
55+
"git:",
56+
},
57+
}
58+
59+
for _, s := range scenarios {
60+
t.Run(s.name, func(t *testing.T) {
61+
expectedConfig := GetDefaultConfig()
62+
err := yaml.Unmarshal([]byte(s.expected), expectedConfig)
63+
if err != nil {
64+
t.Error(err)
65+
}
66+
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
67+
if err != nil {
68+
t.Error(err)
69+
}
70+
actualConfig := GetDefaultConfig()
71+
err = yaml.Unmarshal(actual, actualConfig)
72+
if err != nil {
73+
t.Error(err)
74+
}
75+
assert.Equal(t, expectedConfig, actualConfig)
76+
})
77+
}
78+
}

pkg/config/user_config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,9 @@ type GitConfig struct {
256256
// If true, do not allow force pushes
257257
DisableForcePushing bool `yaml:"disableForcePushing"`
258258
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
259-
CommitPrefix *CommitPrefixConfig `yaml:"commitPrefix"`
259+
CommitPrefix []CommitPrefixConfig `yaml:"commitPrefix"`
260260
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
261-
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
261+
CommitPrefixes map[string][]CommitPrefixConfig `yaml:"commitPrefixes"`
262262
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-branch-name-prefix
263263
BranchPrefix string `yaml:"branchPrefix"`
264264
// If true, parse emoji strings in commit messages e.g. render :rocket: as 🚀
@@ -784,7 +784,7 @@ func GetDefaultConfig() *UserConfig {
784784
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
785785
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
786786
DisableForcePushing: false,
787-
CommitPrefixes: map[string]CommitPrefixConfig(nil),
787+
CommitPrefixes: map[string][]CommitPrefixConfig(nil),
788788
BranchPrefix: "",
789789
ParseEmoji: false,
790790
TruncateCopiedCommitHashesTo: 12,

pkg/gui/controllers/helpers/working_tree_helper.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
152152
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()
153153

154154
if message == "" {
155-
commitPrefixConfig := self.commitPrefixConfigForRepo()
156-
if commitPrefixConfig != nil {
155+
commitPrefixConfigs := self.commitPrefixConfigsForRepo()
156+
for _, commitPrefixConfig := range commitPrefixConfigs {
157157
prefixPattern := commitPrefixConfig.Pattern
158158
prefixReplace := commitPrefixConfig.Replace
159159
branchName := self.refHelper.GetCheckedOutRef().Name
@@ -165,6 +165,7 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
165165
if rgx.MatchString(branchName) {
166166
prefix := rgx.ReplaceAllString(branchName, prefixReplace)
167167
message = prefix
168+
break
168169
}
169170
}
170171
}
@@ -228,11 +229,11 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error {
228229
return nil
229230
}
230231

231-
func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
232+
func (self *WorkingTreeHelper) commitPrefixConfigsForRepo() []config.CommitPrefixConfig {
232233
cfg, ok := self.c.UserConfig().Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()]
233234
if ok {
234-
return &cfg
235+
return append(cfg, self.c.UserConfig().Git.CommitPrefix...)
236+
} else {
237+
return self.c.UserConfig().Git.CommitPrefix
235238
}
236-
237-
return self.c.UserConfig().Git.CommitPrefix
238239
}

pkg/integration/tests/commit/commit_wip_with_prefix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var CommitWipWithPrefix = NewIntegrationTest(NewIntegrationTestArgs{
1010
ExtraCmdArgs: []string{},
1111
Skip: false,
1212
SetupConfig: func(cfg *config.AppConfig) {
13-
cfg.GetUserConfig().Git.CommitPrefixes = map[string]config.CommitPrefixConfig{"repo": {Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}
13+
cfg.GetUserConfig().Git.CommitPrefixes = map[string][]config.CommitPrefixConfig{"repo": {{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}}
1414
},
1515
SetupRepo: func(shell *Shell) {
1616
shell.NewBranch("feature/TEST-002")
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package commit
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var CommitWithFallthroughPrefix = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Commit with multiple CommitPrefixConfig",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(cfg *config.AppConfig) {
13+
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{
14+
{Pattern: "^doesntmatch-(\\w+).*", Replace: "[BAD $1]: "},
15+
{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[GOOD $1]: "},
16+
}
17+
cfg.GetUserConfig().Git.CommitPrefixes = map[string][]config.CommitPrefixConfig{
18+
"DifferentProject": {{Pattern: "^otherthatdoesn'tmatch-(\\w+).*", Replace: "[BAD $1]: "}},
19+
}
20+
},
21+
SetupRepo: func(shell *Shell) {
22+
shell.NewBranch("feature/TEST-001")
23+
shell.CreateFile("test-commit-prefix", "This is foo bar")
24+
},
25+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
26+
t.Views().Commits().
27+
IsEmpty()
28+
29+
t.Views().Files().
30+
IsFocused().
31+
PressPrimaryAction().
32+
Press(keys.Files.CommitChanges)
33+
34+
t.ExpectPopup().CommitMessagePanel().
35+
Title(Equals("Commit summary")).
36+
InitialText(Equals("[GOOD TEST-001]: ")).
37+
Type("my commit message").
38+
Cancel()
39+
40+
t.Views().Files().
41+
IsFocused().
42+
Press(keys.Files.CommitChanges)
43+
44+
t.ExpectPopup().CommitMessagePanel().
45+
Title(Equals("Commit summary")).
46+
InitialText(Equals("[GOOD TEST-001]: my commit message")).
47+
Type(". Added something else").
48+
Confirm()
49+
50+
t.Views().Commits().Focus()
51+
t.Views().Main().Content(Contains("[GOOD TEST-001]: my commit message. Added something else"))
52+
},
53+
})

pkg/integration/tests/commit/commit_with_global_prefix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var CommitWithGlobalPrefix = NewIntegrationTest(NewIntegrationTestArgs{
1010
ExtraCmdArgs: []string{},
1111
Skip: false,
1212
SetupConfig: func(cfg *config.AppConfig) {
13-
cfg.GetUserConfig().Git.CommitPrefix = &config.CommitPrefixConfig{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}
13+
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}
1414
},
1515
SetupRepo: func(shell *Shell) {
1616
shell.NewBranch("feature/TEST-001")

pkg/integration/tests/commit/commit_with_non_matching_branch_name.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ var CommitWithNonMatchingBranchName = NewIntegrationTest(NewIntegrationTestArgs{
1010
ExtraCmdArgs: []string{},
1111
Skip: false,
1212
SetupConfig: func(cfg *config.AppConfig) {
13-
cfg.GetUserConfig().Git.CommitPrefix = &config.CommitPrefixConfig{
13+
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{{
1414
Pattern: "^\\w+\\/(\\w+-\\w+).*",
1515
Replace: "[$1]: ",
16-
}
16+
}}
1717
},
1818
SetupRepo: func(shell *Shell) {
1919
shell.NewBranch("branchnomatch")

0 commit comments

Comments
 (0)