Repository: jesseduffield/lazygit (language: go)

Issue:
Squash all fixups in current branch
Now that we have 'i' for conveniently starting an interactive rebase of the current branch, I want the same for squashing fixups. I don't want to have to care about selecting a commit to start from, just an automatic "squash all fixups in my current branch" command.

To do that, we should rename `Squash all 'fixup!' commits above selected commit (autosquash)` to just `Squash 'fixup!' commits (autosquash)`; instead of showing a confirmation prompt it should show a menu with the choices "all fixups above the selected commit" and "all fixups in current branch".

For backwards compatibility and muscle memory the menu entries need to be in that order, so that `shift-S enter` does the same thing as before. On the other hand, I find "all fixups in current branch" much more useful and I expect to use it by default most of the time, so I'm inclined to make it the default and break compatibility. With #3261, maybe it's acceptable.


Discussion / hints:

Fix patch:
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index 693c5635950..550b3640f5e 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -89,7 +89,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 If you would instead like to start an interactive rebase from the selected commit, press `e`. |
 | `` p `` | Pick | Mark the selected commit to be picked (when mid-rebase). This means that the commit will be retained upon continuing the rebase. |
 | `` F `` | Create fixup commit | Create 'fixup!' commit for the selected commit. Later on, you can press `S` on this same commit to apply all above fixup commits. |
-| `` S `` | Apply fixup commits | Squash all 'fixup!' commits above selected commit (autosquash). |
+| `` S `` | Apply fixup commits | Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash). |
 | `` <c-j> `` | Move commit down one |  |
 | `` <c-k> `` | Move commit up one |  |
 | `` V `` | Paste (cherry-pick) |  |
diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md
index beb9ee3f44a..b19f42fe7f6 100644
--- a/docs/keybindings/Keybindings_ja.md
+++ b/docs/keybindings/Keybindings_ja.md
@@ -106,7 +106,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
 If you would instead like to start an interactive rebase from the selected commit, press `e`. |
 | `` p `` | Pick | Mark the selected commit to be picked (when mid-rebase). This means that the commit will be retained upon continuing the rebase. |
 | `` F `` | Create fixup commit | このコミットに対するfixupコミットを作成 |
-| `` S `` | Apply fixup commits | Squash all 'fixup!' commits above selected commit (autosquash). |
+| `` S `` | Apply fixup commits | Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash). |
 | `` <c-j> `` | コミットを1つ下に移動 |  |
 | `` <c-k> `` | コミットを1つ上に移動 |  |
 | `` V `` | コミットを貼り付け (cherry-pick) |  |
diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go
index 3dc1bae7fba..3fadbe9e92f 100644
--- a/pkg/gui/controllers/local_commits_controller.go
+++ b/pkg/gui/controllers/local_commits_controller.go
@@ -167,13 +167,13 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
 		},
 		{
 			Key:     opts.GetKey(opts.Config.Commits.SquashAboveCommits),
-			Handler: self.withItem(self.squashAllAboveFixupCommits),
+			Handler: self.squashFixupCommits,
 			GetDisabledReason: self.require(
 				self.notMidRebase(self.c.Tr.AlreadyRebasing),
-				self.singleItemSelected(),
 			),
 			Description: self.c.Tr.SquashAboveCommits,
 			Tooltip:     self.c.Tr.SquashAboveCommitsTooltip,
+			OpensMenu:   true,
 		},
 		{
 			Key:     opts.GetKey(opts.Config.Commits.MoveDownCommit),
@@ -816,25 +816,62 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err
 	})
 }
 
