Repository navigation
Do not index a directory the watcher could not read or classify - #112
Merged
Merged
Conversation
walkDirectories returned silently when it could not read a directory, for example when the process had no file descriptor left. The subtree below it was then not watched, not marked failed and never retried, and the Create handler reported every file in it as if it were a plain directory, even when it was a worktree that had been moved into place. Such a directory is now marked failed: the coverage report says that it is not covered, the retry reads it again, and a retry clears the failure only when it could read it. Its files are not reported from the Create event; the reconcile that follows restored coverage indexes them. Both index walks (WalkElixirFiles and CollectElixirFilesParallel) now skip a directory whose .git file names no git directory yet, as the watchers do, so a reconcile does not index a worktree that git is still moving. This is a likely cause of a rare failure of TestFSNotifyWatcherSkipsWorktreeAddedWhileRunning under load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
git 2.48 and later rename a worktree on `git worktree move` and then write its .git file again in place (truncate, then write). The watcher's walk asked two questions with two reads: "is this a worktree?" and "is its .git file still being written?". The first read could see the truncated file and the second the complete one, so a worktree was neither, and the walk reported every file in it as a plain directory. With git 2.55 on Linux this made TestFSNotifyWatcherSkipsWorktreeAddedWhileRunning fail in 2 of 20 runs; git 2.47, which does not write the file again, never failed. parser.GitFile and parser.GitFileFromEntries now return one state (none, plain, worktree, unsettled) from one read of the file. The fsnotify walk and pending check, the FSEvents handler and its later check, both index walks, and the runtime's removal of a reported worktree use it. The cost is the same: no syscall for a directory without a .git entry, and one read for a directory with one. Tests: TestGitFileStates (each state) and TestGitFileReadsTheFileOnce (serves an empty file on the first read and a complete one on the second, as git does; one call must read once and answer "unsettled"). On Linux, the watcher test now passes 100 of 100 runs with git 2.55 and 50 of 50 with git 2.47. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1181bba. Configure here.
The Create handler skipped indexing any directory marked failed, but a directory is also marked failed when its watch cannot be added (the inotify watch limit) although it was read and is known to be plain. When the watcher was already degraded, no coverage edge followed, so such a directory was never indexed. The handler now skips only a directory that could not be read. GitFileFromEntries read any .git entry that is not a directory, while GitFile reads only a regular file. A .git symlink could then be "unsettled" in a walk and "none" in the later check. Both now read only a regular file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes four gaps in how nested worktrees and unreadable directories are handled (#111, and the worktree-move fix in #110). The last one is the cause of the flaky
TestFSNotifyWatcherSkipsWorktreeAddedWhileRunningfailures in CI.Summary
Two reads of one
.gitfile could disagree (the cause of the CI flake). git 2.48 and later rename a worktree ongit worktree moveand then write its.gitfile again in place (truncate, then write; confirmed with strace on git 2.55). The watcher's walk asked "is this a worktree?" and "is its.gitfile still being written?" with two separate reads. The first could see the truncated file and the second the complete one, so a worktree was neither, and every file in it was reported as a plain directory. git 2.47 does not write the file again, which is why it only showed up on CI's newer git.A directory that the watcher cannot read was skipped silently.
walkDirectoriesreturned whenReadDirfailed, for example when the process had no file descriptor left (the kqueue backend uses one per watched file). The subtree below it was not watched, not marked failed and never retried, so the coverage report said nothing. TheCreatehandler then reported every file in it as if it were a plain directory, also when it was a worktree that had been moved into place.The index walks did not skip a worktree that git is still moving.
git worktree moverenames the directory and then writes its.gitfile again in place. Send generated functions to the line that declared them #110 made the watchers treat a.gitfile that names no git directory yet as a pending worktree, butWalkElixirFilesandCollectElixirFilesParalleldid not, so a reconcile or a first build at that moment indexed the whole worktree.Changes
parser.GitFileandparser.GitFileFromEntriesreturn one state (none, plain, worktree, unsettled) from one read of the.gitfile. The fsnotify walk and pending check, the FSEvents handler and its later check, both index walks, and the runtime's removal of a reported worktree use it. Same cost as before: no syscall for a directory without a.gitentry, one read for a directory with one.walkDirectoriesmarks a directory it cannot read as failed. The coverage report then says that the tree is not covered, and the retry timer reads it again. A retry clears the failure only when it could read the directory.The
Createhandler does not report the files of a directory that it could not read. When coverage comes back, the runtime reconciles, and that walk indexes the files of a plain directory and skips a worktree.Both index walks skip a nested directory whose
.gitfile names no git directory yet, as the watchers do.The watcher's directory reads can be replaced in tests (
readDir), so a failed read can be simulated without timing.Validation
TestWatcherDoesNotReportDirectoryItCouldNotRead: a worktree moved into place and a plain directory, each with a failed read, a retry that still fails, and a retry that works. Nothing is reported from the unreadable directory, the coverage goes degraded and then restored, and the retry classifies the directory. With the old behavior, the test reports the moved-in worktree's files.TestWalkAndCollectSkipUnsettledGitFile: both walks skip a directory with an empty.gitfile. Without the change, both index it.TestGitFileStates(each state) andTestGitFileReadsTheFileOnce, which serves an empty file on the first read and a complete one on the second, as git does: one classification reads once and answers "unsettled".TestFSNotifyWatcherSkipsWorktreeAddedWhileRunningfailed in 2 of 20 race runs before the change. After it: 100 of 100 with git 2.55, and 50 of 50 with git 2.47. Tracing the failing run showed the walk deciding "not a worktree" while the.gitfile and git's records were valid a moment later.go test ./...,go test -raceoninternal/workspaceandinternal/parser, ten race runs of every watcher test, andgolangci-lintpass. No change to the walk for directories without a.gitentry, so the walk costs the same.🤖 Generated with Claude Code
Note
Medium Risk
Changes when directories are indexed and how nested worktrees are detected; wrong behavior could skip or duplicate index content, but scope is filesystem watching and walks only.
Overview
Fixes incorrect indexing when a git worktree is moved into the project or when the file watcher cannot read a directory (e.g. EMFILE).
Single-read
.gitclassification — Replaces separate “linked worktree?” and “unsettled.gitfile?” checks withGitFile/GitFileFromEntries, which classify a directory in one read (WorktreeGitFile,UnsettledGitFile,PlainGitFile, etc.). That closes a race on git 2.48+ where two reads could see an empty then complete.gitfile and treat a worktree as a normal folder (the CI flake). Index walks (WalkElixirFiles,CollectElixirFilesParallel), fsnotify/FSEvents, and runtime removal all use this API; unsettled (empty).gitfiles are skipped like settled worktrees.Unreadable directories —
walkDirectoriesno longer silently skips failedReadDir: it marks the path failed, reports degraded coverage, and retries later. OnCreate, files under a directory that could not be read are not indexed immediately (it might be a moved-in worktree); readable-but-unwatchable dirs are still indexed at once.CHANGELOG documents the fix; new tests cover unsettled
.git, single-read behavior, unreadable dirs, and watch-at-limit cases.Reviewed by Cursor Bugbot for commit 69703fa. Bugbot is set up for automated code reviews on this repo. Configure here.