Skip to content

Commit 6285402

Browse files
committed
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.
1 parent c88381b commit 6285402

File tree

8 files changed

+275
-19
lines changed

8 files changed

+275
-19
lines changed

pkg/commands/git_commands/branch.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ func (self *BranchCommands) Merge(branchName string, variant MergeVariant) error
285285
return self.cmd.New(cmdArgs).Run()
286286
}
287287

288+
// Returns whether refName can be fast-forward merged into the current branch
289+
func (self *BranchCommands) CanDoFastForwardMerge(refName string) bool {
290+
cmdArgs := NewGitCmd("merge-base").
291+
Arg("--is-ancestor").
292+
Arg("HEAD", refName).
293+
ToArgv()
294+
err := self.cmd.New(cmdArgs).DontLog().Run()
295+
return err == nil
296+
}
297+
288298
// Only choose between non-empty, non-identical commands
289299
func (self *BranchCommands) allBranchesLogCandidates() []string {
290300
return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))

pkg/commands/git_commands/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
9797
return self.gitConfig.GetBool("rebase.updateRefs")
9898
}
9999

100+
func (self *ConfigCommands) GetMergeFF() string {
101+
return self.gitConfig.Get("merge.ff")
102+
}
103+
100104
func (self *ConfigCommands) DropConfigCache() {
101105
self.gitConfig.DropCache()
102106
}

pkg/gui/controllers/helpers/merge_and_rebase_helper.go

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -381,21 +381,86 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
381381
return errors.New(self.c.Tr.CantMergeBranchIntoItself)
382382
}
383383

384+
wantFastForward, wantNonFastForward := self.fastForwardMergeUserPreference()
385+
canFastForward := self.c.Git().Branch.CanDoFastForwardMerge(refName)
386+
387+
var firstRegularMergeItem *types.MenuItem
388+
var secondRegularMergeItem *types.MenuItem
389+
var fastForwardMergeItem *types.MenuItem
390+
391+
if !wantNonFastForward && (wantFastForward || canFastForward) {
392+
firstRegularMergeItem = &types.MenuItem{
393+
Label: self.c.Tr.RegularMergeFastForward,
394+
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
395+
Key: 'm',
396+
Tooltip: utils.ResolvePlaceholderString(
397+
self.c.Tr.RegularMergeFastForwardTooltip,
398+
map[string]string{
399+
"checkedOutBranch": checkedOutBranchName,
400+
"selectedBranch": refName,
401+
},
402+
),
403+
}
404+
fastForwardMergeItem = firstRegularMergeItem
405+
406+
secondRegularMergeItem = &types.MenuItem{
407+
Label: self.c.Tr.RegularMergeNonFastForward,
408+
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_NON_FAST_FORWARD),
409+
Key: 'n',
410+
Tooltip: utils.ResolvePlaceholderString(
411+
self.c.Tr.RegularMergeNonFastForwardTooltip,
412+
map[string]string{
413+
"checkedOutBranch": checkedOutBranchName,
414+
"selectedBranch": refName,
415+
},
416+
),
417+
}
418+
} else {
419+
firstRegularMergeItem = &types.MenuItem{
420+
Label: self.c.Tr.RegularMergeNonFastForward,
421+
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
422+
Key: 'm',
423+
Tooltip: utils.ResolvePlaceholderString(
424+
self.c.Tr.RegularMergeNonFastForwardTooltip,
425+
map[string]string{
426+
"checkedOutBranch": checkedOutBranchName,
427+
"selectedBranch": refName,
428+
},
429+
),
430+
}
431+
432+
secondRegularMergeItem = &types.MenuItem{
433+
Label: self.c.Tr.RegularMergeFastForward,
434+
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_FAST_FORWARD),
435+
Key: 'f',
436+
Tooltip: utils.ResolvePlaceholderString(
437+
self.c.Tr.RegularMergeFastForwardTooltip,
438+
map[string]string{
439+
"checkedOutBranch": checkedOutBranchName,
440+
"selectedBranch": refName,
441+
},
442+
),
443+
}
444+
fastForwardMergeItem = secondRegularMergeItem
445+
}
446+
447+
if !canFastForward {
448+
fastForwardMergeItem.DisabledReason = &types.DisabledReason{
449+
Text: utils.ResolvePlaceholderString(
450+
self.c.Tr.CannotFastForwardMerge,
451+
map[string]string{
452+
"checkedOutBranch": checkedOutBranchName,
453+
"selectedBranch": refName,
454+
},
455+
),
456+
}
457+
}
458+
384459
return self.c.Menu(types.CreateMenuOptions{
385460
Title: self.c.Tr.Merge,
386461
Items: []*types.MenuItem{
387-
{
388-
Label: self.c.Tr.RegularMerge,
389-
OnPress: self.RegularMerge(refName),
390-
Key: 'm',
391-
Tooltip: utils.ResolvePlaceholderString(
392-
self.c.Tr.RegularMergeTooltip,
393-
map[string]string{
394-
"checkedOutBranch": checkedOutBranchName,
395-
"selectedBranch": refName,
396-
},
397-
),
398-
},
462+
firstRegularMergeItem,
463+
secondRegularMergeItem,
399464
{
400465
Label: self.c.Tr.SquashMergeUncommitted,
401466
OnPress: self.SquashMergeUncommitted(refName),
@@ -423,10 +488,10 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
423488
})
424489
}
425490

426-
func (self *MergeAndRebaseHelper) RegularMerge(refName string) func() error {
491+
func (self *MergeAndRebaseHelper) RegularMerge(refName string, variant git_commands.MergeVariant) func() error {
427492
return func() error {
428493
self.c.LogAction(self.c.Tr.Actions.Merge)
429-
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_REGULAR)
494+
err := self.c.Git().Branch.Merge(refName, variant)
430495
return self.CheckMergeOrRebase(err)
431496
}
432497
}
@@ -459,6 +524,31 @@ func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranch
459524
}
460525
}
461526

