Skip to content

worktree: Avoid dropping git repositories during watcher rescans - #59976

Merged
ChristopherBiscardi merged 2 commits into
zed-industries:mainfrom
RemcoSmitsDev:preserve-git-repos-on-recursive-rescan
Jun 29, 2026
Merged

worktree: Avoid dropping git repositories during watcher rescans#59976
ChristopherBiscardi merged 2 commits into
zed-industries:mainfrom
RemcoSmitsDev:preserve-git-repos-on-recursive-rescan

Conversation

@RemcoSmitsDev

@RemcoSmitsDev RemcoSmitsDev commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Related to #59610.

Objective

When the filesystem watcher loses sync (common on macOS under heavy churn, e.g. a dev build constantly rewriting target/ and .git/), it forces a recursive rescan of the worktree root. That rescan removes the whole subtree from the snapshot before re-scanning it, and remove_path_from_snapshot was unconditionally pruning the worktree's git repositories along with it.

Because the scanner publishes intermediate snapshots while it works, a snapshot could be published in the window after the repository was pruned but before .git was re-scanned. The GitStore would see the repository as removed, tear it down, and then re-create it with a fresh RepositoryId once it reappeared — churning the id on every watcher overflow and spamming RepositoryUpdated events to all consumers (git panel, git graph, blame, etc.).

Here some debug logs,
See that the repoId inside the git graph event is different and is 8 now, even though I only have one repository and didn't do any git related updates.

2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for Native; scheduling rescans for 11 registrations
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 INFO  [git::repository] opening git repository at "/Users/remcosmits/Documents/code/zed/.git" using git binary "/opt/homebrew/bin/git"
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/zed/src/main.rs:1991] Is a directory (os error 21)
2026-06-26T18:38:44+02:00 INFO  [project::prettier_store] Prettier config file ".prettierrc" changed, reloading prettier instances for worktree 1
2026-06-26T18:38:45+02:00 INFO  [project::prettier_store] Prettier config file ".prettierrc" changed, reloading prettier instances for worktree 1
2026-06-26T18:38:45+02:00 INFO  [git::repository] opening git repository at "/Users/remcosmits/Documents/code/zed/.git" using git binary "/opt/homebrew/bin/git"
2026-06-26T18:38:45+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:45+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled


