Skip to content

Commit

Permalink
feat: Support multiple commit prefixes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ChrisMcD1 committed Feb 16, 2025
1 parent 01eece3 commit c9d6f2f
Show file tree
Hide file tree
Showing 14 changed files with 395 additions and 64 deletions.
35 changes: 20 additions & 15 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,14 +341,6 @@ git:
# If true, do not allow force pushes
disableForcePushing: false

# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
commitPrefix:
# pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use "^\\w+\\/(\\w+-\\w+).*"
pattern: ""

# Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use "[$1] "
replace: ""

# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-branch-name-prefix
branchPrefix: ""

Expand Down Expand Up @@ -922,27 +914,40 @@ Where:
## Predefined commit message prefix

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.
If you define multiple naming patterns, they will be attempted in order until one matches.

Example:
Example hitting first match:

- Branch name: feature/AB-123
- Commit message: [AB-123] Adding feature
- Generated commit message prefix: [AB-123]

Example hitting second match:

- Branch name: CD-456_fix_problem
- Generated commit message prefix: (CD-456)

```yaml
git:
commitPrefix:
pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
- pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
- pattern: "^([^_]+)_.*" # Take all text prior to the first underscore
replace: '($1) '
```

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.
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.

```yaml
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
- pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
commitPrefix:
- pattern: "^(\\w+)-.*" # A more general match for any leading word
replace : '[$1] '
- pattern: ".*" # The final fallthrough regex that copies over the whole branch name
replace : '[$0] '
```

> [!IMPORTANT]
Expand Down
79 changes: 69 additions & 10 deletions pkg/config/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,26 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
// from one container to another, or changing the type of a key (e.g. from bool
// to an enum).
func migrateUserConfig(path string, content []byte) ([]byte, error) {
changedContent, err := computeMigratedConfig(path, content)
if err != nil {
return nil, err
}

// Write config back if changed
if string(changedContent) != string(content) {
fmt.Println("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file...")
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
return nil, fmt.Errorf("While attempting to write back fixed user config to %s, an error occurred: %s", path, err)
}
fmt.Printf("Success. New config written to %s\n", path)
return changedContent, nil
}

return content, nil
}

// A pure function helper for testing purposes
func computeMigratedConfig(path string, content []byte) ([]byte, error) {
changedContent := content

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

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

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

return content, nil
return changedContent, nil
}

func changeNullKeybindingsToDisabled(changedContent []byte) ([]byte, error) {
Expand All @@ -267,6 +286,46 @@ func changeNullKeybindingsToDisabled(changedContent []byte) ([]byte, error) {
})
}

func changeElementToSequence(changedContent []byte, path []string) ([]byte, error) {
return yaml_utils.TransformNode(changedContent, path, func(node *yaml.Node) (bool, error) {
if node.Kind == yaml.MappingNode {
nodeContentCopy := node.Content
node.Kind = yaml.SequenceNode
node.Value = ""
node.Tag = "!!seq"
node.Content = []*yaml.Node{{
Kind: yaml.MappingNode,
Content: nodeContentCopy,
}}

return true, nil
}
return false, nil
})
}

func changeCommitPrefixesMap(changedContent []byte) ([]byte, error) {
return yaml_utils.TransformNode(changedContent, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) (bool, error) {
if prefixesNode.Kind == yaml.MappingNode {
for _, contentNode := range prefixesNode.Content {
if contentNode.Kind == yaml.MappingNode {
nodeContentCopy := contentNode.Content
contentNode.Kind = yaml.SequenceNode
contentNode.Value = ""
contentNode.Tag = "!!seq"
contentNode.Content = []*yaml.Node{{
Kind: yaml.MappingNode,
Content: nodeContentCopy,
}}

}
}
return true, nil
}
return false, nil
})
}

func (c *AppConfig) GetDebug() bool {
return c.debug
}
Expand Down
78 changes: 78 additions & 0 deletions pkg/config/app_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)

func TestCommitPrefixMigrations(t *testing.T) {
scenarios := []struct {
name string
input string
expected string
}{
{
"Empty String",
"",
"",
}, {
"Single CommitPrefix Rename",
`
git:
commitPrefix:
pattern: "^\\w+-\\w+.*"
replace: '[JIRA $0] '`,
`
git:
commitPrefix:
- pattern: "^\\w+-\\w+.*"
replace: '[JIRA $0] '`,
}, {
"Complicated CommitPrefixes Rename",
`
git:
commitPrefixes:
foo:
pattern: "^\\w+-\\w+.*"
replace: '[OTHER $0] '
CrazyName!@#$^*&)_-)[[}{f{[]:
pattern: "^foo.bar*"
replace: '[FUN $0] '`,
`
git:
commitPrefixes:
foo:
- pattern: "^\\w+-\\w+.*"
replace: '[OTHER $0] '
CrazyName!@#$^*&)_-)[[}{f{[]:
- pattern: "^foo.bar*"
replace: '[FUN $0] '`,
}, {
"Incomplete Configuration",
"git:",
"git:",
},
}

for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
expectedConfig := GetDefaultConfig()
err := yaml.Unmarshal([]byte(s.expected), expectedConfig)
if err != nil {
t.Error(err)
}
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
if err != nil {
t.Error(err)
}
actualConfig := GetDefaultConfig()
err = yaml.Unmarshal(actual, actualConfig)
if err != nil {
t.Error(err)
}
assert.Equal(t, expectedConfig, actualConfig)
})
}
}
6 changes: 3 additions & 3 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,9 @@ type GitConfig struct {
// If true, do not allow force pushes
DisableForcePushing bool `yaml:"disableForcePushing"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
CommitPrefix *CommitPrefixConfig `yaml:"commitPrefix"`
CommitPrefix []CommitPrefixConfig `yaml:"commitPrefix"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
CommitPrefixes map[string][]CommitPrefixConfig `yaml:"commitPrefixes"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-branch-name-prefix
BranchPrefix string `yaml:"branchPrefix"`
// If true, parse emoji strings in commit messages e.g. render :rocket: as 🚀
Expand Down Expand Up @@ -784,7 +784,7 @@ func GetDefaultConfig() *UserConfig {
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",
DisableForcePushing: false,
CommitPrefixes: map[string]CommitPrefixConfig(nil),
CommitPrefixes: map[string][]CommitPrefixConfig(nil),
BranchPrefix: "",
ParseEmoji: false,
TruncateCopiedCommitHashesTo: 12,
Expand Down
13 changes: 7 additions & 6 deletions pkg/gui/controllers/helpers/working_tree_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.c.Contexts().CommitMessage.GetPreservedMessageAndLogError()

if message == "" {
commitPrefixConfig := self.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
commitPrefixConfigs := self.commitPrefixConfigsForRepo()
for _, commitPrefixConfig := range commitPrefixConfigs {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
branchName := self.refHelper.GetCheckedOutRef().Name
Expand All @@ -165,6 +165,7 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
if rgx.MatchString(branchName) {
prefix := rgx.ReplaceAllString(branchName, prefixReplace)
message = prefix
break
}
}
}
Expand Down Expand Up @@ -228,11 +229,11 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error {
return nil
}

func (self *WorkingTreeHelper) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
func (self *WorkingTreeHelper) commitPrefixConfigsForRepo() []config.CommitPrefixConfig {
cfg, ok := self.c.UserConfig().Git.CommitPrefixes[self.c.Git().RepoPaths.RepoName()]
if ok {
return &cfg
return append(cfg, self.c.UserConfig().Git.CommitPrefix...)
} else {
return self.c.UserConfig().Git.CommitPrefix
}

return self.c.UserConfig().Git.CommitPrefix
}
2 changes: 1 addition & 1 deletion pkg/integration/tests/commit/commit_wip_with_prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ var CommitWipWithPrefix = NewIntegrationTest(NewIntegrationTestArgs{
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.CommitPrefixes = map[string]config.CommitPrefixConfig{"repo": {Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}
cfg.GetUserConfig().Git.CommitPrefixes = map[string][]config.CommitPrefixConfig{"repo": {{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("feature/TEST-002")
Expand Down
53 changes: 53 additions & 0 deletions pkg/integration/tests/commit/commit_with_fallthrough_prefix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package commit

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

var CommitWithFallthroughPrefix = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Commit with multiple CommitPrefixConfig",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{
{Pattern: "^doesntmatch-(\\w+).*", Replace: "[BAD $1]: "},
{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[GOOD $1]: "},
}
cfg.GetUserConfig().Git.CommitPrefixes = map[string][]config.CommitPrefixConfig{
"DifferentProject": {{Pattern: "^otherthatdoesn'tmatch-(\\w+).*", Replace: "[BAD $1]: "}},
}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("feature/TEST-001")
shell.CreateFile("test-commit-prefix", "This is foo bar")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
IsEmpty()

t.Views().Files().
IsFocused().
PressPrimaryAction().
Press(keys.Files.CommitChanges)

t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("[GOOD TEST-001]: ")).
Type("my commit message").
Cancel()

t.Views().Files().
IsFocused().
Press(keys.Files.CommitChanges)

t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("[GOOD TEST-001]: my commit message")).
Type(". Added something else").
Confirm()

t.Views().Commits().Focus()
t.Views().Main().Content(Contains("[GOOD TEST-001]: my commit message. Added something else"))
},
})
2 changes: 1 addition & 1 deletion pkg/integration/tests/commit/commit_with_global_prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ var CommitWithGlobalPrefix = NewIntegrationTest(NewIntegrationTestArgs{
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.CommitPrefix = &config.CommitPrefixConfig{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{{Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("feature/TEST-001")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ var CommitWithNonMatchingBranchName = NewIntegrationTest(NewIntegrationTestArgs{
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.CommitPrefix = &config.CommitPrefixConfig{
cfg.GetUserConfig().Git.CommitPrefix = []config.CommitPrefixConfig{{
Pattern: "^\\w+\\/(\\w+-\\w+).*",
Replace: "[$1]: ",
}
}}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("branchnomatch")
Expand Down
Loading

0 comments on commit c9d6f2f

Please sign in to comment.