Fix Files Panel artefacts during rebase commands (#5661)

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.
This commit is contained in:
Stefan Haller 2026-06-19 18:06:58 +02:00 committed by GitHub
commit fcaf1512bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 43 additions and 6 deletions

View file

@ -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 {

View file

@ -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) }()

View file

@ -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)

View file

@ -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
}

View file

@ -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