Files
ClaudeDo/src/ClaudeDo.Ui/Services/RepoScanner.cs
T
mika kuns 3348736e9d fix(ui): warn and block save when a list's working dir isn't a git repo
Reuses RepoScanner.IsRepo (now public) to validate WorkingDir whenever it
changes in ListSettingsModalViewModel — Browse or typing. A missing folder
or a non-repo folder shows an inline warning and gates Save via the same
CanExecute pattern the load-failure gate already uses; an empty WorkingDir
is untouched. Previously this only surfaced as a Failed task deep in
WorktreeManager on first run.

Also localizes the two hardcoded EN strings in the same file (delete
confirm, planning-sessions block) into vm.listSettings.* keys.
2026-09-10 13:32:52 +02:00

74 lines
2.4 KiB
C#

namespace ClaudeDo.Ui.Services;
public sealed record RepoCandidate(string Name, string FullPath);
public static class RepoScanner
{
private const int MaxDepth = 5;
private static readonly HashSet<string> SkipDirNames = new(StringComparer.OrdinalIgnoreCase)
{
"node_modules", "bin", "obj", ".git", ".vs", "packages",
};
public static IReadOnlyList<RepoCandidate> Scan(string parentFolder)
{
if (string.IsNullOrWhiteSpace(parentFolder) || !Directory.Exists(parentFolder))
return Array.Empty<RepoCandidate>();
try
{
var info = new DirectoryInfo(parentFolder);
if (info.Attributes.HasFlag(FileAttributes.ReparsePoint))
return Array.Empty<RepoCandidate>();
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{ return Array.Empty<RepoCandidate>(); }
var result = new List<RepoCandidate>();
ScanDirectory(parentFolder, depth: 0, result);
return result;
}
private static void ScanDirectory(string dir, int depth, List<RepoCandidate> result)
{
if (IsRepo(dir))
{
result.Add(new RepoCandidate(Path.GetFileName(dir), dir));
return;
}
if (depth >= MaxDepth)
return;
IEnumerable<string> subdirs;
try { subdirs = Directory.EnumerateDirectories(dir); }
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{ return; }
foreach (var subdir in subdirs)
{
var name = Path.GetFileName(subdir);
if (SkipDirNames.Contains(name))
continue;
try
{
var info = new DirectoryInfo(subdir);
if (info.Attributes.HasFlag(FileAttributes.ReparsePoint))
continue;
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{ continue; }
try { ScanDirectory(subdir, depth + 1, result); }
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{ }
}
}
// A real clone has .git as a directory. Worktrees and submodules have it as a file
// ("gitdir: ...") — those are not standalone repos and would flood the list.
public static bool IsRepo(string dir) => Directory.Exists(Path.Combine(dir, ".git"));
}