Skip to content

fix: survive an unreadable directory while scanning the dev directory [patch] - #428

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/425-scan-skips-unreadable-dirs
Sep 22, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/425-scan-skips-unreadable-dirs

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #425

What was wrong

ScanDevDirectoryForOwnersAndRepos handed the whole tree to the recursive form of Directory.EnumerateDirectories:

IEnumerable<string> gitDirs = Directory.EnumerateDirectories(Options.DevDirectory, ".git", SearchOption.AllDirectories);

That overload builds its EnumerationOptions with IgnoreInaccessible off, 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 existing try/catch (NotSupportedException) only wraps GitRepository.Create for 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

EnumerateGitDirectories lists 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. UnauthorizedAccessException covers the denied read; IOException covers a directory removed mid-walk, a dead junction, and an unreadable volume.

The walk also stops at a .git directory 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 for DecidePull and DescribePendingChanges — 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 goes Assert.Inconclusive where the host cannot actually deny a read — the same convention GitCliTests uses for a missing git-lfs.

Verified by reverting the fix and re-running: ADeniedDirectoryCostsOnlyItsOwnSubtree, ADirectoryThatVanishesMidWalkIsSkipped and AMissingRootYieldsNothingRatherThanThrowing all 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 Release is clean.

Noted, not changed

SearchOption.AllDirectories follows 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

… [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
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 9dd56bc into main Sep 22, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the fix/425-scan-skips-unreadable-dirs branch September 22, 2026 00:35
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.

Dev-directory scan crashes the app on any inaccessible or locked subfolder

2 participants