Disable moving a patch between a merge commit and another commit

If the patch comes from a merge commit, moving it to a later commit
leaves the user in a rebase that is stuck before its first todo.
Moving it to an earlier commit, or into a new commit before the merge,
finishes, but with surprising results. The rebase doesn't replay a
merge commit; it recreates it from its parents. If the patch contains
changes from the merged branch, the commits on that branch that made
them become empty. And a new commit "before" the merge ends up at the
tip of the merged branch.

Moving a patch to a later merge commit gets stuck in the same way.
Moving a patch to a merge commit isn't useful in either direction, so
disable both.

The other patch commands amend the merge commit itself and work fine,
so they stay enabled. They are useful for removing changes that were
added to a merge commit by accident.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-11-03 19:25:48 +01:00
co-authored by Claude Opus 5.5
parent 9383172a6c
commit e8261e06d1
5 changed files with 156 additions and 5 deletions
@@ -48,6 +48,11 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
}
if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState().None() {
var disabledReasonForMergeCommit *types.DisabledReason
if self.isPatchFromMergeCommit() {
disabledReasonForMergeCommit = &types.DisabledReason{Text: self.c.Tr.NotSupportedForPatchFromMergeCommit}
}
menuItems = append(menuItems, []*types.MenuItem{
{
Label: fmt.Sprintf(self.c.Tr.RemovePatchFromOriginalCommit, utils.ShortHash(self.c.Git().Patch.PatchBuilder.To)),
@@ -68,10 +73,11 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
Keys: menuKey('n'),
},
{
Label: self.c.Tr.MovePatchIntoNewCommitBefore,
Tooltip: self.c.Tr.MovePatchIntoNewCommitBeforeTooltip,
OnPress: self.handlePullPatchIntoNewCommitBefore,
Keys: menuKey('N'),
Label: self.c.Tr.MovePatchIntoNewCommitBefore,
Tooltip: self.c.Tr.MovePatchIntoNewCommitBeforeTooltip,
OnPress: self.handlePullPatchIntoNewCommitBefore,
Keys: menuKey('N'),
DisabledReason: disabledReasonForMergeCommit,
},
}...)
@@ -79,7 +85,10 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
selectedCommit := self.c.Contexts().LocalCommits.GetSelected()
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Hash() {
var disabledReason *types.DisabledReason
disabledReason := disabledReasonForMergeCommit
if selectedCommit.IsMerge() {
disabledReason = &types.DisabledReason{Text: self.c.Tr.CannotMovePatchToMergeCommit}
}
if self.c.Contexts().LocalCommits.AreMultipleItemsSelected() {
disabledReason = &types.DisabledReason{Text: self.c.Tr.RangeSelectNotSupported}
}
@@ -123,6 +132,11 @@ func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
return -1
}
func (self *CustomPatchOptionsMenuAction) isPatchFromMergeCommit() bool {
commitIndex := self.getPatchCommitIndex()
return commitIndex != -1 && self.c.Model().Commits[commitIndex].IsMerge()
}
func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
commits := self.c.Model().Commits
commitIndex := self.getPatchCommitIndex()
+4
View File
@@ -1013,6 +1013,8 @@ type TranslationSet struct {
RangeSelectUp string
RangeSelectDown string
RangeSelectNotSupported string
NotSupportedForPatchFromMergeCommit string
CannotMovePatchToMergeCommit string
NoItemSelected string
SelectedItemIsNotABranch string
SelectedItemDoesNotHaveFiles string
@@ -2230,6 +2232,8 @@ func EnglishTranslationSet() *TranslationSet {
RangeSelectUp: "Range select up",
RangeSelectDown: "Range select down",
RangeSelectNotSupported: "Action does not support range selection, please select a single item",
NotSupportedForPatchFromMergeCommit: "Not supported for a patch from a merge commit",
CannotMovePatchToMergeCommit: "Cannot move a patch to a merge commit",
NoItemSelected: "No item selected",
SelectedItemIsNotABranch: "Selected item is not a branch",
SelectedItemDoesNotHaveFiles: "Selected item does not have files to view",
@@ -0,0 +1,60 @@
package main_view
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DisableMovingPatchFromMergeCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Moving a patch from a merge commit to another commit, or into a new commit before it, is disabled",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
NewBranch("main-branch").
CreateFileAndAdd("file1", "file1 content\n").
Commit("first commit").
NewBranch("feature").
CreateFileAndAdd("file2", "file2 content\n").
Commit("feature commit").
Checkout("main-branch").
CreateFileAndAdd("file3", "file3 content\n").
Commit("main commit").
Merge("feature")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("Merge branch 'feature' into main-branch").IsSelected(),
Contains("feature commit"),
Contains("main commit"),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file2").IsSelected(),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("Building patch"))
t.Views().Commits().
Focus().
NavigateToLine(Contains("first commit"))
t.GlobalPress(keys.Universal.CreatePatchOptionsMenu)
t.ExpectPopup().Menu().
Title(Equals("Patch options")).
Select(Contains("Move patch to selected commit")).
Tooltip(Contains("Disabled: Not supported for a patch from a merge commit")).
Select(Contains("Move patch into new commit before the original commit")).
Tooltip(Contains("Disabled: Not supported for a patch from a merge commit")).
Cancel()
},
})
@@ -0,0 +1,71 @@
package main_view
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DisableMovingPatchToMergeCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Moving a patch to an earlier or later merge commit is disabled",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
NewBranch("main-branch").
CreateFileAndAdd("file1", "file1 content\n").
Commit("first commit").
NewBranch("feature").
CreateFileAndAdd("file2", "file2 content\n").
Commit("feature commit").
Checkout("main-branch").
CreateFileAndAdd("file3", "file3 content\n").
Commit("main commit").
Merge("feature").
CreateFileAndAdd("file4", "file4 content\n").
Commit("top commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
checkMovingPatchToMergeCommitIsDisabled := func(commitToMoveFrom string, file string) {
t.Views().Commits().
Focus().
NavigateToLine(Contains(commitToMoveFrom)).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(file).IsSelected(),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("Building patch"))
t.Views().Commits().
Focus().
NavigateToLine(Contains("Merge branch 'feature' into main-branch"))
t.GlobalPress(keys.Universal.CreatePatchOptionsMenu)
t.ExpectPopup().Menu().
Title(Equals("Patch options")).
Select(Contains("Move patch to selected commit")).
Tooltip(Contains("Disabled: Cannot move a patch to a merge commit")).
Cancel()
t.Common().ResetCustomPatch()
}
t.Views().Commits().
Lines(
Contains("top commit").IsSelected(),
Contains("Merge branch 'feature' into main-branch"),
Contains("feature commit"),
Contains("main commit"),
Contains("first commit"),
)
checkMovingPatchToMergeCommitIsDisabled("top commit", "file4")
checkMovingPatchToMergeCommitIsDisabled("main commit", "file3")
},
})
+2
View File
@@ -407,6 +407,8 @@ var tests = []*components.IntegrationTest{
main_view.CopySelectedDiffLines,
main_view.CustomPatchGoesThroughTheDiffRenderer,
main_view.CustomPatchIgnoresLineEndingConversion,
main_view.DisableMovingPatchFromMergeCommit,
main_view.DisableMovingPatchToMergeCommit,
main_view.DiscardAllChanges,
main_view.DiscardDiffLines,
main_view.DiscardFromACommitOnlyWhereItCanBeRewritten,