1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-10-30 23:57:43 +02:00

Add no-ff merge option

This will put whatever git's default merge variant is as the first menu item,
and add a second item which is the opposite (no-ff if the default is ff, and
vice versa).

If users prefer to always have the same option first no matter whether it's
applicable, they can make ff always appear first by setting git's "merge.ff"
config to "true" or "only", or by setting lazygit's "git.merging.args" config to
"--ff" or "--ff-only"; if they want no-ff to appear first, they can do that by
setting git's "merge.ff" config to "false", or by setting lazygit's
"git.merging.args" config to "--no-ff". Which of these they choose depends on
whether they want the config to also apply to other git clients including the
cli, or only to lazygit.
This commit is contained in:
Stefan Haller
2025-10-15 14:59:04 +02:00
parent c88381b9ce
commit 62854026a3
8 changed files with 275 additions and 19 deletions

View File

@@ -285,6 +285,16 @@ func (self *BranchCommands) Merge(branchName string, variant MergeVariant) error
return self.cmd.New(cmdArgs).Run()
}
// Returns whether refName can be fast-forward merged into the current branch
func (self *BranchCommands) CanDoFastForwardMerge(refName string) bool {
cmdArgs := NewGitCmd("merge-base").
Arg("--is-ancestor").
Arg("HEAD", refName).
ToArgv()
err := self.cmd.New(cmdArgs).DontLog().Run()
return err == nil
}
// Only choose between non-empty, non-identical commands
func (self *BranchCommands) allBranchesLogCandidates() []string {
return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))

View File

@@ -97,6 +97,10 @@ func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}
func (self *ConfigCommands) GetMergeFF() string {
return self.gitConfig.Get("merge.ff")
}
func (self *ConfigCommands) DropConfigCache() {
self.gitConfig.DropCache()
}

View File

@@ -381,21 +381,86 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
return errors.New(self.c.Tr.CantMergeBranchIntoItself)
}
wantFastForward, wantNonFastForward := self.fastForwardMergeUserPreference()
canFastForward := self.c.Git().Branch.CanDoFastForwardMerge(refName)
var firstRegularMergeItem *types.MenuItem
var secondRegularMergeItem *types.MenuItem
var fastForwardMergeItem *types.MenuItem
if !wantNonFastForward && (wantFastForward || canFastForward) {
firstRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
Key: 'm',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
fastForwardMergeItem = firstRegularMergeItem
secondRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeNonFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_NON_FAST_FORWARD),
Key: 'n',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeNonFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
} else {
firstRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeNonFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
Key: 'm',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeNonFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
secondRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_FAST_FORWARD),
Key: 'f',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
fastForwardMergeItem = secondRegularMergeItem
}
if !canFastForward {
fastForwardMergeItem.DisabledReason = &types.DisabledReason{
Text: utils.ResolvePlaceholderString(
self.c.Tr.CannotFastForwardMerge,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
}
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Merge,
Items: []*types.MenuItem{
{
Label: self.c.Tr.RegularMerge,
OnPress: self.RegularMerge(refName),
Key: 'm',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
},
firstRegularMergeItem,
secondRegularMergeItem,
{
Label: self.c.Tr.SquashMergeUncommitted,
OnPress: self.SquashMergeUncommitted(refName),
@@ -423,10 +488,10 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
})
}
func (self *MergeAndRebaseHelper) RegularMerge(refName string) func() error {
func (self *MergeAndRebaseHelper) RegularMerge(refName string, variant git_commands.MergeVariant) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.Merge)
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_REGULAR)
err := self.c.Git().Branch.Merge(refName, variant)
return self.CheckMergeOrRebase(err)
}
}
@@ -459,6 +524,31 @@ func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranch
}
}
// Returns wantsFastForward, wantsNonFastForward. These will never both be true, but they can both be false.
func (self *MergeAndRebaseHelper) fastForwardMergeUserPreference() (bool, bool) {
// Check user config first, because it takes precedence over git config
mergingArgs := self.c.UserConfig().Git.Merging.Args
if strings.Contains(mergingArgs, "--ff") { // also covers "--ff-only"
return true, false
}
if strings.Contains(mergingArgs, "--no-ff") {
return false, true
}
// Then check git config
mergeFfConfig := self.c.Git().Config.GetMergeFF()
if mergeFfConfig == "true" || mergeFfConfig == "only" {
return true, false
}
if mergeFfConfig == "false" {
return false, true
}
return false, false
}
func (self *MergeAndRebaseHelper) ResetMarkedBaseCommit() error {
self.c.Modes().MarkedBaseCommit.Reset()
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)

View File

