Pull git-flow prefix parsing into a config-level helper

Lift the inline parsing in FinishCmdObj into parseGitFlowPrefixMap on
ConfigCommands. The caller now does a direct map lookup against the
parsed prefix → branchType map instead of iterating the raw config
output and suffix-matching. This is preparation for adding git-flow-next
support, which needs to merge a second config schema into the same map.

One incidental change: a branch name without a slash now returns
NotAGitFlowBranch immediately, rather than falling through the
line loop with an empty suffix. Previously a configured
gitflow.prefix.X whose value happened to equal the entire branch
name could match — never a useful outcome.
This commit is contained in:
Henry Maddocks
2026-05-26 11:05:22 +02:00
committed by Stefan Haller
parent 53e39dc7b8
commit 415015c66a
3 changed files with 137 additions and 18 deletions
+37
View File
@@ -1,6 +1,7 @@
package git_commands
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
@@ -116,6 +117,42 @@ func (self *ConfigCommands) GetGitFlowPrefixes() string {
return self.gitConfig.GetGeneral("--local --get-regexp gitflow.prefix")
}
// parseGitFlowPrefixMap parses git-flow config output into a prefix → branchType map.
// Line format: "gitflow.prefix.<type> <prefix>". Prefixes are normalized to end in "/".
func parseGitFlowPrefixMap(legacyOutput string) map[string]string {
legacyRegexp := regexp.MustCompile(`gitflow\.prefix\.(\S+)\s+(.*)`)
prefixToType := make(map[string]string)
for line := range strings.SplitSeq(legacyOutput, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if m := legacyRegexp.FindStringSubmatch(line); len(m) == 3 {
prefix := normalizeGitFlowPrefix(m[2])
if prefix == "" {
continue
}
prefixToType[prefix] = m[1]
}
}
return prefixToType
}
func normalizeGitFlowPrefix(prefix string) string {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
return ""
}
if !strings.HasSuffix(prefix, "/") {
return prefix + "/"
}
return prefix
}
func (self *ConfigCommands) GetGitFlowPrefixMap() map[string]string {
return parseGitFlowPrefixMap(self.GetGitFlowPrefixes())
}
func (self *ConfigCommands) GetCoreCommentChar() byte {
if commentCharStr := self.gitConfig.Get("core.commentChar"); len(commentCharStr) == 1 {
return commentCharStr[0]
+94
View File
@@ -0,0 +1,94 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/stretchr/testify/assert"
)
func TestParseGitFlowPrefixMap(t *testing.T) {
type scenario struct {
testName string
legacyOutput string
expected map[string]string
}
scenarios := []scenario{
{
testName: "empty input",
legacyOutput: "",
expected: map[string]string{},
},
{
testName: "feature and hotfix",
legacyOutput: "gitflow.prefix.feature feature/\ngitflow.prefix.hotfix hotfix/",
expected: map[string]string{"feature/": "feature", "hotfix/": "hotfix"},
},
{
testName: "prefix normalized with trailing slash",
legacyOutput: "gitflow.prefix.feature feature",
expected: map[string]string{"feature/": "feature"},
},
{
testName: "malformed lines skipped",
legacyOutput: "gitflow.prefix.feature feature/\nnot-a-valid-line\ngitflow.prefix.hotfix hotfix/",
expected: map[string]string{"feature/": "feature", "hotfix/": "hotfix"},
},
{
testName: "blank lines and whitespace ignored",
legacyOutput: " \n gitflow.prefix.feature feature/ \n \n ",
expected: map[string]string{"feature/": "feature"},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
got := parseGitFlowPrefixMap(s.legacyOutput)
assert.Equal(t, s.expected, got)
})
}
}
func TestGetGitFlowPrefixMap(t *testing.T) {
type scenario struct {
testName string
gitConfigMockResponses map[string]string
expected map[string]string
}
scenarios := []scenario{
{
testName: "empty when no config",
gitConfigMockResponses: nil,
expected: map[string]string{},
},
{
testName: "correct map from legacy output",
gitConfigMockResponses: map[string]string{
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/\ngitflow.prefix.hotfix hotfix/",
},
expected: map[string]string{"feature/": "feature", "hotfix/": "hotfix"},
},
{
testName: "prefix normalized with trailing slash",
gitConfigMockResponses: map[string]string{
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature",
},
expected: map[string]string{"feature/": "feature"},
},
{
testName: "malformed lines skipped",
gitConfigMockResponses: map[string]string{
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/\nnot-a-valid-line\n",
},
expected: map[string]string{"feature/": "feature"},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
config := NewConfigCommands(common.NewDummyCommon(), git_config.NewFakeGitConfig(s.gitConfigMockResponses))
got := config.GetGitFlowPrefixMap()
assert.Equal(t, s.expected, got)
})
}
}
+6 -18
View File
@@ -1,7 +1,6 @@
package git_commands
import (
"regexp"
"strings"
"github.com/go-errors/errors"
@@ -25,26 +24,15 @@ func (self *FlowCommands) GitFlowEnabled() bool {
}
func (self *FlowCommands) FinishCmdObj(branchName string) (*oscommands.CmdObj, error) {
prefixes := self.config.GetGitFlowPrefixes()
prefixMap := self.config.GetGitFlowPrefixMap()
// need to find out what kind of branch this is
prefix := strings.SplitAfterN(branchName, "/", 2)[0]
suffix := strings.Replace(branchName, prefix, "", 1)
branchType := ""
for line := range strings.SplitSeq(strings.TrimSpace(prefixes), "\n") {
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*")
matches := regex.FindAllStringSubmatch(line, 1)
if len(matches) > 0 && len(matches[0]) > 1 {
branchType = matches[0][1]
break
}
}
prefixPart, suffix, ok := strings.Cut(branchName, "/")
if !ok || prefixPart == "" || suffix == "" {
return nil, errors.New(self.Tr.NotAGitFlowBranch)
}
prefix := prefixPart + "/"
branchType := prefixMap[prefix]
if branchType == "" {
return nil, errors.New(self.Tr.NotAGitFlowBranch)
}