[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
[crates/git_ui/src/git_graph.rs:1468:21] "other repo id" = "other repo id"
[crates/git_ui/src/git_graph.rs:1468:21] &updated_repo_id = RepositoryId(
    8,
)
[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
[crates/git_ui/src/git_graph.rs:1468:21] "other repo id" = "other repo id"
[crates/git_ui/src/git_graph.rs:1468:21] &updated_repo_id = RepositoryId(
    8,
)
[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
[crates/git_ui/src/git_graph.rs:1468:21] "other repo id" = "other repo id"
[crates/git_ui/src/git_graph.rs:1468:21] &updated_repo_id = RepositoryId(
    8,
)
[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)

Solution

This change makes remove_path_from_snapshot prune git repositories only when the path was genuinely removed (metadata == Ok(None)), not during a recursive refresh where the subtree is about to be re-scanned. Stale repositories are still reaped authoritatively against the filesystem in update_git_repositories.

Testing

It seems to be that I'm the only that can reproduce this issue @Anthony-Eid tried reproducing this but couldn't.
Note: that I dindn't write a regression test since this is a async timing issue that I couldn't figure out to write a test for.

Steps how I can reproduce this:

  1. Run cargo run
  2. Open Zed as only project
  3. Open the git graph
  4. Open a rust file e.g. git_graph.rs
  5. CTRL-C inside the terminal to kill Zed
  6. Run cargo run see that you have git graph open and a rust file, note that cargo check is doing it's thing.
  7. See within a few seconds that Zed's UI flickers (especially the project panel & nav bar with the selected repo & branch)

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content adheres to Zed's UI standards (UX/UI and icon guidelines)
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Showcase

Note: This PR only fixes that we don't send as many git store events, so this results in not having to update the nav bar where the selected repo & branch on every event (that is duplicated in a way). Result of that we now also fixed a rare case that the git graph commit entries where gone, this was because we incremented the repoId and the graph couldn't find the repo anymore with the id that it has stored on the gitGraph struct itself.

Before

See that the nav bar with selected repo & branch flickers and the git graph commits are empty.

Screen.Recording.2026-06-26.at.18.18.18.mov

See that the git panel entries flicker and we have an temporary empty panel.

Screen.Recording.2026-06-27.at.11.49.37.mov

After

See that the nav bar with selected repo & branch does not flicker anymore and that the git graph keeps showing the commits.

Screen.Recording.2026-06-26.at.20.03.30.mov

See that the git panel does not show an empy state and does not flicker anymore.

Screen.Recording.2026-06-27.at.11.52.36.mov

cc @Anthony-Eid Since we where debugging this yesterday.

Release Notes:

  • Fixed git repositories being repeatedly torn down and re-created when the filesystem watcher forced a rescan, which caused redundant git status refreshes and UI churn.

When the filesystem watcher loses sync (common on macOS under heavy
churn, e.g. a dev build constantly rewriting `target/` and `.git/`), it
forces a recursive rescan of the worktree root. That rescan removes the
whole subtree from the snapshot before re-scanning it, and
`remove_path_from_snapshot` was unconditionally pruning the worktree's
git repositories along with it.

Because the scanner publishes intermediate snapshots while it works, a
snapshot could be published in the window after the repository was
pruned but before `.git` was re-scanned. The `GitStore` would see the
repository as removed, tear it down, and then re-create it with a fresh
`RepositoryId` once it reappeared — churning the id on every watcher
overflow and spamming `RepositoryUpdated` events to all consumers (git
panel, git graph, blame, etc.).

This change makes `remove_path_from_snapshot` prune git repositories
only when the path was genuinely removed (`metadata == Ok(None)`), not
during a recursive refresh where the subtree is about to be re-scanned.
Stale repositories are still reaped authoritatively against the
filesystem in `update_git_repositories`.
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jun 26, 2026
@zed-community-bot zed-community-bot Bot added the community champion Issues filed by our amazing community champions! 🫶 label Jun 26, 2026
@RemcoSmitsDev RemcoSmitsDev changed the title git: Reduce the amount of repo updates/events we send over the git store git_ui: Reduce flickering while processing git events Jun 26, 2026
@RemcoSmitsDev

Copy link
Copy Markdown
Collaborator Author

Here a quick before/after profiling summary:

GitStoreEvent events observed in git_graph (before vs. after)

Metric Before After Improvement
GitStoreEvent events received 258 99 ~62% fewer (−159)
RepositoryId at end RepositoryId(4) RepositoryId(1) No id churn (stayed 1)

The fix roughly halves the GitStoreEvent events the git graph has to react to, and — most importantly — the RepositoryId stays stable at 1 instead of climbing to 4. The id no longer churning is the direct signal that the repository is no longer being torn down and re-created on each watcher rescan, which is what was driving the redundant updates and UI flicker.

@RemcoSmitsDev RemcoSmitsDev changed the title git_ui: Reduce flickering while processing git events worktree: Avoid dropping git repositories during watcher rescans Jun 29, 2026

@ChristopherBiscardi ChristopherBiscardi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This is a great PR. The screenrecordings, code comments, and benchmarking is very much appreciated.

@ChristopherBiscardi ChristopherBiscardi self-assigned this Jun 29, 2026
@ChristopherBiscardi
ChristopherBiscardi added this pull request to the merge queue Jun 29, 2026
Merged via the queue into zed-industries:main with commit cd7f1a0 Jun 29, 2026
39 checks passed
@RemcoSmitsDev
RemcoSmitsDev deleted the preserve-git-repos-on-recursive-rescan branch July 4, 2026 12:55
pull Bot pushed a commit to Jaleel-zhu/zed that referenced this pull request Jul 24, 2026
…ed-industries#61541)

# Objective

When the OS file watcher loses sync (e.g. its event queue overflows
under heavy fs churn), it drops pending events and reports a single
`Rescan` event for the watched root. Git changes hidden behind such a
rescan were silently lost, leaving the git panel and branch indicator
stale until something else touched `.git`.

Contributes to zed-industries#13176. May fix zed-industries#60102, though that report predates
zed-industries#60660 and may already be addressed by it on nightly (see Related PRs
below).

Two bugs caused this, and they masked each other:

1. **A rescan never triggered a git reload.** The `Rescan` event's path
is the worktree root, not something inside `.git`, so it never populated
`dot_git_abs_paths` in `process_events` — and since `.git` is excluded
from entry scanning, the rescan produced no `.git` events either. The
dropped git changes were simply never picked up.
2. **Re-scanning reset `git_dir_scan_id` to 0.** The snapshot diff
detects git changes by comparing scan ids, so re-inserting the
repository with a fresh id could wipe out a bump made earlier in the
same scan cycle, swallowing the corresponding `UpdatedGitRepositories`
signal.

The masking is why these fixes land together rather than as two PRs: the
root-rescan case in `test_dot_git_dir_event_does_not_suppress_children`
only passed on `main` because the buggy scan-id reset made the snapshot
diff fire spuriously. Fixing either bug alone turns that (currently
green) test red.

## Solution

- `process_events`: a `Rescan` event now schedules a git state reload
for every repository whose git directory (`dot_git`, `common_dir`, or
`repository_dir`) lies under the rescanned path, covering linked
worktrees and gitfile repositories, including git dirs watched outside
the worktree root.
- `insert_git_repository_for_path`: carry the existing `git_dir_scan_id`
forward when re-inserting a repository instead of resetting it to 0.
Deliberately *not* bumped either: re-insertion is snapshot bookkeeping,
not evidence of a git change — bumping would trigger spurious full
reloads on non-lossy paths (explicit refreshes, path-prefix scans). Only
`update_git_repositories` claims that git state changed.
- `changed_repos`: a `debug_assert` enforcing that `git_dir_scan_id`
never regresses, so future violations of this invariant fail loudly in
tests instead of manifesting as a stale git panel.

## Testing

New fault-injection infrastructure and tests:

- `FakeFs::simulate_watcher_overflow` models the kernel's watch queue
overflowing: buffered (undelivered) events are discarded and replaced by
a single `Rescan` for the given root, mirroring FSEvents
`kFSEventStreamEventFlagMustScanSubDirs`, inotify `IN_Q_OVERFLOW`, and
Windows `ERROR_NOTIFY_ENUM_DIR`.
- `test_watcher_overflow_rescan_reloads_git_state`: a git change whose
events are lost to an overflow must still be picked up via the rescan
(reproduces bug 1; fails on `main`).
- `test_git_update_in_same_batch_as_rescan_is_not_lost`: a git event
processed in the same batch as a rescan must not lose its scan-id bump
to the repository re-insertion (reproduces bug 2; fails on `main`).
- `test_random_git_updates_with_watcher_overflows`: randomized property
test (100 iterations) asserting that every git state change is
eventually signaled via `UpdatedGitRepositories` under random event
batching, delays, and overflows. Fails on `main` within the first few
seeds.
- `test_random_worktree_changes` now also injects watcher overflows,
extending its existing convergence property to rescan reconciliation of
worktree entries (this already passed; the injection guards it going
forward).

Full `worktree` and `fs` suites pass. Verified the directed tests
exercise the intended code paths via trace logging (the same-batch test
hits `update_git_repositories` stamping followed by re-insertion,
distinct from the overflow test where no `.git` event arrives at all).

## Related PRs

The recent stale-git-state reports trace back to three distinct
mechanisms that share one symptom. This PR addresses the third:

- **Events never generated** — zed-industries#60660 (merged, in this PR's base):
Linux's non-recursive watcher missed nested `refs/` directories, so
external commits/fetches produced no events at all. That PR (together
with zed-industries#60590, which explicitly rescans after Zed-initiated reset/fetch
and touches only `git_store.rs`) fixed zed-industries#60348. No overlap with this PR;
a merge against current `main` is clean, and the refs-watching tests are
disjoint from the overflow/rescan tests added here.
- **Events coalesced** — zed-industries#59876 (open, complementary): FSEvents can
merge `.git` child events into a bare `.git` `Changed` event; the signal
arrives, in a shape Zed ignores. @RemcoSmitsDev's review comment there
describes this PR's failure mode and calls the two fixes complementary;
this is effectively the follow-up promised in that comment. Both PRs
touch the same region of `process_events`, so whichever lands second
needs a trivial rebase, and zed-industries#59876 flips the bare-`.git` expectation in
`test_dot_git_dir_event_does_not_suppress_children` Case 2, which this
PR preserves.
- **Events dropped** — this PR: the watcher generated events but lost
them to a queue overflow, and the resulting `Rescan` did not reach the
git reload path. zed-industries#59976 and zed-industries#60098 (merged) reduced how often this
happens; this PR makes git state recover correctly when it does.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed the git panel and branch indicator showing stale state after
heavy file-system activity caused the file watcher to lose events
0arm pushed a commit to 0arm/zed that referenced this pull request Jul 26, 2026
…ed-industries#61541)

# Objective

When the OS file watcher loses sync (e.g. its event queue overflows
under heavy fs churn), it drops pending events and reports a single
`Rescan` event for the watched root. Git changes hidden behind such a
rescan were silently lost, leaving the git panel and branch indicator
stale until something else touched `.git`.

Contributes to zed-industries#13176. May fix zed-industries#60102, though that report predates
zed-industries#60660 and may already be addressed by it on nightly (see Related PRs
below).

Two bugs caused this, and they masked each other:

1. **A rescan never triggered a git reload.** The `Rescan` event's path
is the worktree root, not something inside `.git`, so it never populated
`dot_git_abs_paths` in `process_events` — and since `.git` is excluded
from entry scanning, the rescan produced no `.git` events either. The
dropped git changes were simply never picked up.
2. **Re-scanning reset `git_dir_scan_id` to 0.** The snapshot diff
detects git changes by comparing scan ids, so re-inserting the
repository with a fresh id could wipe out a bump made earlier in the
same scan cycle, swallowing the corresponding `UpdatedGitRepositories`
signal.

The masking is why these fixes land together rather than as two PRs: the
root-rescan case in `test_dot_git_dir_event_does_not_suppress_children`
only passed on `main` because the buggy scan-id reset made the snapshot
diff fire spuriously. Fixing either bug alone turns that (currently
green) test red.

## Solution

- `process_events`: a `Rescan` event now schedules a git state reload
for every repository whose git directory (`dot_git`, `common_dir`, or
`repository_dir`) lies under the rescanned path, covering linked
worktrees and gitfile repositories, including git dirs watched outside
the worktree root.
- `insert_git_repository_for_path`: carry the existing `git_dir_scan_id`
forward when re-inserting a repository instead of resetting it to 0.
Deliberately *not* bumped either: re-insertion is snapshot bookkeeping,
not evidence of a git change — bumping would trigger spurious full
reloads on non-lossy paths (explicit refreshes, path-prefix scans). Only
`update_git_repositories` claims that git state changed.
- `changed_repos`: a `debug_assert` enforcing that `git_dir_scan_id`
never regresses, so future violations of this invariant fail loudly in
tests instead of manifesting as a stale git panel.

## Testing

New fault-injection infrastructure and tests:

- `FakeFs::simulate_watcher_overflow` models the kernel's watch queue
overflowing: buffered (undelivered) events are discarded and replaced by
a single `Rescan` for the given root, mirroring FSEvents
`kFSEventStreamEventFlagMustScanSubDirs`, inotify `IN_Q_OVERFLOW`, and
Windows `ERROR_NOTIFY_ENUM_DIR`.
- `test_watcher_overflow_rescan_reloads_git_state`: a git change whose
events are lost to an overflow must still be picked up via the rescan
(reproduces bug 1; fails on `main`).
- `test_git_update_in_same_batch_as_rescan_is_not_lost`: a git event
processed in the same batch as a rescan must not lose its scan-id bump
to the repository re-insertion (reproduces bug 2; fails on `main`).
- `test_random_git_updates_with_watcher_overflows`: randomized property
test (100 iterations) asserting that every git state change is
eventually signaled via `UpdatedGitRepositories` under random event
batching, delays, and overflows. Fails on `main` within the first few
seeds.
- `test_random_worktree_changes` now also injects watcher overflows,
extending its existing convergence property to rescan reconciliation of
worktree entries (this already passed; the injection guards it going
forward).

Full `worktree` and `fs` suites pass. Verified the directed tests
exercise the intended code paths via trace logging (the same-batch test
hits `update_git_repositories` stamping followed by re-insertion,
distinct from the overflow test where no `.git` event arrives at all).

## Related PRs

The recent stale-git-state reports trace back to three distinct
mechanisms that share one symptom. This PR addresses the third:

- **Events never generated** — zed-industries#60660 (merged, in this PR's base):
Linux's non-recursive watcher missed nested `refs/` directories, so
external commits/fetches produced no events at all. That PR (together
with zed-industries#60590, which explicitly rescans after Zed-initiated reset/fetch
and touches only `git_store.rs`) fixed zed-industries#60348. No overlap with this PR;
a merge against current `main` is clean, and the refs-watching tests are
disjoint from the overflow/rescan tests added here.
- **Events coalesced** — zed-industries#59876 (open, complementary): FSEvents can
merge `.git` child events into a bare `.git` `Changed` event; the signal
arrives, in a shape Zed ignores. @RemcoSmitsDev's review comment there
describes this PR's failure mode and calls the two fixes complementary;
this is effectively the follow-up promised in that comment. Both PRs
touch the same region of `process_events`, so whichever lands second
needs a trivial rebase, and zed-industries#59876 flips the bare-`.git` expectation in
`test_dot_git_dir_event_does_not_suppress_children` Case 2, which this
PR preserves.
- **Events dropped** — this PR: the watcher generated events but lost
them to a queue overflow, and the resulting `Rescan` did not reach the
git reload path. zed-industries#59976 and zed-industries#60098 (merged) reduced how often this
happens; this PR makes git state recover correctly when it does.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed the git panel and branch indicator showing stale state after
heavy file-system activity caused the file watcher to lose events
jolutz pushed a commit to jolutz/zed that referenced this pull request Aug 8, 2026
…-industries#59976)

Related to zed-industries#59610.

# Objective

When the filesystem watcher loses sync (common on macOS under heavy
churn, e.g. a dev build constantly rewriting `target/` and `.git/`), it
forces a recursive rescan of the worktree root. That rescan removes the
whole subtree from the snapshot before re-scanning it, and
`remove_path_from_snapshot` was unconditionally pruning the worktree's
git repositories along with it.

Because the scanner publishes intermediate snapshots while it works, a
snapshot could be published in the window after the repository was
pruned but before `.git` was re-scanned. The `GitStore` would see the
repository as removed, tear it down, and then re-create it with a fresh
`RepositoryId` once it reappeared — churning the id on every watcher
overflow and spamming `RepositoryUpdated` events to all consumers (git
panel, git graph, blame, etc.).

Here some debug logs, 
**See** that the **repoId** inside the git graph event is different and
is **8** now, even though I only have one repository and didn't do any
git related updates.

```
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for Native; scheduling rescans for 11 registrations
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-06-26T18:38:44+02:00 INFO  [git::repository] opening git repository at "/Users/remcosmits/Documents/code/zed/.git" using git binary "/opt/homebrew/bin/git"
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:44+02:00 ERROR [crates/zed/src/main.rs:1991] Is a directory (os error 21)
2026-06-26T18:38:44+02:00 INFO  [project::prettier_store] Prettier config file ".prettierrc" changed, reloading prettier instances for worktree 1
2026-06-26T18:38:45+02:00 INFO  [project::prettier_store] Prettier config file ".prettierrc" changed, reloading prettier instances for worktree 1
2026-06-26T18:38:45+02:00 INFO  [git::repository] opening git repository at "/Users/remcosmits/Documents/code/zed/.git" using git binary "/opt/homebrew/bin/git"
2026-06-26T18:38:45+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled
2026-06-26T18:38:45+02:00 ERROR [crates/git_ui/src/git_panel.rs:3892] oneshot canceled


[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
[crates/git_ui/src/git_graph.rs:1468:21] "other repo id" = "other repo id"
[crates/git_ui/src/git_graph.rs:1468:21] &updated_repo_id = RepositoryId(
    8,
)
[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
[crates/git_ui/src/git_graph.rs:1468:21] "other repo id" = "other repo id"
[crates/git_ui/src/git_graph.rs:1468:21] &updated_repo_id = RepositoryId(
    8,
)
[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
[crates/git_ui/src/git_graph.rs:1468:21] "other repo id" = "other repo id"
[crates/git_ui/src/git_graph.rs:1468:21] &updated_repo_id = RepositoryId(
    8,
)
[crates/git_ui/src/git_graph.rs:1468:21] &this.repo_id = RepositoryId(
    1,
)
```

## Solution

This change makes `remove_path_from_snapshot` prune git repositories
only when the path was genuinely removed (`metadata == Ok(None)`), not
during a recursive refresh where the subtree is about to be re-scanned.
Stale repositories are still reaped authoritatively against the
filesystem in `update_git_repositories`.

## Testing

It seems to be that I'm the only that can reproduce this issue
@Anthony-Eid tried reproducing this but couldn't.
**Note**: that I dindn't write a regression test since this is a async
timing issue that I couldn't figure out to write a test for.

Steps how I can reproduce this:
1. Run `cargo run`
2. Open `Zed` as only project
3. Open the git graph
4. Open a rust file e.g. `git_graph.rs`
5. CTRL-C inside the terminal to kill Zed
6. Run `cargo run` see that you have git graph open and a rust file,
note that cargo check is doing it's thing.
7. See within a few seconds that Zed's UI flickers (especially the
project panel & nav bar with the selected repo & branch)

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

