fix: survive an unreadable directory while scanning the dev directory [patch] - #428
Merged
Merged
Conversation
… [patch] ScanDevDirectoryForOwnersAndRepos handed the whole tree to the recursive form of Directory.EnumerateDirectories, which leaves IgnoreInaccessible off and enumerates lazily. A single permission-denied folder anywhere under the dev directory therefore threw part way through the walk, on the ImGui render thread, and took the application with it -- and a dev directory holds exactly the package caches, build output and IDE metadata that produce such a folder. Replace it with EnumerateGitDirectories, which lists one level at a time and skips a directory that refuses to be read, so a refusal costs only that subtree rather than every repository that would have been found after it. The walk also stops at a .git directory, whose contents are git's own storage and hold no further working trees. The lister is injectable because a test process running as root -- which CI containers routinely do -- bypasses the permission bits, so chmod alone would pass against the unfixed code. The on-disk test is kept as well and goes inconclusive where the host cannot deny a read. Fixes #425 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
Brings in #429 and #430, both of which changed ProjectDirector.cs, so the combination is built and tested here rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
|
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.



Fixes #425
What was wrong
ScanDevDirectoryForOwnersAndReposhanded the whole tree to the recursive form ofDirectory.EnumerateDirectories:That overload builds its
EnumerationOptionswithIgnoreInaccessibleoff, and it enumerates lazily. So one permission-denied folder anywhere under the dev directory throws part way through the walk — on the ImGui render thread, with nothing between it and the top — and takes the whole application with it. The existingtry/catch (NotSupportedException)only wrapsGitRepository.Createfor repos already found, so it never sees the enumeration.A dev directory holds package caches, build output and IDE metadata. A folder the process cannot read is ordinary there, not exotic.
The fix
EnumerateGitDirectorieslists one level at a time and skips a directory that refuses to be listed, so a refusal costs only that subtree instead of every repository that would have been found after it.UnauthorizedAccessExceptioncovers the denied read;IOExceptioncovers a directory removed mid-walk, a dead junction, and an unreadable volume.The walk also stops at a
.gitdirectory rather than descending into it — its contents are git's own storage and hold no further working trees, and skipping them saves walking the largest directory in every repo.Name matching is
OrdinalIgnoreCase, which is what the previous pattern match did on Windows, where this application primarily runs.Tests
ProjectDirector.Test/DevDirectoryScanTests.cs, following the pattern CLAUDE.md sets out forDecidePullandDescribePendingChanges— the part with a rule in it is a plain method, drivable without a live ImGui context.The lister is an injectable parameter for one specific reason: a test process running as root bypasses the permission bits entirely, so a
chmod-based test quietly produces a readable directory and passes against the unfixed code. That is exactly what happens in a CI container. Injecting the refusal makes the regression deterministic everywhere. The real-filesystem test is kept alongside it and goesAssert.Inconclusivewhere the host cannot actually deny a read — the same conventionGitCliTestsuses for a missing git-lfs.Verified by reverting the fix and re-running:
ADeniedDirectoryCostsOnlyItsOwnSubtree,ADirectoryThatVanishesMidWalkIsSkippedandAMissingRootYieldsNothingRatherThanThrowingall fail without it and pass with it.Full suite on this branch: 46 total, 43 passed, 0 failed, 3 inconclusive (2 need git-lfs, 1 needs a non-root user).
dotnet build --configuration Releaseis clean.Noted, not changed
SearchOption.AllDirectoriesfollows directory symlinks and loops forever on a cycle — I confirmed this on .NET 10 with a probe. The new walk behaves the same way, so this is neither introduced nor fixed here; it is a separate defect from the one #425 describes and would widen this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
Generated by Claude Code