mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
Add support for git flow using git-flow-next
git-flow-next (https://github.com/gittower/git-flow-next) uses a different config schema than legacy git-flow: gitflow.branch.<type>.prefix instead of gitflow.prefix.<type>. Recognize both schemas in GetGitFlowPrefixMap by querying each and merging into a single prefix → branchType map. GitFlowEnabled now consults the merged map so a next-only setup counts as enabled. When both schemas configure the same prefix, the legacy entry wins. In normal usage both schemas agree, so the rule mainly matters as a deterministic tie-breaker.
This commit is contained in:
committed by
Stefan Haller
parent
415015c66a
commit
dd0d90837d
@@ -596,7 +596,7 @@ See the [docs](docs/Custom_Command_Keybindings.md)
|
||||
|
||||
### Git flow support
|
||||
|
||||
Lazygit supports [Gitflow](https://github.com/nvie/gitflow) if you have it installed. To understand how the Gitflow model works check out Vincent Driessen's original [post](https://nvie.com/posts/a-successful-git-branching-model/) explaining it. To view Gitflow options from within Lazygit, press `i` from within the branches view.
|
||||
Lazygit supports [Gitflow](https://github.com/nvie/gitflow) (or [git-flow-next](https://github.com/gittower/git-flow-next)) if you have it installed. To understand how the Gitflow model works check out Vincent Driessen's original [post](https://nvie.com/posts/a-successful-git-branching-model/) explaining it. To view Gitflow options from within Lazygit, press `i` from within the branches view.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -113,28 +113,50 @@ func (self *ConfigCommands) Branches(cmd oscommands.ICmdObjBuilder) map[string]*
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *ConfigCommands) GetGitFlowPrefixes() string {
|
||||
return self.gitConfig.GetGeneral("--local --get-regexp gitflow.prefix")
|
||||
// git-flow config key patterns: legacy uses gitflow.prefix.<type>, git-flow-next uses gitflow.branch.<type>.prefix
|
||||
const (
|
||||
gitFlowLegacyConfigArgs = "--local --get-regexp gitflow.prefix"
|
||||
gitFlowNextConfigArgs = "--local --get-regexp gitflow\\.branch\\..*\\.prefix"
|
||||
)
|
||||
|
||||
func (self *ConfigCommands) getGitFlowPrefixes() string {
|
||||
return self.gitConfig.GetGeneral(gitFlowLegacyConfigArgs)
|
||||
}
|
||||
|
||||
// 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") {
|
||||
func (self *ConfigCommands) getGitFlowNextPrefixes() string {
|
||||
return self.gitConfig.GetGeneral(gitFlowNextConfigArgs)
|
||||
}
|
||||
|
||||
// parseGitFlowLines parses lines matching re (submatch 1 = branch type, 2 = prefix) into prefixToType.
|
||||
// When overwrite is false, existing keys are left unchanged so legacy entries win over next.
|
||||
func parseGitFlowLines(output string, re *regexp.Regexp, prefixToType map[string]string, overwrite bool) {
|
||||
for line := range strings.SplitSeq(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if m := legacyRegexp.FindStringSubmatch(line); len(m) == 3 {
|
||||
if m := re.FindStringSubmatch(line); len(m) == 3 {
|
||||
prefix := normalizeGitFlowPrefix(m[2])
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
prefixToType[prefix] = m[1]
|
||||
if overwrite || prefixToType[prefix] == "" {
|
||||
prefixToType[prefix] = m[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseGitFlowPrefixMap parses legacy and git-flow-next config output into a unified prefix → branchType map.
|
||||
// Legacy line format: "gitflow.prefix.<type> <prefix>"
|
||||
// Next line format: "gitflow.branch.<type>.prefix <prefix>"
|
||||
// Prefixes are normalized to end in "/". Legacy entries win on duplicate prefix.
|
||||
func parseGitFlowPrefixMap(legacyOutput, nextOutput string) map[string]string {
|
||||
legacyRegexp := regexp.MustCompile(`gitflow\.prefix\.(\S+)\s+(.*)`)
|
||||
nextRegexp := regexp.MustCompile(`gitflow\.branch\.([^.]+)\.prefix\s+(.*)`)
|
||||
prefixToType := make(map[string]string)
|
||||
parseGitFlowLines(legacyOutput, legacyRegexp, prefixToType, true)
|
||||
parseGitFlowLines(nextOutput, nextRegexp, prefixToType, false)
|
||||
return prefixToType
|
||||
}
|
||||
|
||||
@@ -150,7 +172,7 @@ func normalizeGitFlowPrefix(prefix string) string {
|
||||
}
|
||||
|
||||
func (self *ConfigCommands) GetGitFlowPrefixMap() map[string]string {
|
||||
return parseGitFlowPrefixMap(self.GetGitFlowPrefixes())
|
||||
return parseGitFlowPrefixMap(self.getGitFlowPrefixes(), self.getGitFlowNextPrefixes())
|
||||
}
|
||||
|
||||
func (self *ConfigCommands) GetCoreCommentChar() byte {
|
||||
|
||||
@@ -12,38 +12,56 @@ func TestParseGitFlowPrefixMap(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
legacyOutput string
|
||||
nextOutput string
|
||||
expected map[string]string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "empty input",
|
||||
testName: "empty inputs",
|
||||
legacyOutput: "",
|
||||
nextOutput: "",
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
testName: "feature and hotfix",
|
||||
testName: "legacy only",
|
||||
legacyOutput: "gitflow.prefix.feature feature/\ngitflow.prefix.hotfix hotfix/",
|
||||
nextOutput: "",
|
||||
expected: map[string]string{"feature/": "feature", "hotfix/": "hotfix"},
|
||||
},
|
||||
{
|
||||
testName: "prefix normalized with trailing slash",
|
||||
testName: "next only",
|
||||
legacyOutput: "",
|
||||
nextOutput: "gitflow.branch.feature.prefix feature/\ngitflow.branch.release.prefix release/",
|
||||
expected: map[string]string{"feature/": "feature", "release/": "release"},
|
||||
},
|
||||
{
|
||||
testName: "legacy wins on duplicate prefix",
|
||||
legacyOutput: "gitflow.prefix.foo feature/",
|
||||
nextOutput: "gitflow.branch.bar.prefix feature/",
|
||||
expected: map[string]string{"feature/": "foo"},
|
||||
},
|
||||
{
|
||||
testName: "prefix normalized with trailing slash from legacy",
|
||||
legacyOutput: "gitflow.prefix.feature feature",
|
||||
nextOutput: "",
|
||||
expected: map[string]string{"feature/": "feature"},
|
||||
},
|
||||
{
|
||||
testName: "malformed lines skipped",
|
||||
testName: "malformed legacy lines skipped",
|
||||
legacyOutput: "gitflow.prefix.feature feature/\nnot-a-valid-line\ngitflow.prefix.hotfix hotfix/",
|
||||
nextOutput: "",
|
||||
expected: map[string]string{"feature/": "feature", "hotfix/": "hotfix"},
|
||||
},
|
||||
{
|
||||
testName: "blank lines and whitespace ignored",
|
||||
legacyOutput: " \n gitflow.prefix.feature feature/ \n \n ",
|
||||
nextOutput: "",
|
||||
expected: map[string]string{"feature/": "feature"},
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
got := parseGitFlowPrefixMap(s.legacyOutput)
|
||||
got := parseGitFlowPrefixMap(s.legacyOutput, s.nextOutput)
|
||||
assert.Equal(t, s.expected, got)
|
||||
})
|
||||
}
|
||||
@@ -57,17 +75,32 @@ func TestGetGitFlowPrefixMap(t *testing.T) {
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "empty when no config",
|
||||
testName: "empty when both queries empty",
|
||||
gitConfigMockResponses: nil,
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
testName: "correct map from legacy output",
|
||||
testName: "correct map from legacy-only 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: "correct map from git-flow-next-only output",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow\\.branch\\..*\\.prefix": "gitflow.branch.feature.prefix feature/\ngitflow.branch.release.prefix release/",
|
||||
},
|
||||
expected: map[string]string{"feature/": "feature", "release/": "release"},
|
||||
},
|
||||
{
|
||||
testName: "merged map with legacy winning when both have same prefix",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow.prefix": "gitflow.prefix.foo feature/",
|
||||
"--local --get-regexp gitflow\\.branch\\..*\\.prefix": "gitflow.branch.bar.prefix feature/",
|
||||
},
|
||||
expected: map[string]string{"feature/": "foo"},
|
||||
},
|
||||
{
|
||||
testName: "prefix normalized with trailing slash",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
|
||||
@@ -20,7 +20,7 @@ func NewFlowCommands(
|
||||
}
|
||||
|
||||
func (self *FlowCommands) GitFlowEnabled() bool {
|
||||
return self.config.GetGitFlowPrefixes() != ""
|
||||
return len(self.config.GetGitFlowPrefixMap()) > 0
|
||||
}
|
||||
|
||||
func (self *FlowCommands) FinishCmdObj(branchName string) (*oscommands.CmdObj, error) {
|
||||
|
||||
@@ -7,17 +7,56 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitFlowEnabled(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
expected bool
|
||||
gitConfigMockResponses map[string]string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "disabled when no config",
|
||||
expected: false,
|
||||
gitConfigMockResponses: nil,
|
||||
},
|
||||
{
|
||||
testName: "enabled with legacy config",
|
||||
expected: true,
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "enabled with git-flow-next only config",
|
||||
expected: true,
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow\\.branch\\..*\\.prefix": "gitflow.branch.feature.prefix feature/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
})
|
||||
assert.Equal(t, s.expected, instance.GitFlowEnabled())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartCmdObj(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branchType string
|
||||
name string
|
||||
branchName string
|
||||
expected []string
|
||||
}{
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "basic",
|
||||
branchType: "feature",
|
||||
name: "test",
|
||||
branchName: "test",
|
||||
expected: []string{"git", "flow", "feature", "start", "test"},
|
||||
},
|
||||
}
|
||||
@@ -27,7 +66,7 @@ func TestStartCmdObj(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{})
|
||||
|
||||
assert.Equal(t,
|
||||
instance.StartCmdObj(s.branchType, s.name).Args(),
|
||||
instance.StartCmdObj(s.branchType, s.branchName).Args(),
|
||||
s.expected,
|
||||
)
|
||||
})
|
||||
@@ -35,13 +74,14 @@ func TestStartCmdObj(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFinishCmdObj(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branchName string
|
||||
expected []string
|
||||
expectedError string
|
||||
gitConfigMockResponses map[string]string
|
||||
}{
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "not a git flow branch",
|
||||
branchName: "mybranch",
|
||||
@@ -57,7 +97,7 @@ func TestFinishCmdObj(t *testing.T) {
|
||||
gitConfigMockResponses: nil,
|
||||
},
|
||||
{
|
||||
testName: "feature branch with config",
|
||||
testName: "feature branch with legacy config",
|
||||
branchName: "feature/mybranch",
|
||||
expected: []string{"git", "flow", "feature", "finish", "mybranch"},
|
||||
expectedError: "",
|
||||
@@ -65,6 +105,25 @@ func TestFinishCmdObj(t *testing.T) {
|
||||
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "feature branch with git-flow-next only config",
|
||||
branchName: "feature/mybranch",
|
||||
expected: []string{"git", "flow", "feature", "finish", "mybranch"},
|
||||
expectedError: "",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow\\.branch\\..*\\.prefix": "gitflow.branch.feature.prefix feature/",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "legacy wins when both configs have same prefix",
|
||||
branchName: "feature/mybranch",
|
||||
expected: []string{"git", "flow", "foo", "finish", "mybranch"},
|
||||
expectedError: "",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow.prefix": "gitflow.prefix.foo feature/",
|
||||
"--local --get-regexp gitflow\\.branch\\..*\\.prefix": "gitflow.branch.bar.prefix feature/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
@@ -76,15 +135,12 @@ func TestFinishCmdObj(t *testing.T) {
|
||||
cmd, err := instance.FinishCmdObj(s.branchName)
|
||||
|
||||
if s.expectedError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
} else {
|
||||
assert.Equal(t, err.Error(), s.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cmd.Args(), s.expected)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, s.expectedError, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expected, cmd.Args())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user