**Note**: This PR only fixes that we don't send as many git store
events, so this results in not having to update the nav bar where the
selected repo & branch on every event (that is duplicated in a way).
Result of that we now also fixed a rare case that the git graph commit
entries where gone, this was because we incremented the **repoId** and
the graph couldn't find the repo anymore with the id that it has stored
on the gitGraph struct itself.

**Before** 

See that the nav bar with selected repo & branch flickers and the git
graph commits are empty.


https://github.com/user-attachments/assets/0957be7f-e732-4fd0-a950-29c496b6407d

See that the git panel entries flicker and we have an temporary empty
panel.


https://github.com/user-attachments/assets/3b70e45c-c720-4009-aefa-696986d731b4

**After** 

See that the nav bar with selected repo & branch does not flicker
anymore and that the git graph keeps showing the commits.


https://github.com/user-attachments/assets/6e8a74f6-4a8e-4b28-ad30-574c2f33c6d9

See that the git panel does not show an empy state and does not flicker
anymore.


https://github.com/user-attachments/assets/1df7aacf-b337-4b81-b195-e67d8554d9da

---

cc @Anthony-Eid Since we where debugging this yesterday.

Release Notes:

- Fixed git repositories being repeatedly torn down and re-created when
the filesystem watcher forced a rescan, which caused redundant git
status refreshes and UI churn.
jolutz pushed a commit to jolutz/zed that referenced this pull request Aug 8, 2026
…ed-industries#61541)

