From 0f0584cad7179a1c8a5d4e9e7f244002dc54bc81 Mon Sep 17 00:00:00 2001 From: mricherzhagen Date: Mon, 12 Jan 2026 15:35:07 +0100 Subject: [PATCH 1/3] Create `FixupHelper` function to create Handler that stages changes and selects commit. --- pkg/gui/controllers/helpers/fixup_helper.go | 32 ++++++++++++--------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index 2e3b19e7780..dceafee4563 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -126,22 +126,26 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { _, index, _ := self.findCommit(commits, hashGroups[NOT_MERGED][0]) return self.c.ConfirmIf(warnAboutAddedLines, types.ConfirmOpts{ - Title: self.c.Tr.FindBaseCommitForFixup, - Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning, - HandleConfirm: func() error { - if !hasStagedChanges { - if err := self.c.Git().WorkingTree.StageAll(true); err != nil { - return err - } - self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}}) + Title: self.c.Tr.FindBaseCommitForFixup, + Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning, + HandleConfirm: self.getHandlerToStageAndSelectIndex(hasStagedChanges, index), + }) +} + +func (self *FixupHelper) getHandlerToStageAndSelectIndex(hasStagedChanges bool, index int) func() error { + return func() error { + if !hasStagedChanges { + if err := self.c.Git().WorkingTree.StageAll(true); err != nil { + return err } + self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}}) + } - self.c.Contexts().LocalCommits.SetSelection(index) - self.c.Contexts().LocalCommits.FocusLine(true) - self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{}) - return nil - }, - }) + self.c.Contexts().LocalCommits.SetSelection(index) + self.c.Contexts().LocalCommits.FocusLine(true) + self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{}) + return nil + } } func (self *FixupHelper) getHashesAndSubjects(commits []*models.Commit, hashes []string) string { From 7f95ff7dd029740c9fcdbb007fba35d192d91f75 Mon Sep 17 00:00:00 2001 From: mricherzhagen Date: Mon, 12 Jan 2026 14:37:00 +0100 Subject: [PATCH 2/3] Make fixup helper ignore other fixup commits when trying to find base commit --- pkg/gui/controllers/helpers/fixup_helper.go | 41 ++++++++++--- pkg/i18n/english.go | 2 + ...e_commit_for_fixup_ignore_fixup_commits.go | 58 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index dceafee4563..00a02952e66 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -111,14 +111,30 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { } if len(hashGroups[NOT_MERGED]) > 1 { - // If there are multiple commits that could be the base commit, list - // them in the error message. But only the candidates from the current - // branch, not including any that are already merged. + // If there are multiple commits that could be the base commit, check + // if there is only a single one that isn't a fixup commit itself. + // List the commits in the error message. But only the candidates from + // the current branch, not including any that are already merged. + + unmerged_commits := self.getCommitsFromHashes(commits, hashGroups[NOT_MERGED]) + non_fixup_commits := lo.Filter(unmerged_commits, func(c *models.Commit, index int) bool { + return !strings.HasPrefix(c.Name, "fixup! ") && !strings.HasPrefix(c.Name, "squash! ") && !strings.HasPrefix(c.Name, "amend! ") + }) subjects := self.getHashesAndSubjects(commits, hashGroups[NOT_MERGED]) - message := lo.Ternary(hasStagedChanges, - self.c.Tr.MultipleBaseCommitsFoundStaged, - self.c.Tr.MultipleBaseCommitsFoundUnstaged) - return fmt.Errorf("%s\n\n%s", message, subjects) + if len(non_fixup_commits) > 1 || len(non_fixup_commits) == 0 { + message := lo.Ternary(hasStagedChanges, + self.c.Tr.MultipleBaseCommitsFoundStaged, + self.c.Tr.MultipleBaseCommitsFoundUnstaged) + return fmt.Errorf("%s\n\n%s", message, subjects) + } + // There is only a single non fixup commit found. Present it with a Confirmation dialog. + _, index, _ := self.findCommit(commits, non_fixup_commits[0].Hash()) + + return self.c.ConfirmIf(true, types.ConfirmOpts{ + Title: self.c.Tr.FindBaseCommitForFixup, + Prompt: fmt.Sprintf("%s\n\n%s", self.c.Tr.MultipleBaseCommitsOnlyOneNonFixup, subjects), + HandleConfirm: self.getHandlerToStageAndSelectIndex(hasStagedChanges, index), + }) } // At this point we know that the NOT_MERGED bucket has exactly one commit, @@ -148,6 +164,17 @@ func (self *FixupHelper) getHandlerToStageAndSelectIndex(hasStagedChanges bool, } } +func (self *FixupHelper) getCommitsFromHashes(commits []*models.Commit, hashes []string) []*models.Commit { + commitsSet := set.NewFromSlice(hashes) + found_commits := make([]*models.Commit, 0, len(hashes)) + for _, c := range commits { + if commitsSet.Includes(c.Hash()) { + found_commits = append(found_commits, c) + } + } + return found_commits +} + func (self *FixupHelper) getHashesAndSubjects(commits []*models.Commit, hashes []string) string { // This is called only for the NOT_MERGED commits, and we know that all of them are contained in // the commits slice. diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 970afd498be..fc3d265e076 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -45,6 +45,7 @@ type TranslationSet struct { NoBaseCommitsFound string MultipleBaseCommitsFoundStaged string MultipleBaseCommitsFoundUnstaged string + MultipleBaseCommitsOnlyOneNonFixup string BaseCommitIsAlreadyOnMainBranch string BaseCommitIsNotInCurrentView string HunksWithOnlyAddedLinesWarning string @@ -1140,6 +1141,7 @@ func EnglishTranslationSet() *TranslationSet { NoBaseCommitsFound: "No base commits found", MultipleBaseCommitsFoundStaged: "Multiple base commits found. (Try staging fewer changes at once)", MultipleBaseCommitsFoundUnstaged: "Multiple base commits found. (Try staging some of the changes)", + MultipleBaseCommitsOnlyOneNonFixup: "Multiple base commits found, however all but one of them were fixup commits", BaseCommitIsAlreadyOnMainBranch: "The base commit for this change is already on the main branch", BaseCommitIsNotInCurrentView: "Base commit is not in current view", HunksWithOnlyAddedLinesWarning: "There are ranges of only added lines in the diff; be careful to check that these belong in the found base commit.\n\nProceed?", diff --git a/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go b/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go new file mode 100644 index 00000000000..f54f3a3d67b --- /dev/null +++ b/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go @@ -0,0 +1,58 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FindBaseCommitForFixupIgnoreFixupCommits = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Finds the base commit to create a fixup for, disregarding changes to a commit that is already on master", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + EmptyCommit("1st commit"). + CreateFileAndAdd("file", "file1 content line 1\nfile1 content line 2\n"). + Commit("2nd commit"). + NewBranch("mybranch"). + UpdateFileAndAdd("file", "file1 changed content line 1\nfile1 changed content line 2\n"). + Commit("3rd commit"). + EmptyCommit("4th commit"). + UpdateFileAndAdd("file", "file1 1st fixup content line 1\nfile1 changed content line 2\n"). + Commit("fixup! 3rd commit"). + UpdateFile("file", "file1 2nd fixup content line 1\nfile1 2nd fixup content line 2\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("fixup! 3rd commit").IsSelected(), + Contains("4th commit"), + Contains("3rd commit"), + Contains("2nd commit"), + Contains("1st commit"), + ) + + t.Views().Files(). + Focus(). + Press(keys.Files.FindBaseCommitForFixup) + t.ExpectPopup(). + Confirmation(). + Title(Equals("Find base commit for fixup")). + Content( + Contains("all but one of them were fixup commits"). + Contains("3rd commit"). + Contains("fixup! 3rd commit"), + ). + Confirm() + t.Views().Commits(). + IsFocused(). + Lines( + Contains("fixup! 3rd commit"), + Contains("4th commit"), + Contains("3rd commit").IsSelected(), + Contains("2nd commit"), + Contains("1st commit"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 26d6d30305f..b351ccb5d56 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -131,6 +131,7 @@ var tests = []*components.IntegrationTest{ commit.FailHooksThenCommitNoHooks, commit.FindBaseCommitForFixup, commit.FindBaseCommitForFixupDisregardMainBranch, + commit.FindBaseCommitForFixupIgnoreFixupCommits, commit.FindBaseCommitForFixupOnlyAddedLines, commit.FindBaseCommitForFixupWarningForAddedLines, commit.Highlight, From ea16e0f1bf1a7a194e8cc0b38082ab9e40f3d70a Mon Sep 17 00:00:00 2001 From: mricherzhagen Date: Tue, 13 Jan 2026 19:02:43 +0100 Subject: [PATCH 3/3] fixup! Make fixup helper ignore other fixup commits when trying to find base commit --- pkg/gui/controllers/helpers/fixup_helper.go | 15 ++++- ...e_commit_for_fixup_ignore_fixup_commits.go | 9 --- ...xup_ignore_fixup_commits_warn_for_other.go | 58 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 4 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits_warn_for_other.go diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index 00a02952e66..fcd63572d6f 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -128,9 +128,20 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { return fmt.Errorf("%s\n\n%s", message, subjects) } // There is only a single non fixup commit found. Present it with a Confirmation dialog. - _, index, _ := self.findCommit(commits, non_fixup_commits[0].Hash()) + found_commit := non_fixup_commits[0] + all_fixup_for_found_commit := lo.EveryBy(unmerged_commits, func(c *models.Commit) bool { + if c == found_commit { + return true + } + fixup_subject := c.Name + fixup_subject = strings.TrimPrefix(fixup_subject, "fixup! ") + fixup_subject = strings.TrimPrefix(fixup_subject, "squash! ") + fixup_subject = strings.TrimPrefix(fixup_subject, "amend! ") + return strings.HasPrefix(found_commit.Name, fixup_subject) + }) + _, index, _ := self.findCommit(commits, found_commit.Hash()) - return self.c.ConfirmIf(true, types.ConfirmOpts{ + return self.c.ConfirmIf(!all_fixup_for_found_commit, types.ConfirmOpts{ Title: self.c.Tr.FindBaseCommitForFixup, Prompt: fmt.Sprintf("%s\n\n%s", self.c.Tr.MultipleBaseCommitsOnlyOneNonFixup, subjects), HandleConfirm: self.getHandlerToStageAndSelectIndex(hasStagedChanges, index), diff --git a/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go b/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go index f54f3a3d67b..229eb6e4ec6 100644 --- a/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go +++ b/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits.go @@ -36,15 +36,6 @@ var FindBaseCommitForFixupIgnoreFixupCommits = NewIntegrationTest(NewIntegration t.Views().Files(). Focus(). Press(keys.Files.FindBaseCommitForFixup) - t.ExpectPopup(). - Confirmation(). - Title(Equals("Find base commit for fixup")). - Content( - Contains("all but one of them were fixup commits"). - Contains("3rd commit"). - Contains("fixup! 3rd commit"), - ). - Confirm() t.Views().Commits(). IsFocused(). Lines( diff --git a/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits_warn_for_other.go b/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits_warn_for_other.go new file mode 100644 index 00000000000..af7bc73084c --- /dev/null +++ b/pkg/integration/tests/commit/find_base_commit_for_fixup_ignore_fixup_commits_warn_for_other.go @@ -0,0 +1,58 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FindBaseCommitForFixupIgnoreFixupCommitsWarnForOther = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Finds the base commit to create a fixup for, disregarding changes to a commit that is already on master", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + EmptyCommit("1st commit"). + CreateFileAndAdd("file", "file1 content line 1\nfile1 content line 2\n"). + Commit("2nd commit"). + NewBranch("mybranch"). + UpdateFileAndAdd("file", "file1 changed content line 1\nfile1 changed content line 2\n"). + Commit("3rd commit"). + EmptyCommit("4th commit"). + UpdateFileAndAdd("file", "file1 1st fixup content line 1\nfile1 changed content line 2\n"). + Commit("fixup! some other commit"). + UpdateFile("file", "file1 2nd fixup content line 1\nfile1 2nd fixup content line 2\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("fixup! some other commit").IsSelected(), + Contains("4th commit"), + Contains("3rd commit"), + Contains("2nd commit"), + Contains("1st commit"), + ) + + t.Views().Files(). + Focus(). + Press(keys.Files.FindBaseCommitForFixup) + t.ExpectPopup(). + Confirmation(). + Title(Equals("Find base commit for fixup")). + Content( + Contains("all but one of them were fixup commits"). + Contains("3rd commit"). + Contains("fixup! some other commit"), + ). + Confirm() + t.Views().Commits(). + IsFocused(). + Lines( + Contains("fixup! some other commit"), + Contains("4th commit"), + Contains("3rd commit").IsSelected(), + Contains("2nd commit"), + Contains("1st commit"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index b351ccb5d56..739b2ffb97f 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -132,6 +132,7 @@ var tests = []*components.IntegrationTest{ commit.FindBaseCommitForFixup, commit.FindBaseCommitForFixupDisregardMainBranch, commit.FindBaseCommitForFixupIgnoreFixupCommits, + commit.FindBaseCommitForFixupIgnoreFixupCommitsWarnForOther, commit.FindBaseCommitForFixupOnlyAddedLines, commit.FindBaseCommitForFixupWarningForAddedLines, commit.Highlight,