From 7a0f475414cfd4e28d934fbb6f10a1087669a27a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 16 Sep 2026 07:55:30 +0200 Subject: [PATCH] Let a render refresh the index again on Windows Renders were kept from refreshing git's index because a pty-rendered command on Windows is terminated at an arbitrary instruction when its task stops, and one landing in the window where git holds index.lock to write back refreshed stat information leaves that lock behind. A render no longer runs in a pty there, and nothing kills it any more either: the pipe to the renderer breaks, git's write fails, and git dies through its own die path with its lock files cleaned up. So let the refresh happen, and let renders heal stale stat info the way they do everywhere else. The teardown of a pseudoconsole took its reassurance about index.lock from this, and renders were the reason it held. What still runs in a pty there is a custom command asking for logWithPty and a command that may be asked for a credential, so the claim no longer follows; drop it rather than restate it for clients it was never about. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/commands/oscommands/pty_windows.go | 4 +-- pkg/gui/main_view_render.go | 2 -- pkg/gui/pty.go | 39 -------------------------- pkg/gui/pty_test.go | 29 ------------------- 4 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 pkg/gui/pty_test.go diff --git a/pkg/commands/oscommands/pty_windows.go b/pkg/commands/oscommands/pty_windows.go index ff707c519..b36342e61 100644 --- a/pkg/commands/oscommands/pty_windows.go +++ b/pkg/commands/oscommands/pty_windows.go @@ -144,9 +144,7 @@ func TerminateLivePtys() { // graceful signal worth waiting on — git and the common diff tools leave it // to the default handler, which calls ExitProcess at whatever instruction // the process happens to execute — so clients that got the event are -// already dying. Killing at an arbitrary point cannot leak a stale -// index.lock, because pty-rendered commands don't take that lock (see -// withPtyGitConfig in pkg/gui/pty.go). +// already dying. // // The pseudoconsole close gets its own goroutine because the kill must not // wait for it: on builds where ClosePseudoConsole blocks until the console diff --git a/pkg/gui/main_view_render.go b/pkg/gui/main_view_render.go index 1a6eb459a..bfeba9b16 100644 --- a/pkg/gui/main_view_render.go +++ b/pkg/gui/main_view_render.go @@ -40,8 +40,6 @@ func (gui *Gui) newRenderTask(view *gocui.View, cmd *exec.Cmd, prefix string) er return gui.newCmdTask(view, cmd, prefix) } - cmd.Args = withPtyGitConfig(cmd.Args, runtime.GOOS) - // Mark the view as loading synchronously now, before the layout pass: the // actual task is created in afterLayout (below), which runs after layout, so // without this the next layout pass would clamp the scroll position to the diff --git a/pkg/gui/pty.go b/pkg/gui/pty.go index 0cb718d8c..c53b77a21 100644 --- a/pkg/gui/pty.go +++ b/pkg/gui/pty.go @@ -4,8 +4,6 @@ import ( "io" "os" "os/exec" - "path/filepath" - "strings" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gocui" @@ -107,40 +105,3 @@ func (gui *Gui) ptyRender(spec renderSpec) (startRender, onCloseRender) { return start, onClose } - -// withPtyGitConfig returns args with extra git configuration for commands -// that render into a pty. On Windows, such a command is terminated at an -// arbitrary point of its execution when its task stops: tearing down the -// pseudoconsole delivers CTRL_CLOSE_EVENT, which git leaves to the default -// handler, which just calls ExitProcess. git's automatic index refresh -// (diff.autoRefreshIndex, on by default) takes index.lock at the end of a -// diff against the worktree to write back refreshed stat information — -// GIT_OPTIONAL_LOCKS does not cover this lock — and a termination landing -// in that window leaves a stale index.lock behind that the next git command -// chokes on. So don't let pty-rendered commands refresh the index; -// lazygit's foreground `git status` refreshes, which never run in a pty, -// keep the stat cache fresh instead. -// -// On Unix a stopped pty child gets SIGTERM, and git's signal handlers remove -// its lock files, so the refresh can stay enabled there and keep healing -// stale stat info. -func withPtyGitConfig(args []string, goos string) []string { - if goos != "windows" { - return args - } - // Most pty commands are direct git invocations, but the user-configured - // ones can be arbitrary command lines (e.g. a branchLogCmd wrapping git - // in `sh -c`), and injecting git flags into those would corrupt them. - // Only direct git invocations get the config; that loses nothing, since - // the wrapped commands are log commands, which never take the index - // lock. (For direct invocations other than worktree diffs the config is - // simply a no-op.) - base := strings.TrimSuffix(strings.ToLower(filepath.Base(args[0])), ".exe") - if base != "git" { - return args - } - result := make([]string, 0, len(args)+2) - result = append(result, args[0]) - result = append(result, "-c", "diff.autoRefreshIndex=false") - return append(result, args[1:]...) -} diff --git a/pkg/gui/pty_test.go b/pkg/gui/pty_test.go deleted file mode 100644 index 8d7f0e2ca..000000000 --- a/pkg/gui/pty_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package gui - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestWithPtyGitConfig(t *testing.T) { - args := []string{"git", "-C", "/repo", "diff", "--color=always"} - - assert.Equal(t, - []string{"git", "-c", "diff.autoRefreshIndex=false", "-C", "/repo", "diff", "--color=always"}, - withPtyGitConfig(args, "windows")) - - assert.Equal(t, args, withPtyGitConfig(args, "linux")) - assert.Equal(t, args, withPtyGitConfig(args, "darwin")) - - // A user-configured command that wraps git in a shell must not have git - // flags injected into it. - shellArgs := []string{"sh", "-c", "git log --graph {{branchName}} -- | sed -e s/x/y/"} - assert.Equal(t, shellArgs, withPtyGitConfig(shellArgs, "windows")) - - // The guard recognizes git regardless of case and extension. - exeArgs := []string{"GIT.EXE", "diff"} - assert.Equal(t, - []string{"GIT.EXE", "-c", "diff.autoRefreshIndex=false", "diff"}, - withPtyGitConfig(exeArgs, "windows")) -}