# Objective

When the OS file watcher loses sync (e.g. its event queue overflows
under heavy fs churn), it drops pending events and reports a single
`Rescan` event for the watched root. Git changes hidden behind such a
rescan were silently lost, leaving the git panel and branch indicator
stale until something else touched `.git`.

Contributes to zed-industries#13176. May fix zed-industries#60102, though that report predates
zed-industries#60660 and may already be addressed by it on nightly (see Related PRs
below).

Two bugs caused this, and they masked each other:

1. **A rescan never triggered a git reload.** The `Rescan` event's path
is the worktree root, not something inside `.git`, so it never populated
`dot_git_abs_paths` in `process_events` — and since `.git` is excluded
from entry scanning, the rescan produced no `.git` events either. The
dropped git changes were simply never picked up.
2. **Re-scanning reset `git_dir_scan_id` to 0.** The snapshot diff
detects git changes by comparing scan ids, so re-inserting the
repository with a fresh id could wipe out a bump made earlier in the
same scan cycle, swallowing the corresponding `UpdatedGitRepositories`
signal.

The masking is why these fixes land together rather than as two PRs: the
root-rescan case in `test_dot_git_dir_event_does_not_suppress_children`
only passed on `main` because the buggy scan-id reset made the snapshot
diff fire spuriously. Fixing either bug alone turns that (currently
green) test red.

