From a741cfd5e1a01232a59c632c1bf9b2fa0a53948f Mon Sep 17 00:00:00 2001 From: Chris McDonnell Date: Mon, 10 Feb 2025 22:34:22 -0500 Subject: [PATCH] feat: Support multiple commit prefixes This implementation, unlike that proposed in https://github.com/jesseduffield/lazygit/pull/4253 keeps the yaml schema easy, and does a migration from the single elements to a sequence of elements. --- docs/Config.md | 27 +++---- pkg/config/app_config.go | 50 +++++++++++++ pkg/config/user_config.go | 6 +- .../helpers/working_tree_helper.go | 13 ++-- .../tests/commit/commit_wip_with_prefix.go | 2 +- .../tests/commit/commit_with_global_prefix.go | 2 +- .../commit_with_non_matching_branch_name.go | 4 +- .../tests/commit/commit_with_prefix.go | 6 +- schema/config.json | 50 +++++++------ vendor/gopkg.in/yaml.v3/yaml.go | 75 +++++++++---------- 10 files changed, 144 insertions(+), 91 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 5d034695bcd..29b3dd6e345 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -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: "" @@ -931,20 +923,29 @@ Example: ```yaml git: commitPrefix: - pattern: "^\\w+\\/(\\w+-\\w+).*" - replace: '[$1] ' + - pattern: "^\\w+\\/(\\w+-\\w+).*" + 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` entry is given higher precedence. 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] ' ``` +> Outside of `my_project`, only the `commitPrefix` entry will be attempted. +Inside of `my_project`, the repository specific pattern will be attempted. +If there is no match, the `commitPrefix` entries will then be attempted in order until a match is found + > [!IMPORTANT] > The way golang regex works is when you use `$n` in the replacement string, where `n` is a number, it puts the nth captured subgroup at that place. If `n` is out of range because there aren't that many capture groups in the regex, it puts an empty string there. > diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 5d240b87d04..0450bcac123 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "regexp" "strings" "time" @@ -241,6 +242,16 @@ func migrateUserConfig(path string, content []byte) ([]byte, error) { return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err) } + changedContent, err = changeElementToSequence(changedContent, "git.commitPrefix") + if err != nil { + return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err) + } + + 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... // Write config back if changed @@ -267,6 +278,45 @@ func changeNullKeybindingsToDisabled(changedContent []byte) ([]byte, error) { }) } +func changeElementToSequence(changedContent []byte, targetPath string) ([]byte, error) { + return yaml_utils.Walk(changedContent, func(node *yaml.Node, path string) bool { + if path == targetPath && node.Kind == yaml.MappingNode { + nodeContentCopy := node.Content + node.Kind = yaml.SequenceNode + node.Value = "" + node.Tag = "tag:yaml.org,2002:seq" + node.Content = []*yaml.Node{{ + Kind: yaml.MappingNode, + Content: nodeContentCopy, + }} + + return true + } + return false + }) +} + +func changeCommitPrefixesMap(changedContent []byte) ([]byte, error) { + positiveRegex := regexp.MustCompile(`^git\.commitPrefixes\..+$`) + return yaml_utils.Walk(changedContent, func(node *yaml.Node, path string) bool { + // Checking Column == 13 ensures that we are on the 3rd indented element. Even if the input yaml has 2 space indentation, the parser has turned it into 4 now + if positiveRegex.FindStringIndex(path) != nil && node.Column == 13 && node.Kind == yaml.MappingNode { + fmt.Println("Found a positiveRegex at column", node.Column) + nodeContentCopy := node.Content + node.Kind = yaml.SequenceNode + node.Value = "" + node.Tag = "tag:yaml.org,2002:seq" + node.Content = []*yaml.Node{{ + Kind: yaml.MappingNode, + Content: nodeContentCopy, + }} + + return true + } + return false + }) +} + func (c *AppConfig) GetDebug() bool { return c.debug } diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 3df5c5a9ba4..911c38c98d9 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -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 🚀 @@ -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, diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index 6f6e0eaaba2..c967fab92cf 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -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 @@ -165,6 +165,7 @@ func (self *WorkingTreeHelper) HandleCommitPress() error { if rgx.MatchString(branchName) { prefix := rgx.ReplaceAllString(branchName, prefixReplace) message = prefix + break } } } @@ -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 } diff --git a/pkg/integration/tests/commit/commit_wip_with_prefix.go b/pkg/integration/tests/commit/commit_wip_with_prefix.go index a39a168fe95..6223de04fc4 100644 --- a/pkg/integration/tests/commit/commit_wip_with_prefix.go +++ b/pkg/integration/tests/commit/commit_wip_with_prefix.go @@ -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") diff --git a/pkg/integration/tests/commit/commit_with_global_prefix.go b/pkg/integration/tests/commit/commit_with_global_prefix.go index f5e67fba3e9..ceb2314c1e0 100644 --- a/pkg/integration/tests/commit/commit_with_global_prefix.go +++ b/pkg/integration/tests/commit/commit_with_global_prefix.go @@ -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") diff --git a/pkg/integration/tests/commit/commit_with_non_matching_branch_name.go b/pkg/integration/tests/commit/commit_with_non_matching_branch_name.go index 98f35d6d23f..d08264d210f 100644 --- a/pkg/integration/tests/commit/commit_with_non_matching_branch_name.go +++ b/pkg/integration/tests/commit/commit_with_non_matching_branch_name.go @@ -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") diff --git a/pkg/integration/tests/commit/commit_with_prefix.go b/pkg/integration/tests/commit/commit_with_prefix.go index fa49b0baf83..09bbf63b442 100644 --- a/pkg/integration/tests/commit/commit_with_prefix.go +++ b/pkg/integration/tests/commit/commit_with_prefix.go @@ -10,11 +10,11 @@ var CommitWithPrefix = NewIntegrationTest(NewIntegrationTestArgs{ ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(cfg *config.AppConfig) { - cfg.GetUserConfig().Git.CommitPrefixes = map[string]config.CommitPrefixConfig{ - "repo": { + cfg.GetUserConfig().Git.CommitPrefixes = map[string][]config.CommitPrefixConfig{ + "repo": {{ Pattern: `^\w+/(\w+-\w+).*`, Replace: "[$1]: ", - }, + }}, } }, SetupRepo: func(shell *Shell) { diff --git a/schema/config.json b/schema/config.json index 8ce2c57389e..add8966246d 100644 --- a/schema/config.json +++ b/schema/config.json @@ -638,28 +638,7 @@ "default": false }, "commitPrefix": { - "properties": { - "pattern": { - "type": "string", - "description": "pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use \"^\\\\w+\\\\/(\\\\w+-\\\\w+).*\"", - "examples": [ - "^\\w+\\/(\\w+-\\w+).*" - ] - }, - "replace": { - "type": "string", - "description": "Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use \"[$1] \"", - "examples": [ - "[$1]" - ] - } - }, - "additionalProperties": false, - "type": "object", - "description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix" - }, - "commitPrefixes": { - "additionalProperties": { + "items": { "properties": { "pattern": { "type": "string", @@ -679,6 +658,33 @@ "additionalProperties": false, "type": "object" }, + "type": "array", + "description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix" + }, + "commitPrefixes": { + "additionalProperties": { + "items": { + "properties": { + "pattern": { + "type": "string", + "description": "pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use \"^\\\\w+\\\\/(\\\\w+-\\\\w+).*\"", + "examples": [ + "^\\w+\\/(\\w+-\\w+).*" + ] + }, + "replace": { + "type": "string", + "description": "Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use \"[$1] \"", + "examples": [ + "[$1]" + ] + } + }, + "additionalProperties": false, + "type": "object" + }, + "type": "array" + }, "type": "object", "description": "See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix" }, diff --git a/vendor/gopkg.in/yaml.v3/yaml.go b/vendor/gopkg.in/yaml.v3/yaml.go index 8cec6da48d3..f0bedf3d63c 100644 --- a/vendor/gopkg.in/yaml.v3/yaml.go +++ b/vendor/gopkg.in/yaml.v3/yaml.go @@ -17,8 +17,7 @@ // // Source code and other details for the project are available at GitHub: // -// https://github.com/go-yaml/yaml -// +// https://github.com/go-yaml/yaml package yaml import ( @@ -75,16 +74,15 @@ type Marshaler interface { // // For example: // -// type T struct { -// F int `yaml:"a,omitempty"` -// B int -// } -// var t T -// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// var t T +// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) // // See the documentation of Marshal for the format of tags and a list of // supported tag options. -// func Unmarshal(in []byte, out interface{}) (err error) { return unmarshal(in, out, false) } @@ -185,36 +183,35 @@ func unmarshal(in []byte, out interface{}, strict bool) (err error) { // // The field tag format accepted is: // -// `(...) yaml:"[][,[,]]" (...)` +// `(...) yaml:"[][,[,]]" (...)` // // The following flags are currently supported: // -// omitempty Only include the field if it's not set to the zero -// value for the type or to empty slices or maps. -// Zero valued structs will be omitted if all their public -// fields are zero, unless they implement an IsZero -// method (see the IsZeroer interface type), in which -// case the field will be excluded if IsZero returns true. +// omitempty Only include the field if it's not set to the zero +// value for the type or to empty slices or maps. +// Zero valued structs will be omitted if all their public +// fields are zero, unless they implement an IsZero +// method (see the IsZeroer interface type), in which +// case the field will be excluded if IsZero returns true. // -// flow Marshal using a flow style (useful for structs, -// sequences and maps). +// flow Marshal using a flow style (useful for structs, +// sequences and maps). // -// inline Inline the field, which must be a struct or a map, -// causing all of its fields or keys to be processed as if -// they were part of the outer struct. For maps, keys must -// not conflict with the yaml keys of other struct fields. +// inline Inline the field, which must be a struct or a map, +// causing all of its fields or keys to be processed as if +// they were part of the outer struct. For maps, keys must +// not conflict with the yaml keys of other struct fields. // // In addition, if the key is "-", the field is ignored. // // For example: // -// type T struct { -// F int `yaml:"a,omitempty"` -// B int -// } -// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" -// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" -// +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" +// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" func Marshal(in interface{}) (out []byte, err error) { defer handleErr(&err) e := newEncoder() @@ -358,22 +355,21 @@ const ( // // For example: // -// var person struct { -// Name string -// Address yaml.Node -// } -// err := yaml.Unmarshal(data, &person) -// -// Or by itself: +// var person struct { +// Name string +// Address yaml.Node +// } +// err := yaml.Unmarshal(data, &person) // -// var person Node -// err := yaml.Unmarshal(data, &person) +// Or by itself: // +// var person Node +// err := yaml.Unmarshal(data, &person) type Node struct { // Kind defines whether the node is a document, a mapping, a sequence, // a scalar value, or an alias to another node. The specific data type of // scalar nodes may be obtained via the ShortTag and LongTag methods. - Kind Kind + Kind Kind // Style allows customizing the apperance of the node in the tree. Style Style @@ -421,7 +417,6 @@ func (n *Node) IsZero() bool { n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 } - // LongTag returns the long form of the tag that indicates the data type for // the node. If the Tag field isn't explicitly defined, one will be computed // based on the node properties.