From ccfdcd27bef8e7cc59f070b4dd9a6288ee2765e7 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 29 May 2026 18:05:28 +0200 Subject: [PATCH 1/2] Make background-refresh pausing reentrant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the pauseBackgroundRefreshes bool with a count. The single existing caller (subprocess suspend/resume) is unaffected, but we're about to add a second, independent reason to pause — lazygit driving a git operation that the background routines would otherwise catch mid-flight — and the two scopes can overlap. A bool can't represent "two things both want refreshes paused"; a count can. --- pkg/gui/background.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 8795b49aac1..2575aedd123 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -3,6 +3,7 @@ package gui import ( "fmt" "runtime" + "sync/atomic" "time" "github.com/jesseduffield/lazygit/pkg/gocui" @@ -13,17 +14,27 @@ import ( type BackgroundRoutineMgr struct { gui *Gui - // if we've suspended the gui (e.g. because we've switched to a subprocess) - // we typically want to pause some things that are running like background - // file refreshes - pauseBackgroundRefreshes bool + // When this is greater than zero, the background routines (e.g. file refresh) + // skip their work. We pause them while the gui is suspended (e.g. for a + // subprocess) and while lazygit is itself driving a git operation that would + // otherwise be caught mid-flight (see the waiting-status helpers). It's a + // count rather than a bool because these pause scopes can overlap. + pauseRefreshesCount atomic.Int32 // a channel to trigger an immediate background fetch; we use this when switching repos triggerFetch chan struct{} } func (self *BackgroundRoutineMgr) PauseBackgroundRefreshes(pause bool) { - self.pauseBackgroundRefreshes = pause + if pause { + self.pauseRefreshesCount.Add(1) + } else { + self.pauseRefreshesCount.Add(-1) + } +} + +func (self *BackgroundRoutineMgr) backgroundRefreshesPaused() bool { + return self.pauseRefreshesCount.Load() > 0 } func (self *BackgroundRoutineMgr) startBackgroundRoutines() { @@ -124,7 +135,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru ticker := time.NewTicker(interval) defer ticker.Stop() doit := func(retriggered bool) { - if self.pauseBackgroundRefreshes { + if self.backgroundRefreshesPaused() { return } self.gui.c.OnWorker(func(gocui.Task) error { From fab55b90762a4c8a5d544d76ea709994fa53c7f8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 29 May 2026 18:08:07 +0200 Subject: [PATCH 2/2] Pause background refreshes while driving a git operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several commands (rewording or amending an earlier commit, custom patch operations, etc.) are implemented by starting an interactive rebase that stops at a commit, amending it, and continuing. When no conflict occurs, the user isn't meant to notice a rebase happened at all. But a background file refresh can fire while the rebase is mid-flight and render a dirty working copy of whatever the behind-the-scenes rebase is doing (e.g. applying a custom patch). To fix this, we pause the background routines for the duration of any waiting-status operation — exactly the window in which lazygit is driving the git operation itself and will refresh once at the end. The boundary is also right for the conflict case: when a rebase stops on a conflict the operation returns, the pause releases, and background refreshes resume for the interactive resolution that follows. --- pkg/gui/controllers/helpers/app_status_helper.go | 10 ++++++++++ pkg/gui/controllers/helpers/inline_status_helper.go | 8 ++++++++ pkg/gui/gui_common.go | 4 ++++ pkg/gui/types/common.go | 4 ++++ 4 files changed, 26 insertions(+) diff --git a/pkg/gui/controllers/helpers/app_status_helper.go b/pkg/gui/controllers/helpers/app_status_helper.go index 17c61ae2632..b691db4a4b3 100644 --- a/pkg/gui/controllers/helpers/app_status_helper.go +++ b/pkg/gui/controllers/helpers/app_status_helper.go @@ -66,12 +66,22 @@ func (self *AppStatusHelper) WithWaitingStatus(message string, f func(gocui.Task } func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui.Task) error, task gocui.Task) error { + // A waiting status means lazygit is driving a git operation itself (often + // one that internally runs a rebase and continues it). Pause the background + // routines for its duration so they don't refresh from an intermediate + // state and reveal, say, the half-finished history of a reword. + self.c.PauseBackgroundRefreshes(true) + defer self.c.PauseBackgroundRefreshes(false) + return self.statusMgr().WithWaitingStatus(message, self.renderAppStatus, func(waitingStatusHandle *status.WaitingStatusHandle) error { return f(appStatusHelperTask{task, waitingStatusHandle}) }) } func (self *AppStatusHelper) WithWaitingStatusSync(message string, f func() error) error { + self.c.PauseBackgroundRefreshes(true) + defer self.c.PauseBackgroundRefreshes(false) + return self.statusMgr().WithWaitingStatus(message, func() {}, func(*status.WaitingStatusHandle) error { stop := make(chan struct{}) defer func() { close(stop) }() diff --git a/pkg/gui/controllers/helpers/inline_status_helper.go b/pkg/gui/controllers/helpers/inline_status_helper.go index 02afcdd501e..0902c5bf261 100644 --- a/pkg/gui/controllers/helpers/inline_status_helper.go +++ b/pkg/gui/controllers/helpers/inline_status_helper.go @@ -68,6 +68,14 @@ func (self *InlineStatusHelper) WithInlineStatus(opts InlineStatusOpts, f func(g visible := view.Visible && self.windowHelper.TopViewInWindow(context.GetWindowName(), false) == view if visible && context.IsItemVisible(opts.Item) { self.c.OnWorker(func(task gocui.Task) error { + // An inline status is just a waiting status rendered on the item + // rather than in the bottom line, so it gets the same treatment: + // pause the background routines while we drive the operation. (The + // off-screen branch below goes through WithWaitingStatus, which + // already does this.) + self.c.PauseBackgroundRefreshes(true) + defer self.c.PauseBackgroundRefreshes(false) + self.start(opts) defer self.stop(opts) diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index 07945c350ed..c74a99a0568 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -50,6 +50,10 @@ func (self *guiCommon) Resume() error { return self.gui.resume() } +func (self *guiCommon) PauseBackgroundRefreshes(pause bool) { + self.gui.BackgroundRoutineMgr.PauseBackgroundRefreshes(pause) +} + func (self *guiCommon) Context() types.IContextMgr { return self.gui.State.ContextMgr } diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 92f004634a0..b81fb15e135 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -59,6 +59,10 @@ type IGuiCommon interface { Suspend() error Resume() error + // Pause or resume the background routines. Calls nest, so every pause must be balanced + // by a resume. + PauseBackgroundRefreshes(pause bool) + Context() IContextMgr ContextForKey(key ContextKey) Context