## Solution

- `process_events`: a `Rescan` event now schedules a git state reload
for every repository whose git directory (`dot_git`, `common_dir`, or
`repository_dir`) lies under the rescanned path, covering linked
worktrees and gitfile repositories, including git dirs watched outside
the worktree root.
- `insert_git_repository_for_path`: carry the existing `git_dir_scan_id`
forward when re-inserting a repository instead of resetting it to 0.
Deliberately *not* bumped either: re-insertion is snapshot bookkeeping,
not evidence of a git change — bumping would trigger spurious full
reloads on non-lossy paths (explicit refreshes, path-prefix scans). Only
`update_git_repositories` claims that git state changed.
- `changed_repos`: a `debug_assert` enforcing that `git_dir_scan_id`
never regresses, so future violations of this invariant fail loudly in
tests instead of manifesting as a stale git panel.

## Testing

New fault-injection infrastructure and tests:

- `FakeFs::simulate_watcher_overflow` models the kernel's watch queue
overflowing: buffered (undelivered) events are discarded and replaced by
a single `Rescan` for the given root, mirroring FSEvents
`kFSEventStreamEventFlagMustScanSubDirs`, inotify `IN_Q_OVERFLOW`, and
Windows `ERROR_NOTIFY_ENUM_DIR`.
- `test_watcher_overflow_rescan_reloads_git_state`: a git change whose
events are lost to an overflow must still be picked up via the rescan
(reproduces bug 1; fails on `main`).
- `test_git_update_in_same_batch_as_rescan_is_not_lost`: a git event
processed in the same batch as a rescan must not lose its scan-id bump
to the repository re-insertion (reproduces bug 2; fails on `main`).
- `test_random_git_updates_with_watcher_overflows`: randomized property
test (100 iterations) asserting that every git state change is
eventually signaled via `UpdatedGitRepositories` under random event
batching, delays, and overflows. Fails on `main` within the first few
seeds.
- `test_random_worktree_changes` now also injects watcher overflows,
extending its existing convergence property to rescan reconciliation of
worktree entries (this already passed; the injection guards it going
forward).

