From acb54f73d1ae7a91f37d5ea0071cad096ed4d24e Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 7 Nov 2026 17:02:09 +0100 Subject: [PATCH] Disable squash merging while there are staged changes Git refuses to merge a branch while there are staged changes, unless the merge is a fast-forward. A fast-forward keeps the staged changes as they are. A squash merge that git can fast-forward adds the changes of the branch to them, though, and "Squash merge and commit" then commits them all. Disable both squash merge items in the merge menu while there are staged changes, so that squash merges behave like other merges. We will rely on this when we let the user abort a squash merge. Aborting resets the staged changes, so they must all come from the merge. Co-Authored-By: Claude Opus 5.5 --- .../helpers/merge_and_rebase_helper.go | 22 ++++++++++++++----- pkg/i18n/english.go | 2 ++ .../squash_merge_with_staged_changes.go | 18 --------------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 4ce378bfe..222067991 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -611,15 +611,24 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e } } + // Git refuses to merge while there are staged changes, but not if it can + // fast-forward the squash merge. It would add the changes of the branch to + // the staged ones then, and we couldn't tell them apart anymore. + var squashMergeDisabledReason *types.DisabledReason + if AnyStagedFiles(self.c.Model().Files) { + squashMergeDisabledReason = &types.DisabledReason{Text: self.c.Tr.CantSquashMergeWithStagedChanges} + } + return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Merge, Items: []*types.MenuItem{ firstRegularMergeItem, secondRegularMergeItem, { - Label: self.c.Tr.SquashMergeUncommitted, - OnPress: self.SquashMergeUncommitted(refName), - Keys: menuKey('s'), + Label: self.c.Tr.SquashMergeUncommitted, + OnPress: self.SquashMergeUncommitted(refName), + DisabledReason: squashMergeDisabledReason, + Keys: menuKey('s'), Tooltip: utils.ResolvePlaceholderString( self.c.Tr.SquashMergeUncommittedTooltip, map[string]string{ @@ -628,9 +637,10 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e ), }, { - Label: self.c.Tr.SquashMergeCommitted, - OnPress: self.SquashMergeCommitted(refName, checkedOutBranchName), - Keys: menuKey('S'), + Label: self.c.Tr.SquashMergeCommitted, + OnPress: self.SquashMergeCommitted(refName, checkedOutBranchName), + DisabledReason: squashMergeDisabledReason, + Keys: menuKey('S'), Tooltip: utils.ResolvePlaceholderString( self.c.Tr.SquashMergeCommittedTooltip, map[string]string{ diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 7a5a6d3d8..184000577 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -304,6 +304,7 @@ type TranslationSet struct { SquashMergeUncommittedTooltip string SquashMergeCommitted string SquashMergeCommittedTooltip string + CantSquashMergeWithStagedChanges string ConfirmQuit string SwitchRepo string AllBranchesLogGraph string @@ -1533,6 +1534,7 @@ func EnglishTranslationSet() *TranslationSet { SquashMergeUncommittedTooltip: "Squash merge '{{.selectedBranch}}' into the working tree.", SquashMergeCommitted: "Squash merge and commit", SquashMergeCommittedTooltip: "Squash merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}' as a single commit.", + CantSquashMergeWithStagedChanges: "Cannot squash merge while there are staged changes", ConfirmQuit: `Are you sure you want to quit?`, SwitchRepo: `Switch to a recent repo`, AllBranchesLogGraph: `Show/cycle all branch logs`, diff --git a/pkg/integration/tests/branch/squash_merge_with_staged_changes.go b/pkg/integration/tests/branch/squash_merge_with_staged_changes.go index 4778101b0..0e2b70413 100644 --- a/pkg/integration/tests/branch/squash_merge_with_staged_changes.go +++ b/pkg/integration/tests/branch/squash_merge_with_staged_changes.go @@ -34,7 +34,6 @@ var SquashMergeWithStagedChanges = NewIntegrationTest(NewIntegrationTestArgs{ Select(Contains("Squash merge and commit")). Confirm() - /* EXPECTED: t.ExpectToast(Equals("Disabled: Cannot squash merge while there are staged changes")) t.ExpectPopup().Menu(). @@ -43,22 +42,5 @@ var SquashMergeWithStagedChanges = NewIntegrationTest(NewIntegrationTestArgs{ Confirm() t.ExpectToast(Equals("Disabled: Cannot squash merge while there are staged changes")) - ACTUAL: */ - t.Views().Commits(). - Focus(). - Lines( - Contains("Squash merge feature-branch into original-branch"), - Contains("one"), - ). - NavigateToLine(Contains("Squash merge feature-branch into original-branch")). - PressEnter() - - t.Views().CommitFiles(). - IsFocused(). - Lines( - Equals("▼ /"), - Contains("feature-file"), - Contains("staged-file"), - ) }, })