jesseduffield.lazygit/pkg/gui/controllers/helpers/gpg_helper.go
Stefan Haller 080542c9fb Capture the commits refresh's inputs on the UI thread
A commits refresh does its git work on a worker and then reads the model,
the contexts, and the modes for that work directly from there:
LocalCommits.GetSelectionRangeAndMode/GetLimitCommits/GetShowWholeGitGraph,
Model.Commits/MainBranches/HashPool, the filtering path/author. Those are
owned by the UI thread, which is concurrently running the cursor and render
code, so the reads race it — the dominant, confirmed source of the
commits-scope flakes (the startup ClampSelection vs GetSelectionRangeAndMode
race, for one).

Gather them into an immutable capturedCommitState on the UI thread, before
the git work is dispatched, and have refreshCommitsWithLimit compute from
that snapshot. UI-thread callers capture inline; worker callers can't (a
SYNC/BLOCK_UI refresh parks the UI thread at wg.Wait, so hopping from a
scope sub-worker would deadlock), so the capture is lifted out of the scope
worker into the refresh orchestration, and worker callers announce
themselves with a new RefreshFromWorker entry point that hops the capture to
the UI thread and blocks for it (OnUIThreadAndWait). BLOCK_UI runs the whole
refresh on the UI thread regardless of the caller, so it captures inline
too.

Every refresh issued from a worker that reaches the commits (or branches,
which pulls in commits) scope is converted: the fast-forward, branch/tag
delete, worktree remove/detach, push, reword-via-rebase, author edits,
custom-command, hard-reset-with-autostash, reset-to-ref, fetch-and-checkout,
gpg-stream, post-fetch, and external-change-poller refreshes, plus the
branch checkout and move-commits-to-new-branch refreshes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00

107 lines
3.1 KiB
Go

package helpers
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type GpgHelper struct {
c *HelperCommon
}
func NewGpgHelper(c *HelperCommon) *GpgHelper {
return &GpgHelper{
c: c,
}
}
func (self *GpgHelper) WithGpgHandling(
cmdObj *oscommands.CmdObj,
configKey git_commands.GpgConfigKey,
waitingStatus string,
onSuccess func() error,
refreshScope []types.RefreshableView,
) error {
refreshOptions := types.RefreshOptions{Mode: types.ASYNC, Scope: refreshScope}
return self.withGpgHandling(
cmdObj, configKey, waitingStatus, onSuccess, refreshOptions, refreshOptions)
}
// WithGpgHandlingAndSelectHeadCommit is like WithGpgHandling, but on success it
// selects the new HEAD commit rather than restoring the previous selection. For
// committing, where the commit we just created is the one we want selected.
func (self *GpgHelper) WithGpgHandlingAndSelectHeadCommit(
cmdObj *oscommands.CmdObj,
configKey git_commands.GpgConfigKey,
waitingStatus string,
onSuccess func() error,
) error {
failureRefreshOptions := types.RefreshOptions{Mode: types.ASYNC}
successRefreshOptions := types.RefreshOptions{Mode: types.ASYNC, CommitSelection: types.SelectHeadCommit}
return self.withGpgHandling(
cmdObj, configKey, waitingStatus, onSuccess, failureRefreshOptions, successRefreshOptions)
}
// Currently there is a bug where if we switch to a subprocess from within
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
// fix this bug, or just stop running subprocesses from within there, given that
// we don't need to see a loading status if we're in a subprocess.
func (self *GpgHelper) withGpgHandling(
cmdObj *oscommands.CmdObj,
configKey git_commands.GpgConfigKey,
waitingStatus string,
onSuccess func() error,
failureRefreshOptions types.RefreshOptions,
successRefreshOptions types.RefreshOptions,
) error {
useSubprocess := self.c.Git().Config.NeedsGpgSubprocess(configKey)
if useSubprocess {
success, err := self.c.RunSubprocess(cmdObj)
if success && onSuccess != nil {
if err := onSuccess(); err != nil {
return err
}
}
if success {
self.c.Refresh(successRefreshOptions)
} else {
self.c.Refresh(failureRefreshOptions)
}
return err
}
return self.runAndStream(
cmdObj, waitingStatus, onSuccess, failureRefreshOptions, successRefreshOptions)
}
func (self *GpgHelper) runAndStream(
cmdObj *oscommands.CmdObj,
waitingStatus string,
onSuccess func() error,
failureRefreshOptions types.RefreshOptions,
successRefreshOptions types.RefreshOptions,
) error {
return self.c.WithWaitingStatus(waitingStatus, func(gocui.Task) error {
if err := cmdObj.StreamOutput().Run(); err != nil {
self.c.RefreshFromWorker(failureRefreshOptions)
return fmt.Errorf(
self.c.Tr.GitCommandFailed, self.c.UserConfig().Keybinding.Universal.ExtrasMenu,
)
}
if onSuccess != nil {
if err := onSuccess(); err != nil {
return err
}
}
self.c.RefreshFromWorker(successRefreshOptions)
return nil
})
}