mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 18:24:17 -05:00
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>
193 lines
4.7 KiB
Go
193 lines
4.7 KiB
Go
package gui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v3"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
|
)
|
|
|
|
// this gives our integration test a way of interacting with the gui for sending keypresses
|
|
// and reading state.
|
|
type GuiDriver struct {
|
|
gui *Gui
|
|
isIdleChan chan struct{}
|
|
toastChan chan string
|
|
headless bool
|
|
}
|
|
|
|
var _ integrationTypes.GuiDriver = &GuiDriver{}
|
|
|
|
func (self *GuiDriver) PressKey(keyStr string) {
|
|
self.CheckAllToastsAcknowledged()
|
|
|
|
key, ok := config.KeyFromLabel(keyStr)
|
|
if !ok {
|
|
self.Fail("Unrecognized key: " + keyStr)
|
|
}
|
|
|
|
self.gui.g.ReplayedEvents.Keys <- gocui.NewTcellKeyEventWrapper(
|
|
tcell.NewEventKey(tcell.Key(key.KeyName()), key.Str(), tcell.ModMask(key.Mod())),
|
|
0,
|
|
)
|
|
|
|
self.waitTillIdle()
|
|
}
|
|
|
|
func (self *GuiDriver) Click(x, y int) {
|
|
self.CheckAllToastsAcknowledged()
|
|
|
|
self.gui.g.ReplayedEvents.MouseEvents <- gocui.NewTcellMouseEventWrapper(
|
|
tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0),
|
|
0,
|
|
)
|
|
self.waitTillIdle()
|
|
self.gui.g.ReplayedEvents.MouseEvents <- gocui.NewTcellMouseEventWrapper(
|
|
tcell.NewEventMouse(x, y, tcell.ButtonNone, 0),
|
|
0,
|
|
)
|
|
self.waitTillIdle()
|
|
}
|
|
|
|
// FocusIn simulates the terminal window regaining focus, which is how lazygit
|
|
// learns to reload changed config files. Tests use it to exercise the live
|
|
// config-reload path.
|
|
func (self *GuiDriver) FocusIn() {
|
|
self.gui.g.ReplayedEvents.FocusEvents <- gocui.NewTcellFocusEventWrapper(
|
|
tcell.NewEventFocus(true),
|
|
0,
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func (self *GuiDriver) CheckAllToastsAcknowledged() {
|
|
if t := self.NextToast(); t != nil {
|
|
self.Fail("Toast not acknowledged: " + *t)
|
|
}
|
|
}
|
|
|
|
func (self *GuiDriver) Keys() config.KeybindingConfig {
|
|
return self.gui.Config.GetUserConfig().Keybinding
|
|
}
|
|
|
|
func (self *GuiDriver) CurrentContext() types.Context {
|
|
return self.gui.c.Context().Current()
|
|
}
|
|
|
|
func (self *GuiDriver) ContextForView(viewName string) types.Context {
|
|
context, ok := self.gui.helpers.View.ContextForView(viewName)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
func (self *GuiDriver) Fail(message string) {
|
|
currentView := self.gui.g.CurrentView()
|
|
|
|
// Check for unacknowledged toast: it may give us a hint as to why the test failed
|
|
toastMessage := ""
|
|
if t := self.NextToast(); t != nil {
|
|
toastMessage = fmt.Sprintf("Unacknowledged toast message: %s\n", *t)
|
|
}
|
|
|
|
fullMessage := fmt.Sprintf(
|
|
"%s\nFinal Lazygit state:\n%s\nUpon failure, focused view was '%s'.\n%sLog:\n%s", message,
|
|
self.gui.g.Snapshot(),
|
|
currentView.Name(),
|
|
toastMessage,
|
|
strings.Join(self.gui.GuiLog, "\n"),
|
|
)
|
|
|
|
self.gui.g.Close()
|
|
// need to give the gui time to close
|
|
time.Sleep(time.Millisecond * 100)
|
|
_, err := fmt.Fprintln(os.Stderr, fullMessage)
|
|
if err != nil {
|
|
panic("Test failed. Failed writing to stderr")
|
|
}
|
|
panic("Test failed")
|
|
}
|
|
|
|
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
|
|
func (self *GuiDriver) Log(message string) {
|
|
self.gui.c.Log.Warn(message)
|
|
}
|
|
|
|
// logs in the actual UI (in the commands panel)
|
|
func (self *GuiDriver) LogUI(message string) {
|
|
self.gui.c.LogAction(message)
|
|
}
|
|
|
|
func (self *GuiDriver) CheckedOutRef() *models.Branch {
|
|
return self.gui.helpers.Refs.GetCheckedOutRef()
|
|
}
|
|
|
|
func (self *GuiDriver) MainView() *gocui.View {
|
|
return self.gui.mainView()
|
|
}
|
|
|
|
func (self *GuiDriver) SecondaryView() *gocui.View {
|
|
return self.gui.secondaryView()
|
|
}
|
|
|
|
func (self *GuiDriver) View(viewName string) *gocui.View {
|
|
view, err := self.gui.g.View(viewName)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return view
|
|
}
|
|
|
|
// TopViewInWindow returns the frontmost visible view in the given window, i.e.
|
|
// the tab that is currently shown when a window holds several tabbed views.
|
|
func (self *GuiDriver) TopViewInWindow(windowName string) *gocui.View {
|
|
return self.gui.helpers.Window.TopViewInWindow(windowName, false)
|
|
}
|
|
|
|
func (self *GuiDriver) SetCaption(caption string) {
|
|
self.gui.setCaption(caption)
|
|
self.waitTillIdle()
|
|
}
|
|
|
|
func (self *GuiDriver) SetCaptionPrefix(prefix string) {
|
|
self.gui.setCaptionPrefix(prefix)
|
|
self.waitTillIdle()
|
|
}
|
|
|
|
func (self *GuiDriver) NextToast() *string {
|
|
select {
|
|
case t := <-self.toastChan:
|
|
return &t
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (self *GuiDriver) Headless() bool {
|
|
return self.headless
|
|
}
|