From 58bcfc4347e9551fdd09a943ada94e3520c84685 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 30 Mar 2026 16:46:04 +0200 Subject: [PATCH 1/2] Add test to demonstrate the problem --- ...build_errors_with_out_of_date_submodule.go | 118 ++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 2 files changed, 119 insertions(+) create mode 100644 pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go diff --git a/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go b/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go new file mode 100644 index 000000000..22fbfdb5f --- /dev/null +++ b/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go @@ -0,0 +1,118 @@ +package branch + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Rebase onto another branch, deal with the conflicts. While continue prompt is showing, fix build errors; get another prompt when continuing. Check that we don't stage submodules here.", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Git.LocalBranchSortOrder = "recency" + }, + SetupRepo: func(shell *Shell) { + // Create an out-of-date submodule to verify that we don't try to stage it + shell. + EmptyCommit("Initial commit"). + CloneIntoSubmodule("submodule", "submodule"). + Commit("Add submodule"). + AddFileInWorktreeOrSubmodule("submodule", "file", "content"). + CommitInWorktreeOrSubmodule("submodule", "add file in submodule") + + shared.MergeConflictsSetup(shell) + + // Create an untracked file to verify that we don't try to stage it either + shell.UpdateFile("untracked-file", "some untracked file") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits().TopLines( + Contains("first change"), + Contains("original"), + ) + + t.Views().Branches(). + Focus(). + Lines( + Contains("first-change-branch"), + Contains("second-change-branch"), + Contains("original-branch"), + Contains("master"), + ). + SelectNextItem(). + Press(keys.Branches.RebaseBranch) + + t.ExpectPopup().Menu(). + Title(Equals("Rebase 'first-change-branch'")). + Select(Contains("Simple rebase")). + Confirm() + + t.Common().AcknowledgeConflicts() + + t.Views().Files(). + IsFocused(). + SelectedLine(Contains("file")). + PressEnter() + + t.Views().MergeConflicts(). + IsFocused(). + SelectNextItem(). + PressPrimaryAction() + + t.Views().Information().Content(Contains("Rebasing")) + + popup := t.ExpectPopup().Confirmation(). + Title(Equals("Continue")). + Content(Contains("All merge conflicts resolved. Continue the rebase?")) + + // While the popup is showing, fix some build errors + t.Shell().UpdateFile("file", "make it compile again") + + // Continue + popup.Confirm() + + t.Views().Files(). + Lines( + Equals("▼ /").IsSelected(), + Equals(" MM file"), + Equals(" M submodule (submodule)"), + Equals(" ?? untracked-file"), + ) + + t.ExpectPopup().Confirmation(). + Title(Equals("Continue")). + Content(Contains("Files have been modified since conflicts were resolved. Auto-stage them and continue?")). + Confirm() + + t.Views().Information().Content(DoesNotContain("Rebasing")) + + t.Views().Files(). + Lines( + /* EXPECTED: + Equals("▼ /").IsSelected(), + Equals(" M submodule (submodule)"), + Equals(" ?? untracked-file"), + ACTUAL: */ + Equals("?? untracked-file"), + ) + + t.Views().Commits(). + Focus(). + TopLines( + Contains("first change").IsSelected(), + Contains("second-change-branch unrelated change"), + Contains("second change"), + Contains("original"), + ) + + t.Views().Main(). + Content( + /* EXPECTED: + DoesNotContain("submodule").DoesNotContain("untracked-file"), + ACTUAL: */ + Contains("submodule").DoesNotContain("untracked-file"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 3d8edfb56..7f228635a 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -69,6 +69,7 @@ var tests = []*components.IntegrationTest{ branch.RebaseAndDrop, branch.RebaseCancelOnConflict, branch.RebaseConflictsFixBuildErrors, + branch.RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule, branch.RebaseCopiedBranch, branch.RebaseDoesNotAutosquash, branch.RebaseFromMarkedBase, From 9f38d02a4e8c227205b31ec1031e5c5e64c85575 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 30 Mar 2026 17:21:45 +0200 Subject: [PATCH 2/2] Don't stage out-of-date submodules when asking user to auto-stage after resolving conflicts When you rebase a branch and there are conflicts, lazygit asks you to continue the rebase when it detects that all conflicts have been resolved. However, it is common to make additional changes beyond just fixing the conflicts (e.g. to fix build failures), and when doing that while the "Continue the rebase?" prompt is showing, lazygit detects that too and asks you if you want to stage those newly modified files too. This is all well and good (and can be disabled for those who don't like it); however, lazygit would treat out-of-date submodules as unstaged changes and would offer to stage those as well, and this is pretty bad and almost never what you want. Fix this by excluding submodules from that second check. --- pkg/gui/controllers/helpers/merge_and_rebase_helper.go | 6 +++--- pkg/gui/controllers/helpers/working_tree_helper.go | 6 ++++++ ...conflicts_fix_build_errors_with_out_of_date_submodule.go | 6 ------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 7d4f0a96a..7f51bc35d 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -237,14 +237,14 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error { Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}, }) - root := self.c.Contexts().Files.FileTreeViewModel.GetRoot() - if root.GetHasUnstagedChanges() { + unstagedFiles := GetUnstagedFilesExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules) + if len(unstagedFiles) > 0 { self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.Continue, Prompt: self.c.Tr.UnstagedFilesAfterConflictsResolved, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.StageAllFiles) - if err := self.c.Git().WorkingTree.StageAll(true); err != nil { + if err := self.c.Git().WorkingTree.StageFiles(unstagedFiles, []string{}); err != nil { return err } diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index 94a24e5ee..d6289537b 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -97,6 +97,12 @@ func IsWorkingTreeDirtyExceptSubmodules(files []*models.File, submoduleConfigs [ return AnyStagedFilesExceptSubmodules(files, submoduleConfigs) || AnyTrackedFilesExceptSubmodules(files, submoduleConfigs) } +func GetUnstagedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) []string { + return lo.FilterMap(files, func(f *models.File, _ int) (string, bool) { + return f.Path, f.HasUnstagedChanges && f.Tracked && !f.IsSubmodule(submoduleConfigs) + }) +} + func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File { for _, file := range self.c.Model().Files { if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) { diff --git a/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go b/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go index 22fbfdb5f..1b95fd316 100644 --- a/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go +++ b/pkg/integration/tests/branch/rebase_conflicts_fix_build_errors_with_out_of_date_submodule.go @@ -90,12 +90,9 @@ var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(New t.Views().Files(). Lines( - /* EXPECTED: Equals("▼ /").IsSelected(), Equals(" M submodule (submodule)"), Equals(" ?? untracked-file"), - ACTUAL: */ - Equals("?? untracked-file"), ) t.Views().Commits(). @@ -109,10 +106,7 @@ var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(New t.Views().Main(). Content( - /* EXPECTED: DoesNotContain("submodule").DoesNotContain("untracked-file"), - ACTUAL: */ - Contains("submodule").DoesNotContain("untracked-file"), ) }, })