-func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Commit) error {
-	prompt := utils.ResolvePlaceholderString(
-		self.c.Tr.SureSquashAboveCommits,
-		map[string]string{"commit": commit.Sha},
-	)
-
-	return self.c.Confirm(types.ConfirmOpts{
-		Title:  self.c.Tr.SquashAboveCommits,
-		Prompt: prompt,
-		HandleConfirm: func() error {
-			return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error {
-				self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
-				err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit)
-				return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
-			})
+func (self *LocalCommitsController) squashFixupCommits() error {
+	return self.c.Menu(types.CreateMenuOptions{
+		Title: self.c.Tr.SquashAboveCommits,
+		Items: []*types.MenuItem{
+			{
+				Label:          self.c.Tr.SquashCommitsInCurrentBranch,
+				OnPress:        self.squashAllFixupsInCurrentBranch,
+				DisabledReason: self.canFindCommitForSquashFixupsInCurrentBranch(),
+				Key:            'b',
+				Tooltip:        self.c.Tr.SquashCommitsInCurrentBranchTooltip,
+			},
+			{
+				Label:          self.c.Tr.SquashCommitsAboveSelectedCommit,
+				OnPress:        self.withItem(self.squashAllFixupsAboveSelectedCommit),
+				DisabledReason: self.singleItemSelected()(),
+				Key:            'a',
+				Tooltip:        self.c.Tr.SquashCommitsAboveSelectedTooltip,
+			},
 		},
 	})
 }
 
+func (self *LocalCommitsController) squashAllFixupsAboveSelectedCommit(commit *models.Commit) error {
+	return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error {
+		self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
+		err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit)
+		return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
+	})
+}
+
+func (self *LocalCommitsController) squashAllFixupsInCurrentBranch() error {
+	commit, err := self.findCommitForSquashFixupsInCurrentBranch()
+	if err != nil {
+		return self.c.Error(err)
+	}
+
+	return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error {
+		self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
+		err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit)
+		return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
+	})
+}
+
+func (self *LocalCommitsController) findCommitForSquashFixupsInCurrentBranch() (*models.Commit, error) {
+	commits := self.c.Model().Commits
+	_, index, ok := lo.FindIndexOf(commits, func(c *models.Commit) bool {
+		return c.IsMerge() || c.Status == models.StatusMerged
+	})
+
+	if !ok || index == 0 {
+		return nil, errors.New(self.c.Tr.CannotSquashCommitsInCurrentBranch)
+	}
+
+	return commits[index-1], nil
+}
+
 func (self *LocalCommitsController) createTag(commit *models.Commit) error {
 	return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {})
 }
@@ -1019,6 +1056,14 @@ func (self *LocalCommitsController) canFindCommitForQuickStart() *types.Disabled
 	return nil
 }
 
