feat(ui): repo scan discovers nested repos in subfolders

RepoScanner.Scan now recurses into subdirectories (max depth 5) instead
of only checking the immediate children of the chosen folder. A found
repo (.git as dir or file) is added without descending further; the
selected folder itself is checked too. Skips node_modules/bin/obj/.git/
.vs/packages and reparse points (junctions/symlinks). Per-directory
IOException/UnauthorizedAccessException no longer aborts the whole scan.
This commit is contained in:
mika kuns
2026-08-06 13:14:56 +02:00
parent 0d1e3b9a6f
commit 7affb4c204
3 changed files with 124 additions and 12 deletions
+72 -1
View File
@@ -57,7 +57,7 @@ public sealed class RepoScannerTests : IDisposable
}
[Fact]
public void Scan_IsNotRecursive()
public void Scan_FindsRepoAtDepthTwo()
{
var nested = MakeDir(Path.Combine("outer", "inner"));
Directory.CreateDirectory(Path.Combine(nested, ".git"));
@@ -65,6 +65,47 @@ public sealed class RepoScannerTests : IDisposable
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("inner", result[0].Name);
Assert.Equal(nested, result[0].FullPath);
}
[Fact]
public void Scan_FindsRepoAtDepthThree()
{
var nested = MakeDir(Path.Combine("customers", "projX", "repo"));
Directory.CreateDirectory(Path.Combine(nested, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo", result[0].Name);
Assert.Equal(nested, result[0].FullPath);
}
[Fact]
public void Scan_DoesNotDescendBelowAFoundRepo()
{
var repo = MakeDir("repo-a");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var submodule = Path.Combine(repo, "vendor", "nested-repo");
Directory.CreateDirectory(submodule);
Directory.CreateDirectory(Path.Combine(submodule, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo-a", result[0].Name);
}
[Fact]
public void Scan_SkipsNodeModules()
{
var nodeModules = MakeDir(Path.Combine("node_modules", "some-package"));
Directory.CreateDirectory(Path.Combine(nodeModules, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Empty(result);
}
@@ -75,4 +116,34 @@ public sealed class RepoScannerTests : IDisposable
Assert.Empty(result);
}
[Fact]
#pragma warning disable CA1416 // ACL manipulation is Windows-only; this test targets the Windows dev/CI environment
public void Scan_ContinuesAfterUnreadableSubdirectory()
{
var repo = MakeDir("repo-a");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var locked = new DirectoryInfo(MakeDir("locked"));
var denyRule = new System.Security.AccessControl.FileSystemAccessRule(
Environment.UserName,
System.Security.AccessControl.FileSystemRights.ListDirectory,
System.Security.AccessControl.AccessControlType.Deny);
var acl = locked.GetAccessControl();
acl.AddAccessRule(denyRule);
try
{
locked.SetAccessControl(acl);
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo-a", result[0].Name);
}
finally
{
acl.RemoveAccessRule(denyRule);
locked.SetAccessControl(acl);
}
}
#pragma warning restore CA1416
}