You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Workspace repo discovery follows directory symlinks: a symlink loop lists the same repo dozens of times, and repo fetch then runs that many concurrent fetches into it #232
DiscoverGitReposRecursive (KtsuTools.Repo/RepoService.cs:1144-1168) recurses into every entry Directory.GetDirectories returns. That includes directory symlinks and junctions (reparse points). It never skips them and never de-duplicates by real path, so:
A symlink that points at a repo, or at a directory above one, lists that repo again under a second path.
A symlink that points back up the tree (tools/loop -> ..) is followed repeatedly until the OS symlink-depth limit throws. Every level on the way adds the same repos again.
The callers include TryDiscoverWorkspaceRepos (line ~700, used by ktools git and repo lfs install), PullAllAsync (~507), FetchAllAsync and WalkAndCacheAsync. SaveCacheAsync de-duplicates by path string only, so every alias reaches the cache as a separate repo.
Reproduction
Take a workspace with ws/A/.git and ws/tools/loop -> ... A copy of the function returns 41 entries, every one of them repo A, reached as ws/tools/loop/A, ws/tools/loop/tools/loop/A, and so on.
With that workspace:
ktools git status prints A 41 times.
repo pull runs git pull --autostash in A 41 times.
repo fetch is parallel by default, so it runs 41 concurrent fetches into the same repository. They contend for ref locks, and the ones that lose report failures.
Suggested fix
Skip reparse points while walking. For example, use Directory.EnumerateDirectories(directory, "*", new EnumerationOptions { AttributesToSkip = FileAttributes.ReparsePoint, IgnoreInaccessible = true }), or check new DirectoryInfo(subDir).LinkTarget is not null before recursing.
Priority: Medium. It needs a symlink or junction in the workspace. Once one is there, every command that discovers workspace repos repeats work, and repo fetch runs concurrent fetches that fight over ref locks in the same repository.
Area / suggested assignment:KtsuTools.Repo/RepoService.cs (DiscoverGitReposRecursive L1144-1168). Callers: TryDiscoverWorkspaceRepos (~L700), PullAllAsync, FetchAllAsync, WalkAndCacheAsync, and the path-string de-duplication in SaveCacheAsync.
Activity
matt-edmondson commentedon Sep 30, 2026
Triage
repo fetchruns concurrent fetches that fight over ref locks in the same repository.KtsuTools.Repo/RepoService.cs(DiscoverGitReposRecursiveL1144-1168). Callers:TryDiscoverWorkspaceRepos(~L700),PullAllAsync,FetchAllAsync,WalkAndCacheAsync, and the path-string de-duplication inSaveCacheAsync.repo build/update-packagesreport "No solution files found" (exit 0) and crashessync#230 (an unreadable subdirectory stops theAllDirectoriesscans) andsynccopies files into repos reached through a symlinked path but never lists or commits them (macOS /tmp, /var affected) #188 (a symlinked workspace root insync) are neighbours, not duplicates.EnumerationOptions { AttributesToSkip = ReparsePoint, IgnoreInaccessible = true }fixes this issue and also covers the unreadable-directory case in One unreadable subdirectory makesrepo build/update-packagesreport "No solution files found" (exit 0) and crashessync#230 for this walker, so consider landing the two together. Resolve the root path once first, so a symlinked workspace root (synccopies files into repos reached through a symlinked path but never lists or commits them (macOS /tmp, /var affected) #188) still works.Generated by Claude Code