Skip to content

Skip linked git worktrees nested in the project - #111

Merged
JesseHerrick merged 7 commits into
remoteoss:mainfrom
onnimonni:skip-nested-worktrees
Sep 29, 2026
Merged

JesseHerrick merged 7 commits into
remoteoss:mainfrom
onnimonni:skip-nested-worktrees

Conversation

@onnimonni

@onnimonni onnimonni commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #109.

Skips linked git worktrees nested below the project root (e.g. .claude/worktrees/<name>), with no config needed.

What counts as nested: a directory whose .git file links a worktree, or one that git still records in <common dir>/worktrees/. The record covers git worktree remove, which deletes the .git file before the rest of the checkout. Submodules and Mix git deps stay indexed.

Changes

  • The walker, the parallel collector and the sweep skip nested worktrees, and an existing index drops their files. The check uses directory entries already read, and the records cost about 0.2 ms per walk.
  • Single-file updates skip paths in a nested worktree.
  • The fsnotify and FSEvents watchers track worktree tops: they drop events from inside one, and handle a worktree that is added, moved, removed, or becomes a plain directory.
  • FindProjectRoot stops at a linked worktree, so dexter started inside one uses its own index.
  • The HEAD poller follows a .git file, so branch switches are seen when the root is a linked worktree or a submodule.

🤖 Generated with Claude Code

A linked worktree below the root (e.g. .claude/worktrees/) is a full repo
copy; indexing it duplicated every definition. Walkers, the watcher and
single-file updates skip dirs whose .git file points into .git/worktrees/.
Submodules and Mix git deps stay indexed. FindProjectRoot no longer climbs
above a linked worktree to the enclosing checkout's database.

Fixes remoteoss#109
Comment thread beam_server.exs Outdated
JesseHerrick and others added 6 commits September 28, 2026 17:49
Detection
- Detect linked worktrees by git's commondir marker instead of matching
  "/.git/worktrees/" in the gitdir path. This finds worktrees of bare
  repositories, and a submodule at a path named worktrees/ no longer
  counts. When git has pruned the admin directory, its place under a
  worktrees/ directory is the fallback.
- Add GitDir, which resolves a .git file to the git directory it names.

Index
- The incremental sweep removed only stored files that no longer exist
  on disk, so an index built before worktrees were skipped kept its
  duplicates. It now also removes stored files inside a nested worktree,
  checking only the paths the sweep did not see, once per directory.
- Find indexed files below a deleted directory with a range query on the
  path index instead of reading every stored path.

Watchers and runtime
- A mix.exs or mix.lock change in a nested worktree no longer reindexes
  the enclosing project.
- fsnotify finds worktrees from the directory entries it already reads,
  watches only a worktree's top for its .git file, drops events from it,
  and removes the watches below a directory when its .git file appears.
  The watcher walk no longer opens a .git file per directory or sorts
  entries.
- FSEvents knows worktree tops from git's records and from new .git
  files, and drops their events with map lookups, so adding or removing
  a worktree no longer reconciles the whole workspace.
- Files indexed before a directory became a worktree (cp -r can copy
  them before .git) are removed. A top whose .git file goes away while
  the directory stays is indexed as a plain directory after the retry
  interval, which lets git worktree remove finish first.

HEAD pollers
- Both pollers resolve the git directory with GitDir, so branch switches
  are seen when the project root is a linked worktree or a submodule.

Also remove the stray root copy of internal/lsp/beam_server.exs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The only caller is the darwin watcher, so golangci-lint on Linux
reported the function as unused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- git worktree remove deletes a worktree's .git file before the rest of
  the checkout. A full reconcile in between, which the mass delete
  triggers by overflowing the watcher, indexed the remaining files. The
  walkers, the sweep and both watchers now also treat a directory that
  git records in <common dir>/worktrees/ as a worktree, until git
  worktree prune. Reading the records costs about 0.2 ms per walk.
- git worktree add writes the .git file before commondir, so the fsnotify
  watcher could miss a new worktree and watch its whole checkout. The
  admin directory's gitdir backlink now identifies the worktree, and a
  Write to .git is checked as well as its Create.
- The FSEvents watcher now knows a worktree renamed within the project
  under its new name.
- CollectElixirFilesParallel yields a root that is a file, as
  WalkElixirFiles does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- fsnotify: drop events from below a known top, not only from the top,
  so queued events from a tree that just became a worktree add no
  watches.
- Both watchers check a top that git still records again after each
  retry interval, so it is indexed once git worktree prune drops the
  record instead of staying skipped until a restart.
- unwatchBelow reads the watch list in memory instead of walking the
  checkout on the event loop.
- FSEvents: drop the ancestor scan on every directory event; the known
  tops and the check of the directory itself cover it.
- A submodule at worktrees/<name> with a missing git directory is no
  longer taken for a pruned worktree.
- RemoveFilesUnderRoot compares cleaned stored paths again, as editor
  URIs are stored as sent.
- Share ReadDirUnsorted and NestedWorktreeSet from the parser.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A known top whose path comes back as a plain directory, for example
  after a deleted worktree's record was pruned, is checked again and
  indexed instead of being skipped until a restart.
- A directory that is removed or moved takes the known tops below it
  along.
- Darwin test callbacks never block their timer goroutine.
- The CHANGELOG notes that on macOS a worktree of another repository
  that exists at startup can still trigger a workspace sweep.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A stale top only lives in a running watcher's memory and needs a
deleted, pruned worktree replaced by a plain directory at the same
path. That is too rare to pay for on every remove and rename event.
Also drop the CHANGELOG note about macOS sweeps, which describes
unchanged behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JesseHerrick
JesseHerrick merged commit c4b62b7 into remoteoss:main Sep 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ignore nested git worktrees (.claude/worktrees/ style)

2 participants