Full `worktree` and `fs` suites pass. Verified the directed tests
exercise the intended code paths via trace logging (the same-batch test
hits `update_git_repositories` stamping followed by re-insertion,
distinct from the overflow test where no `.git` event arrives at all).

## Related PRs

The recent stale-git-state reports trace back to three distinct
mechanisms that share one symptom. This PR addresses the third:

- **Events never generated** — zed-industries#60660 (merged, in this PR's base):
Linux's non-recursive watcher missed nested `refs/` directories, so
external commits/fetches produced no events at all. That PR (together
with zed-industries#60590, which explicitly rescans after Zed-initiated reset/fetch
and touches only `git_store.rs`) fixed zed-industries#60348. No overlap with this PR;
a merge against current `main` is clean, and the refs-watching tests are
disjoint from the overflow/rescan tests added here.
- **Events coalesced** — zed-industries#59876 (open, complementary): FSEvents can
merge `.git` child events into a bare `.git` `Changed` event; the signal
arrives, in a shape Zed ignores. @RemcoSmitsDev's review comment there
describes this PR's failure mode and calls the two fixes complementary;
this is effectively the follow-up promised in that comment. Both PRs
touch the same region of `process_events`, so whichever lands second
needs a trivial rebase, and zed-industries#59876 flips the bare-`.git` expectation in
`test_dot_git_dir_event_does_not_suppress_children` Case 2, which this
PR preserves.
- **Events dropped** — this PR: the watcher generated events but lost
them to a queue overflow, and the resulting `Rescan` did not reach the
git reload path. zed-industries#59976 and zed-industries#60098 (merged) reduced how often this
happens; this PR makes git state recover correctly when it does.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed the git panel and branch indicator showing stale state after
heavy file-system activity caused the file watcher to lose events
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement community champion Issues filed by our amazing community champions! 🫶

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants