Skip to content

Detect external repo changes via background polling#5662

Open
stefanhaller wants to merge 10 commits into
fix-files-panel-artefacts-during-rebase-commandsfrom
external-change-detection
Open

Detect external repo changes via background polling#5662
stefanhaller wants to merge 10 commits into
fix-files-panel-artefacts-during-rebase-commandsfrom
external-change-detection

Conversation

@stefanhaller
Copy link
Copy Markdown
Collaborator

Lazygit refreshes its UI when its terminal window gets the focus, which is enough for the situation where the user makes a commit in their IDE or in another git client. It isn't enough for the case that a coding agent makes commits in the background. Improve this by doing a light-weight poll of the git state in the background, and refresh when a change is detected.

Addresses #5554.

The schema annotated refreshInterval and fetchInterval with minimum=0,
but the background routines reject a value of 0 (they require
interval > 0 and otherwise log it as invalid and disable the feature).
So 0 is not actually a valid value; switch to exclusiveMinimum=0 so the
schema matches what the code accepts.
For certain kinds of performance investigations it is useful to see this, and
doesn't terribly pollute the log, so just do this always.
Several downstream conditions in Refresh() relied on multi-scope predicates to
express "if X is in scope, Y also needs refreshing". This makes it hard to add
new code that needs to ask "does this refresh re-read refs?", because the answer
involves mirroring one of those predicates and keeping them in sync forever.

Expand the co-refreshing relationships once, up front, right after the scope set
is built. The downstream conditions then collapse to single-scope checks against
the (now-expanded) set. Behavior is preserved.

Two of the scattered multi-scope conditions are intentionally left as-is because
they express subsumption rather than co-refresh (one branch already does the
work of another internally — expanding would cause double-refresh), and one
expresses mid-function coupling on a flag set inside the COMMITS/BRANCHES block.
A cheap fingerprint of local branches and HEAD that future code can poll to
detect when refs have moved externally.

Branches come from a porcelain for-each-ref. HEAD is read directly from
.git/HEAD: that avoids spawning a child process and captures the symref-or-hash
distinction we need to tell "detached at X" apart from "on a branch pointing at
X" — they share a commit hash, which is exactly the situation at the end of a
rebase when HEAD reattaches to the branch. The reftable backend doesn't keep a
real .git/HEAD (it writes a fixed stub), so when we see that stub or the file is
unreadable we fall back to porcelain commands, which are backend-agnostic.

Uses DontLog so a future polling caller won't spam the command log. Not yet
wired up to any caller.
Two settings to control the upcoming background polling mechanism:

- git.autoDetectExternalChanges (default true) is the on/off switch, parallel to
  autoFetch/autoRefresh
- refresher.externalChangeCheckInterval (default 2 seconds) is the poll cadence

Disabling is the bool's job, not a magic 0 interval, matching the existing
convention.

Not yet referenced by any code.
Add the storage and snapshot-update half of the external-change-detection
mechanism. RefreshHelper now keeps a mutex-protected snapshot string and exposes
accessors for it; Refresh captures a fresh snapshot at the start of any refresh
whose scope set includes COMMITS or BRANCHES.

We capture before reading the git state, not after. Capturing after would let an
external change that lands between the git state read and the snapshot (say, the
next step of a rebase running in another terminal) leave the stored snapshot
newer than what we actually rendered; the poller would then see no difference
and never refresh again, stranding the UI on the intermediate state. Capturing
first keeps the snapshot from running ahead of the render, so if disk moves
during the refresh the next poll catches it.

No reader of the snapshot exists yet — the polling goroutine that consumes it
comes in a later commit. Keeping the snapshot hook in its own commit isolates
the invariant that the snapshot stays in sync with what the UI has observed,
which is what makes the poller's change-detection predicate work across in-app
commands and focus-in refreshes.
Add a 2-second background poll that calls Status.RefsSnapshot and
compares against the snapshot stored at the end of the last refs-
touching refresh. On a diff, trigger a full refresh — same scope as the
focus-in handler, because once we know something changed externally
we can't be sure what (an agent might have created a worktree or
stashed something alongside the commit we detected).

Refresh runs in SYNC mode because goEvery already serializes iterations
via <-done: a slow refresh delays the next tick naturally instead of
letting work stack. The post-refresh hook from the previous commit
updates the snapshot, so in-app commands don't cause the next poll to
spuriously re-fire.

Disabled in the integration test config, like autoRefresh and autoFetch,
because demo replays make repo changes throughout the run; at 2-second
cadence the resulting full refreshes compete with the demo's own
choreography and push some demos past their 40-second timeout.

Also list the two new config keys in checkForChangedConfigsThatDontAutoReload
so a config edit warns the user that lazygit needs a restart.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant