diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 2b7f9cd16..be1d7b3a9 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -18,6 +18,13 @@ import ( type MergeAndRebaseHelper struct { c *HelperCommon + + // Whether the "continue the rebase/merge?" prompt is currently on screen. + // We use this to auto-dismiss it if the operation stops being in the state + // that the prompt is offering to act on (e.g. it was continued or aborted + // externally), so the user isn't left with a stale prompt. Only accessed on + // the UI thread. + continueRebasePromptShowing bool } func NewMergeAndRebaseHelper( @@ -259,10 +266,17 @@ func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error { // PromptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress func (self *MergeAndRebaseHelper) PromptToContinueRebase() error { + self.continueRebasePromptShowing = true self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.Continue, Prompt: fmt.Sprintf(self.c.Tr.ConflictsResolved, self.c.Git().Status.WorkingTreeState().CommandName()), + HandleClose: func() error { + self.continueRebasePromptShowing = false + return nil + }, HandleConfirm: func() error { + self.continueRebasePromptShowing = false + // By the time we get here, we might have unstaged changes again, // e.g. if the user had to fix build errors after resolving the // conflicts, but after lazygit opened the prompt already. Ask again @@ -300,6 +314,27 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error { return nil } +// DismissContinueRebasePromptIfShowing closes the "continue the rebase/merge?" +// prompt if it's currently on screen. It's called when the operation is no +// longer in the state the prompt is offering to act on (e.g. it was continued +// or aborted outside lazygit, or new conflicts have appeared), so that the +// user isn't left with a prompt whose "continue" would now be wrong or fail. +// Must be called on the UI thread. +func (self *MergeAndRebaseHelper) DismissContinueRebasePromptIfShowing() { + if !self.continueRebasePromptShowing { + return + } + + self.continueRebasePromptShowing = false + + // Guard against popping something else: while our prompt is up no other + // popup can open, and confirming or closing it would have cleared the flag, + // so if it's set the confirmation context is ours. + if self.c.Context().Current() == self.c.Contexts().Confirmation { + self.c.Context().Pop() + } +} + func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { checkedOutBranch := self.c.Model().Branches[0] checkedOutBranchName := checkedOutBranch.Name diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index e40e0acd5..3dbd19674 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -807,16 +807,30 @@ func (self *RefreshHelper) refreshStateFiles(background bool) error { } repoState := self.c.State().GetRepoState() - if self.c.Git().Status.WorkingTreeState().None() { + workingTreeState := self.c.Git().Status.WorkingTreeState() + if workingTreeState.None() { // No operation is in progress (any more), so forget that we started one. // This also covers an operation that was finished or aborted externally. repoState.SetMergeOrRebaseStartedInLazygit(false) - } else if conflictFileCount == 0 && prevConflictFileCount > 0 && repoState.GetMergeOrRebaseStartedInLazygit() { - // The conflicts of an operation we started have just been resolved (e.g. - // in the user's editor). Offer to continue it. We only do this for - // operations we started ourselves; prompting for one that was started - // outside lazygit (e.g. by a coding agent) would be confusing. - self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() }) + } + + if workingTreeState.Any() && conflictFileCount == 0 { + if prevConflictFileCount > 0 && repoState.GetMergeOrRebaseStartedInLazygit() { + // The conflicts of an operation we started have just been resolved + // (e.g. in the user's editor). Offer to continue it. We only do this + // for operations we started ourselves; prompting for one that was + // started outside lazygit (e.g. by a coding agent) would be confusing. + self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() }) + } + } else { + // Either there's no operation in progress any more, or new conflicts have + // appeared. Either way, a "continue?" prompt we're showing is now stale + // (e.g. the operation was continued or aborted outside lazygit), so + // dismiss it rather than leave the user with a prompt that would fail. + self.c.OnUIThread(func() error { + self.mergeAndRebaseHelper.DismissContinueRebasePromptIfShowing() + return nil + }) } fileTreeViewModel.RWMutex.Lock() diff --git a/pkg/integration/tests/conflicts/continue_prompt_dismissed_when_resolved_externally.go b/pkg/integration/tests/conflicts/continue_prompt_dismissed_when_resolved_externally.go new file mode 100644 index 000000000..e9b2ba3aa --- /dev/null +++ b/pkg/integration/tests/conflicts/continue_prompt_dismissed_when_resolved_externally.go @@ -0,0 +1,49 @@ +package conflicts + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var ContinuePromptDismissedWhenResolvedExternally = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "When the prompt to continue a merge is showing and the merge is then continued outside lazygit, dismiss the prompt", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shared.CreateMergeConflictFile(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Common().PretendMergeOrRebaseStartedInLazygit() + + // Resolve the conflict and refresh so lazygit prompts us to continue. + t.Views().Files(). + IsFocused(). + Lines( + Contains("UU file").IsSelected(), + ). + Tap(func() { + t.Shell().UpdateFile("file", "resolved content") + }). + Press(keys.Universal.Refresh) + + t.ExpectPopup().Confirmation(). + Title(Equals("Continue")). + Content(Contains("All merge conflicts resolved. Continue the merge?")) + + // While the prompt is up, the merge is continued outside lazygit (e.g. by + // a coding agent). + t.Shell().ContinueMerge() + + // Simulate lazygit noticing the change (as it would on its next refresh or + // when the window regains focus); the stale prompt is dismissed. + t.FocusIn() + + t.Views().Files(). + IsFocused(). + IsEmpty() + + t.Views().Information().Content(DoesNotContain("Merging")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 554dc726f..74e471713 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -163,6 +163,7 @@ var tests = []*components.IntegrationTest{ config.NegativeRefspec, config.RemoteNamedStar, config.SidePanelsInPerRepoConfig, + conflicts.ContinuePromptDismissedWhenResolvedExternally, conflicts.Filter, conflicts.MergeFileBoth, conflicts.MergeFileCurrent,