mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
Only prompt to continue a rebase/merge if we started it
When conflicts of an in-progress rebase/merge/cherry-pick/revert are resolved, lazygit pops up a prompt offering to continue it. This is helpful when you started the operation in lazygit and resolved the conflicts in your editor. But it's confusing when the operation was started outside lazygit — e.g. by a coding agent in another terminal that resolves the conflicts but hasn't continued yet because it's still running tests or fixing the build. lazygit would then prompt unbidden. Track whether the in-progress operation was started from within lazygit, and only show the prompt in that case. We record this right after running a merge/rebase step (in CheckMergeOrRebaseWithRefreshOptions, the subprocess branch of genericMergeCommand, and the custom-command conflict path), and clear it whenever a refresh observes that no operation is in progress — which also handles an operation that was finished or aborted externally. The conflict-resolution tests start their operation by running git directly (not through lazygit's UI), so they call the new test helper Common.PretendMergeOrRebaseStartedInLazygit to have lazygit treat the operation as its own and still get the prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3d912f058a
commit
ad507d67f4
@@ -113,6 +113,7 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
||||
Mode: types.ASYNC,
|
||||
CommitSelection: commitSelectionAfterMerge(success && selectHeadCommitOnSuccess),
|
||||
})
|
||||
self.RecordWhetherMergeOrRebaseStartedInLazygit()
|
||||
return err
|
||||
}
|
||||
result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command)
|
||||
@@ -165,9 +166,21 @@ func isMergeConflictErr(errStr string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RecordWhetherMergeOrRebaseStartedInLazygit is called right after we run a
|
||||
// merge/rebase/cherry-pick/revert step. If it left an operation in progress,
|
||||
// that operation is one we started, which is what later lets us auto-prompt to
|
||||
// continue it once its conflicts are resolved. If nothing is in progress
|
||||
// anymore (the step completed or aborted the operation), we clear the flag.
|
||||
func (self *MergeAndRebaseHelper) RecordWhetherMergeOrRebaseStartedInLazygit() {
|
||||
self.c.State().GetRepoState().SetMergeOrRebaseStartedInLazygit(
|
||||
self.c.Git().Status.WorkingTreeState().Any())
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) CheckMergeOrRebaseWithRefreshOptions(result error, refreshOptions types.RefreshOptions) error {
|
||||
self.c.Refresh(refreshOptions)
|
||||
|
||||
self.RecordWhetherMergeOrRebaseStartedInLazygit()
|
||||
|
||||
if result == nil {
|
||||
return nil
|
||||
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
|
||||
|
||||
@@ -806,7 +806,16 @@ func (self *RefreshHelper) refreshStateFiles(background bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if self.c.Git().Status.WorkingTreeState().Any() && conflictFileCount == 0 && prevConflictFileCount > 0 {
|
||||
repoState := self.c.State().GetRepoState()
|
||||
if self.c.Git().Status.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() })
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,14 @@ type GuiRepoState struct {
|
||||
CurrentPopupOpts *types.CreatePopupPanelOpts
|
||||
|
||||
LastBackgroundFetchTime time.Time
|
||||
|
||||
// Whether the rebase/merge/cherry-pick/revert that's currently in progress
|
||||
// was started from within lazygit (as opposed to being started externally,
|
||||
// e.g. in another terminal or by a coding agent). We only auto-prompt to
|
||||
// continue such an operation once its conflicts are resolved if we started
|
||||
// it ourselves; for an externally started one, popping up unbidden would be
|
||||
// confusing. Reset whenever we observe that no operation is in progress.
|
||||
mergeOrRebaseStartedInLazygit bool
|
||||
}
|
||||
|
||||
var _ types.IRepoStateAccessor = new(GuiRepoState)
|
||||
@@ -283,6 +291,14 @@ func (self *GuiRepoState) SetCurrentPopupOpts(value *types.CreatePopupPanelOpts)
|
||||
self.CurrentPopupOpts = value
|
||||
}
|
||||
|
||||
func (self *GuiRepoState) GetMergeOrRebaseStartedInLazygit() bool {
|
||||
return self.mergeOrRebaseStartedInLazygit
|
||||
}
|
||||
|
||||
func (self *GuiRepoState) SetMergeOrRebaseStartedInLazygit(value bool) {
|
||||
self.mergeOrRebaseStartedInLazygit = value
|
||||
}
|
||||
|
||||
func (self *GuiRepoState) GetScreenMode() types.ScreenMode {
|
||||
return self.ScreenMode
|
||||
}
|
||||
|
||||
@@ -68,6 +68,15 @@ func (self *GuiDriver) FocusIn() {
|
||||
self.waitTillIdle()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) PretendMergeOrRebaseStartedInLazygit() {
|
||||
self.gui.onUIThread(func() error {
|
||||
self.gui.State.SetMergeOrRebaseStartedInLazygit(true)
|
||||
return nil
|
||||
})
|
||||
|
||||
self.waitTillIdle()
|
||||
}
|
||||
|
||||
// wait until lazygit is idle (i.e. all processing is done) before continuing
|
||||
func (self *GuiDriver) waitTillIdle() {
|
||||
<-self.isIdleChan
|
||||
|
||||
@@ -318,6 +318,10 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
||||
|
||||
if err != nil {
|
||||
if customCommand.After != nil && customCommand.After.CheckForConflicts {
|
||||
// The custom command may have started a rebase/merge/etc.; if so,
|
||||
// it's one we consider started in lazygit, so that we offer to
|
||||
// continue it once its conflicts are resolved.
|
||||
self.mergeAndRebaseHelper.RecordWhetherMergeOrRebaseStartedInLazygit()
|
||||
return self.mergeAndRebaseHelper.CheckForConflicts(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -401,6 +401,8 @@ type IRepoStateAccessor interface {
|
||||
GetSearchState() *SearchState
|
||||
SetSplitMainPanel(bool)
|
||||
GetSplitMainPanel() bool
|
||||
GetMergeOrRebaseStartedInLazygit() bool
|
||||
SetMergeOrRebaseStartedInLazygit(bool)
|
||||
}
|
||||
|
||||
// startup stages so we don't need to load everything at once
|
||||
|
||||
@@ -38,6 +38,14 @@ func (self *Common) AbortMerge() {
|
||||
Confirm()
|
||||
}
|
||||
|
||||
// PretendMergeOrRebaseStartedInLazygit tells lazygit to treat the in-progress
|
||||
// rebase/merge/etc. as one that it started, so that it will prompt to continue
|
||||
// once the conflicts are resolved. Use it when a test sets up an operation by
|
||||
// running git directly rather than through lazygit's UI.
|
||||
func (self *Common) PretendMergeOrRebaseStartedInLazygit() {
|
||||
self.t.gui.PretendMergeOrRebaseStartedInLazygit()
|
||||
}
|
||||
|
||||
func (self *Common) AcknowledgeConflicts() {
|
||||
self.t.ExpectPopup().Menu().
|
||||
Title(Equals("Conflicts!")).
|
||||
|
||||
@@ -93,6 +93,8 @@ func (self *fakeGuiDriver) CheckAllToastsAcknowledged() {}
|
||||
|
||||
func (self *fakeGuiDriver) Headless() bool { return false }
|
||||
|
||||
func (self *fakeGuiDriver) PretendMergeOrRebaseStartedInLazygit() {}
|
||||
|
||||
func TestManualFailure(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
|
||||
@@ -55,6 +55,8 @@ var MergeFileBoth = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
_, _, _, expected := testDataBoth()
|
||||
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -54,6 +54,8 @@ var MergeFileCurrent = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
_, _, _, expected := testDataCurrent()
|
||||
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -54,6 +54,8 @@ var MergeFileIncoming = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
_, _, _, expected := testDataIncoming()
|
||||
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -16,6 +16,8 @@ var PickBothHunksDiff3 = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
shared.CreateMergeConflictFile(shell)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -15,6 +15,8 @@ var ResolveExternally = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
shared.CreateMergeConflictFile(shell)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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 ResolveExternallyStartedMergeNoPrompt = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "When a merge started outside lazygit has its conflicts resolved, don't prompt to continue it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
// Start the merge by running git directly and never tell lazygit it was
|
||||
// the one to start it, so from lazygit's point of view it was started
|
||||
// externally (e.g. by a coding agent in another terminal).
|
||||
shared.CreateMergeConflictFile(shell)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("UU file").IsSelected(),
|
||||
).
|
||||
Tap(func() {
|
||||
t.Shell().UpdateFile("file", "resolved content")
|
||||
}).
|
||||
Press(keys.Universal.Refresh)
|
||||
|
||||
// No prompt to continue the merge appears; we stay in the files view
|
||||
// with the conflict resolved and the merge still in progress.
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("M file"),
|
||||
)
|
||||
|
||||
t.Views().Information().Content(Contains("Merging"))
|
||||
},
|
||||
})
|
||||
@@ -15,6 +15,8 @@ var ResolveMultipleFiles = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
shared.CreateMergeConflictFiles(shell)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -24,6 +24,8 @@ var ResolveWithoutTrailingLf = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
RunCommandExpectError([]string{"git", "merge", "--no-edit", "branch2"})
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -73,6 +73,8 @@ var DiscardAllDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed2.txt && git add dir/renamed2.txt`)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -16,6 +16,8 @@ var DiscardVariousChanges = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
},
|
||||
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
type statusFile struct {
|
||||
status string
|
||||
label string
|
||||
|
||||
@@ -16,6 +16,8 @@ var DiscardVariousChangesRangeSelect = NewIntegrationTest(NewIntegrationTestArgs
|
||||
},
|
||||
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Common().PretendMergeOrRebaseStartedInLazygit()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
|
||||
@@ -169,6 +169,7 @@ var tests = []*components.IntegrationTest{
|
||||
conflicts.MergeFileIncoming,
|
||||
conflicts.PickBothHunksDiff3,
|
||||
conflicts.ResolveExternally,
|
||||
conflicts.ResolveExternallyStartedMergeNoPrompt,
|
||||
conflicts.ResolveMultipleFiles,
|
||||
conflicts.ResolveNoAutoStage,
|
||||
conflicts.ResolveNonTextualConflicts,
|
||||
|
||||
@@ -52,4 +52,9 @@ type GuiDriver interface {
|
||||
NextToast() *string
|
||||
CheckAllToastsAcknowledged()
|
||||
Headless() bool
|
||||
// Record that the in-progress rebase/merge/etc. is to be treated as one
|
||||
// that was started from within lazygit. Lets a test that starts an
|
||||
// operation by running git directly (rather than through the UI) still get
|
||||
// the "continue?" prompt when its conflicts are resolved.
|
||||
PretendMergeOrRebaseStartedInLazygit()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user