@@ -264,8 +264,11 @@ type TranslationSet struct {
FocusMainView string
Merge string
MergeBranchTooltip string
RegularMerge string
RegularMergeTooltip string
RegularMergeFastForward string
RegularMergeFastForwardTooltip string
CannotFastForwardMerge string
RegularMergeNonFastForward string
RegularMergeNonFastForwardTooltip string
SquashMergeUncommitted string
SquashMergeUncommittedTooltip string
SquashMergeCommitted string
@@ -1351,8 +1354,11 @@ func EnglishTranslationSet() *TranslationSet {
FocusMainView: "Focus main view",
Merge: `Merge`,
MergeBranchTooltip: "View options for merging the selected item into the current branch (regular merge, squash merge)",
RegularMerge: "Regular merge",
RegularMergeTooltip: "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'.",
RegularMergeFastForward: "Regular merge (fast-forward)",
RegularMergeFastForwardTooltip: "Fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}' without creating a merge commit.",
CannotFastForwardMerge: "Cannot fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}'",
RegularMergeNonFastForward: "Regular merge (with merge commit)",
RegularMergeNonFastForwardTooltip: "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}', creating a merge commit.",
SquashMergeUncommitted: "Squash merge and leave uncommitted",
SquashMergeUncommittedTooltip: "Squash merge '{{.selectedBranch}}' into the working tree.",
SquashMergeCommitted: "Squash merge and commit",

View File

@@ -0,0 +1,68 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MergeFastForward = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Merge a branch into another using fast-forward merge",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("original-branch").
EmptyCommit("one").
NewBranch("branch1").
EmptyCommit("branch1").
Checkout("original-branch").
NewBranchFrom("branch2", "original-branch").
EmptyCommit("branch2").
Checkout("original-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("original-branch").IsSelected(),
Contains("branch1"),
Contains("branch2"),
).
SelectNextItem().
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (fast-forward)"),
Contains("Regular merge (with merge commit)"),
).
Select(Contains("Regular merge (fast-forward)")).
Confirm()
t.Views().Commits().
Lines(
Contains("branch1").IsSelected(),
Contains("one"),
)
// Check that branch2 can't be merged using fast-forward
t.Views().Branches().
Focus().
NavigateToLine(Contains("branch2")).
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (with merge commit)"),
Contains("Regular merge (fast-forward)"),
).
Select(Contains("Regular merge (fast-forward)")).
Confirm()
t.ExpectToast(Contains("Cannot fast-forward 'original-branch' to 'branch2'"))
},
})

View File

@@ -0,0 +1,76 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MergeNonFastForward = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Merge a branch into another using non-fast-forward merge",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("original-branch").
EmptyCommit("one").
NewBranch("branch1").
EmptyCommit("branch1").
Checkout("original-branch").
NewBranchFrom("branch2", "original-branch").
EmptyCommit("branch2").
Checkout("original-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("original-branch").IsSelected(),
Contains("branch1"),
Contains("branch2"),
).
SelectNextItem().
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (fast-forward)"),
Contains("Regular merge (with merge commit)"),
).
Select(Contains("Regular merge (with merge commit)")).
Confirm()
t.Views().Commits().
Lines(
Contains("⏣─╮ Merge branch 'branch1' into original-branch").IsSelected(),
Contains("│ ◯ * branch1"),
Contains("◯─╯ one"),
)
// Check that branch2 shows the non-fast-forward option first
t.Views().Branches().
Focus().
NavigateToLine(Contains("branch2")).
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (with merge commit)"),
Contains("Regular merge (fast-forward)"),
).
Select(Contains("Regular merge (with merge commit)")).
Confirm()
t.Views().Commits().
Lines(
Contains("⏣─╮ Merge branch 'branch2' into original-branch").IsSelected(),
Contains("│ ◯ * branch2"),
Contains("⏣─│─╮ Merge branch 'branch1' into original-branch"),
Contains("│ │ ◯ * branch1"),
Contains("◯─┴─╯ one"),
)
},
})

View File

@@ -48,6 +48,8 @@ var tests = []*components.IntegrationTest{
branch.DeleteRemoteBranchWithDifferentName,
branch.DeleteWhileFiltering,
branch.DetachedHead,
branch.MergeFastForward,
branch.MergeNonFastForward,
branch.MoveCommitsToNewBranchFromBaseBranch,
branch.MoveCommitsToNewBranchFromMainBranch,
branch.MoveCommitsToNewBranchKeepStacked,

View File

@@ -103,7 +103,7 @@ var ModeSpecificKeybindingSuggestions = NewIntegrationTest(NewIntegrationTestArg
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Merge")).
Select(Contains("Regular merge")).
Select(Contains("Regular merge (with merge commit)")).
Confirm()
t.Common().AcknowledgeConflicts()