+func (self *LocalCommitsController) canFindCommitForSquashFixupsInCurrentBranch() *types.DisabledReason {
+	if _, err := self.findCommitForSquashFixupsInCurrentBranch(); err != nil {
+		return &types.DisabledReason{Text: err.Error()}
+	}
+
+	return nil
+}
+
 func (self *LocalCommitsController) canSquashOrFixup(_selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
 	if endIdx >= len(self.c.Model().Commits)-1 {
 		return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit}
diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go
index 6f444063369..90d53995d56 100644
--- a/pkg/i18n/chinese.go
+++ b/pkg/i18n/chinese.go
@@ -254,7 +254,6 @@ func chineseTranslationSet() TranslationSet {
 		ViewResetOptions:                    `查看重置选项`,
 		CreateFixupCommit:                   `为此提交创建修正`,
 		SquashAboveCommitsTooltip:           `压缩在所选提交之上的所有“fixup!”提交（自动压缩）`,
-		SureSquashAboveCommits:              `您确定要压缩在 {{.commit}} 之上的所有“fixup!”提交吗?`,
 		CreateFixupCommitTooltip:            `创建修正提交`,
 		SureCreateFixupCommit:               `您确定要对 {{.commit}} 创建修正提交吗？`,
 		ExecuteCustomCommand:                "执行自定义命令",
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index 5f7aa297821..7e8a89707ca 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -217,7 +217,6 @@ func dutchTranslationSet() TranslationSet {
 		HardReset:                           "Harde reset",
 		CreateFixupCommit:                   `Creëer fixup commit voor deze commit`,
 		SquashAboveCommitsTooltip:           `Squash bovenstaande commits`,
-		SureSquashAboveCommits:              `Weet je zeker dat je alles wil squash/fixup! voor de bovenstaand commits {{.commit}}?`,
 		CreateFixupCommitTooltip:            `Creëer fixup commit`,
 		SureCreateFixupCommit:               `Weet je zeker dat je een fixup wil maken! commit voor commit {{.commit}}?`,
 		ExecuteCustomCommand:                "Voer aangepaste commando uit",
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 7d4c60c2695..5ff58d08565 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -386,8 +386,12 @@ type TranslationSet struct {
 	CreateFixupCommitDescription          string
 	CreateFixupCommitTooltip              string
 	SquashAboveCommitsTooltip             string
+	SquashCommitsAboveSelectedTooltip     string
+	SquashCommitsInCurrentBranchTooltip   string
 	SquashAboveCommits                    string
-	SureSquashAboveCommits                string
+	SquashCommitsInCurrentBranch          string
+	SquashCommitsAboveSelectedCommit      string
+	CannotSquashCommitsInCurrentBranch    string
 	SureCreateFixupCommit                 string
 	ExecuteCustomCommand                  string
 	ExecuteCustomCommandTooltip           string
@@ -1322,8 +1326,12 @@ func EnglishTranslationSet() TranslationSet {
 		CreateFixupCommitDescription:        `Create fixup commit`,
 		CreateFixupCommitTooltip:            "Create 'fixup!' commit for the selected commit. Later on, you can press `{{.squashAbove}}` on this same commit to apply all above fixup commits.",
 		SquashAboveCommits:                  "Apply fixup commits",
-		SquashAboveCommitsTooltip:           `Squash all 'fixup!' commits above selected commit (autosquash).`,
-		SureSquashAboveCommits:              `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
+		SquashAboveCommitsTooltip:           `Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash).`,
+		SquashCommitsAboveSelectedTooltip:   `Squash all 'fixup!' commits above the selected commit (autosquash).`,
+		SquashCommitsInCurrentBranchTooltip: `Squash all 'fixup!' commits in the current branch (autosquash).`,
+		SquashCommitsInCurrentBranch:        "In current branch",
+		SquashCommitsAboveSelectedCommit:    "Above the selected commit",
+		CannotSquashCommitsInCurrentBranch:  "Cannot squash commits in current branch: the HEAD commit is a merge commit or is present on the main branch.",
 		CreateFixupCommit:                   `Create fixup commit`,
 		SureCreateFixupCommit:               `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
 		ExecuteCustomCommand:                "Execute custom command",
diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go
index 19f1a3ca972..f0947f650b1 100644
--- a/pkg/i18n/japanese.go
+++ b/pkg/i18n/japanese.go
@@ -263,7 +263,6 @@ func japaneseTranslationSet() TranslationSet {
 		CreateFixupCommitTooltip: `このコミットに対するfixupコミットを作成`,
 		// LcSquashAboveCommits:                `squash all 'fixup!' commits above selected commit (autosquash)`,
 		// SquashAboveCommits:                  `Squash all 'fixup!' commits above selected commit (autosquash)`,
-		SureSquashAboveCommits:   `{{.commit}}に対するすべての fixup! コミットをsquashします。よろしいですか?`,
 		CreateFixupCommit:        `Fixupコミットを作成`,
 		SureCreateFixupCommit:    `{{.commit}} に対する fixup! コミットを作成します。よろしいですか?`,
 		ExecuteCustomCommand:     "カスタムコマンドを実行",
diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go
index 26bb0542e5e..fdfb96c0add 100644
--- a/pkg/i18n/korean.go
+++ b/pkg/i18n/korean.go
@@ -258,7 +258,6 @@ func koreanTranslationSet() TranslationSet {
 		ViewResetOptions:                    `View reset options`,
 		CreateFixupCommitTooltip:            `Create fixup commit for this commit`,
 		SquashAboveCommitsTooltip:           `Squash all 'fixup!' commits above selected commit (autosquash)`,
-		SureSquashAboveCommits:              `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
 		CreateFixupCommit:                   `Create fixup commit`,
 		SureCreateFixupCommit:               `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
 		ExecuteCustomCommand:                "Execute custom command",
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 2366ff17457..fdde08a9cf4 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -180,7 +180,6 @@ func polishTranslationSet() TranslationSet {
 		ViewResetOptions:                    "Wyświetl opcje resetu",
 		CreateFixupCommitTooltip:            "Utwórz commit naprawczy dla tego commita",
 		SquashAboveCommitsTooltip:           `Spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)`,
-		SureSquashAboveCommits:              `Na pewno chcesz spłaszczyć wszystkie commity naprawcze powyżej {{.commit}}?`,
 		CreateFixupCommit:                   `Utwóż commit naprawczy`,
 		SureCreateFixupCommit:               `Na pewno utworzyć commit naprawczy dla commita {{.commit}}?`,
 		ExecuteCustomCommand:                "Wykonaj własną komendę",
diff --git a/pkg/i18n/russian.go b/pkg/i18n/russian.go
index b1ac91dbf82..af3fdd6bd1a 100644
--- a/pkg/i18n/russian.go
+++ b/pkg/i18n/russian.go
@@ -314,7 +314,6 @@ func RussianTranslationSet() TranslationSet {
 		ViewResetOptions:                    `Просмотреть параметры сброса`,
 		CreateFixupCommitTooltip:            `Создать fixup коммит для этого коммита`,
 		SquashAboveCommitsTooltip:           `Объединить все 'fixup!' коммиты выше в выбранный коммит (автосохранение)`,
-		SureSquashAboveCommits:              `Вы уверены, что хотите объединить все fixup! коммиты выше {{.commit}}?`,
 		CreateFixupCommit:                   `Создать fixup коммит`,
 		SureCreateFixupCommit:               `Вы уверены, что хотите создать fixup! коммит для коммита {{.commit}}?`,
 		ExecuteCustomCommand:                "Выполнить пользовательскую команду",
diff --git a/pkg/i18n/traditional_chinese.go b/pkg/i18n/traditional_chinese.go
index 76be7b6ef1e..714c259af8a 100644
--- a/pkg/i18n/traditional_chinese.go
+++ b/pkg/i18n/traditional_chinese.go
@@ -341,7 +341,6 @@ func traditionalChineseTranslationSet() TranslationSet {
 		ViewResetOptions:                    "檢視重設選項",
 		CreateFixupCommitTooltip:            "為此提交建立修復提交",
 		SquashAboveCommitsTooltip:           "壓縮上方所有的“fixup!”提交 (自動壓縮)",
-		SureSquashAboveCommits:              "你確定要壓縮{{.commit}}上方所有的fixup!提交嗎？",
 		CreateFixupCommit:                   "建立修復提交",
 		SureCreateFixupCommit:               "你確定要為提交{{.commit}}建立fixup!提交嗎？",
 		ExecuteCustomCommand:                "執行自訂命令",


Test patch:
diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go b/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go
index 94324db7799..9445dcf5899 100644
--- a/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go
+++ b/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go
@@ -33,9 +33,9 @@ var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
 			NavigateToLine(Contains("commit 01").DoesNotContain("fixup!")).
 			Press(keys.Commits.SquashAboveCommits).
 			Tap(func() {
-				t.ExpectPopup().Confirmation().
+				t.ExpectPopup().Menu().
 					Title(Equals("Apply fixup commits")).
-					Content(Contains("Are you sure you want to squash all fixup! commits above")).
+					Select(Contains("Above the selected commit")).
 					Confirm()
 			}).
 			Lines(
diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go b/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go
new file mode 100644
index 00000000000..63681053329
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/squash_fixups_in_current_branch.go
@@ -0,0 +1,55 @@
+package interactive_rebase
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SquashFixupsInCurrentBranch = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Squashes all fixups in the current branch.",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.
+			CreateFileAndAdd("file1", "file1").
+			Commit("master commit").
+			NewBranch("branch").
+			// Test the pathological case that the first commit of a branch is a
+			// fixup for the last master commit below it. We _don't_ want this to
+			// be squashed.
+			UpdateFileAndAdd("file1", "changed file1").
+			Commit("fixup! master commit").
+			CreateNCommits(2).
+			CreateFileAndAdd("fixup-file", "fixup content").
+			Commit("fixup! commit 01")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Commits().
+			Focus().
+			Lines(
+				Contains("fixup! commit 01"),
+				Contains("commit 02"),
+				Contains("commit 01"),
+				Contains("fixup! master commit"),
+				Contains("master commit"),
+			).
+			Press(keys.Commits.SquashAboveCommits).
+			Tap(func() {
+				t.ExpectPopup().Menu().
+					Title(Equals("Apply fixup commits")).
+					Select(Contains("In current branch")).
+					Confirm()
+			}).
+			Lines(
+				Contains("commit 02"),
+				Contains("commit 01"),
+				Contains("fixup! master commit"),
+				Contains("master commit"),
+			).
+			NavigateToLine(Contains("commit 01"))
+
+		t.Views().Main().
+			Content(Contains("fixup content"))
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 9c1f8b5fe76..cfadde3b133 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -183,6 +183,7 @@ var tests = []*components.IntegrationTest{
 	interactive_rebase.SquashDownFirstCommit,
 	interactive_rebase.SquashDownSecondCommit,
 	interactive_rebase.SquashFixupsAboveFirstCommit,
+	interactive_rebase.SquashFixupsInCurrentBranch,
 	interactive_rebase.SwapInRebaseWithConflict,
 	interactive_rebase.SwapInRebaseWithConflictAndEdit,
 	interactive_rebase.SwapWithConflict,