527+
// Returns wantsFastForward, wantsNonFastForward. These will never both be true, but they can both be false.
528+
func (self *MergeAndRebaseHelper) fastForwardMergeUserPreference() (bool, bool) {
529+
// Check user config first, because it takes precedence over git config
530+
mergingArgs := self.c.UserConfig().Git.Merging.Args
531+
if strings.Contains(mergingArgs, "--ff") { // also covers "--ff-only"
532+
return true, false
533+
}
534+
535+
if strings.Contains(mergingArgs, "--no-ff") {
536+
return false, true
537+
}
538+
539+
// Then check git config
540+
mergeFfConfig := self.c.Git().Config.GetMergeFF()
541+
if mergeFfConfig == "true" || mergeFfConfig == "only" {
542+
return true, false
543+
}
544+
545+
if mergeFfConfig == "false" {
546+
return false, true
547+
}
548+
549+
return false, false
550+
}
551+
462552
func (self *MergeAndRebaseHelper) ResetMarkedBaseCommit() error {
463553
self.c.Modes().MarkedBaseCommit.Reset()
464554
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)

pkg/i18n/english.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,11 @@ type TranslationSet struct {
264264
FocusMainView string
265265
Merge string
266266
MergeBranchTooltip string
267-
RegularMerge string
268-
RegularMergeTooltip string
267+
RegularMergeFastForward string
268+
RegularMergeFastForwardTooltip string
269+
CannotFastForwardMerge string
270+
RegularMergeNonFastForward string
271+
RegularMergeNonFastForwardTooltip string
269272
SquashMergeUncommitted string
270273
SquashMergeUncommittedTooltip string
271274
SquashMergeCommitted string
@@ -1351,8 +1354,11 @@ func EnglishTranslationSet() *TranslationSet {
13511354
FocusMainView: "Focus main view",
13521355
Merge: `Merge`,
13531356
MergeBranchTooltip: "View options for merging the selected item into the current branch (regular merge, squash merge)",
1354-
RegularMerge: "Regular merge",
1355-
RegularMergeTooltip: "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'.",
1357+
RegularMergeFastForward: "Regular merge (fast-forward)",
1358+
RegularMergeFastForwardTooltip: "Fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}' without creating a merge commit.",
1359+
CannotFastForwardMerge: "Cannot fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}'",
1360+
RegularMergeNonFastForward: "Regular merge (with merge commit)",
1361+
RegularMergeNonFastForwardTooltip: "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}', creating a merge commit.",
13561362
SquashMergeUncommitted: "Squash merge and leave uncommitted",
13571363
SquashMergeUncommittedTooltip: "Squash merge '{{.selectedBranch}}' into the working tree.",
13581364
SquashMergeCommitted: "Squash merge and commit",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package branch
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var MergeFastForward = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Merge a branch into another using fast-forward merge",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {
13+
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
14+
},
15+
SetupRepo: func(shell *Shell) {
16+
shell.NewBranch("original-branch").
17+
EmptyCommit("one").
18+
NewBranch("branch1").
19+
EmptyCommit("branch1").
20+
Checkout("original-branch").
21+
NewBranchFrom("branch2", "original-branch").
22+
EmptyCommit("branch2").
23+
Checkout("original-branch")
24+
},
25+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
26+
t.Views().Branches().
27+
Focus().
28+
Lines(
29+
Contains("original-branch").IsSelected(),
30+
Contains("branch1"),
31+
Contains("branch2"),
32+
).
33+
SelectNextItem().
34+
Press(keys.Branches.MergeIntoCurrentBranch)
35+
36+
t.ExpectPopup().Menu().
37+
Title(Equals("Merge")).
38+
TopLines(
39+
Contains("Regular merge (fast-forward)"),
40+
Contains("Regular merge (with merge commit)"),
41+
).
42+
Select(Contains("Regular merge (fast-forward)")).
43+
Confirm()
44+
45+
t.Views().Commits().
46+
Lines(
47+
Contains("branch1").IsSelected(),
48+
Contains("one"),
49+
)
50+
51+
// Check that branch2 can't be merged using fast-forward
52+
t.Views().Branches().
53+
Focus().
54+
NavigateToLine(Contains("branch2")).
55+
Press(keys.Branches.MergeIntoCurrentBranch)
56+
57+
t.ExpectPopup().Menu().
58+
Title(Equals("Merge")).
59+
TopLines(
60+
Contains("Regular merge (with merge commit)"),
61+
Contains("Regular merge (fast-forward)"),
62+
).
63+
Select(Contains("Regular merge (fast-forward)")).
64+
Confirm()
65+
66+
t.ExpectToast(Contains("Cannot fast-forward 'original-branch' to 'branch2'"))
67+
},
68+
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package branch
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var MergeNonFastForward = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Merge a branch into another using non-fast-forward merge",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {
13+
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
14+
},
15+
SetupRepo: func(shell *Shell) {
16+
shell.NewBranch("original-branch").
17+
EmptyCommit("one").
18+
NewBranch("branch1").
19+
EmptyCommit("branch1").
20+
Checkout("original-branch").
21+
NewBranchFrom("branch2", "original-branch").
22+
EmptyCommit("branch2").
23+
Checkout("original-branch")
24+
},
25+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
26+
t.Views().Branches().
27+
Focus().
28+
Lines(
29+
Contains("original-branch").IsSelected(),
30+
Contains("branch1"),
31+
Contains("branch2"),
32+
).
33+
SelectNextItem().
34+
Press(keys.Branches.MergeIntoCurrentBranch)
35+
36+
t.ExpectPopup().Menu().
37+
Title(Equals("Merge")).
38+
TopLines(
39+
Contains("Regular merge (fast-forward)"),
40+
Contains("Regular merge (with merge commit)"),
41+
).
42+
Select(Contains("Regular merge (with merge commit)")).
43+
Confirm()
44+
45+
t.Views().Commits().
46+
Lines(
47+
Contains("⏣─╮ Merge branch 'branch1' into original-branch").IsSelected(),
48+
Contains("│ ◯ * branch1"),
49+
Contains("◯─╯ one"),
50+
)
51+
52+
// Check that branch2 shows the non-fast-forward option first
53+
t.Views().Branches().
54+
Focus().
55+
NavigateToLine(Contains("branch2")).
56+
Press(keys.Branches.MergeIntoCurrentBranch)
57+
58+
t.ExpectPopup().Menu().
59+
Title(Equals("Merge")).
60+
TopLines(
61+
Contains("Regular merge (with merge commit)"),
62+
Contains("Regular merge (fast-forward)"),
63+
).
64+
Select(Contains("Regular merge (with merge commit)")).
65+
Confirm()
66+
67+
t.Views().Commits().
68+
Lines(
69+
Contains("⏣─╮ Merge branch 'branch2' into original-branch").IsSelected(),
70+
Contains("│ ◯ * branch2"),
71+
Contains("⏣─│─╮ Merge branch 'branch1' into original-branch"),
72+
Contains("│ │ ◯ * branch1"),
73+
Contains("◯─┴─╯ one"),
74+
)
75+
},
76+
})

pkg/integration/tests/test_list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ var tests = []*components.IntegrationTest{
4848
branch.DeleteRemoteBranchWithDifferentName,
4949
branch.DeleteWhileFiltering,
5050
branch.DetachedHead,
51+
branch.MergeFastForward,
52+
branch.MergeNonFastForward,
5153
branch.MoveCommitsToNewBranchFromBaseBranch,
5254
branch.MoveCommitsToNewBranchFromMainBranch,
5355
branch.MoveCommitsToNewBranchKeepStacked,

pkg/integration/tests/ui/mode_specific_keybinding_suggestions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ var ModeSpecificKeybindingSuggestions = NewIntegrationTest(NewIntegrationTestArg
103103
Tap(func() {
104104
t.ExpectPopup().Menu().
105105
Title(Equals("Merge")).
106-
Select(Contains("Regular merge")).
106+
Select(Contains("Regular merge (with merge commit)")).
107107
Confirm()
108108

109109
t.Common().AcknowledgeConflicts()

0 commit comments

Comments
 (0)