feat(ui): add RepoScanner for git repo discovery
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
25
src/ClaudeDo.Ui/Services/RepoScanner.cs
Normal file
25
src/ClaudeDo.Ui/Services/RepoScanner.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
public sealed record RepoCandidate(string Name, string FullPath);
|
||||||
|
|
||||||
|
public static class RepoScanner
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<RepoCandidate> Scan(string parentFolder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(parentFolder) || !Directory.Exists(parentFolder))
|
||||||
|
return Array.Empty<RepoCandidate>();
|
||||||
|
|
||||||
|
var result = new List<RepoCandidate>();
|
||||||
|
IEnumerable<string> subdirs;
|
||||||
|
try { subdirs = Directory.EnumerateDirectories(parentFolder); }
|
||||||
|
catch { return Array.Empty<RepoCandidate>(); }
|
||||||
|
|
||||||
|
foreach (var dir in subdirs)
|
||||||
|
{
|
||||||
|
var gitPath = Path.Combine(dir, ".git");
|
||||||
|
if (Directory.Exists(gitPath) || File.Exists(gitPath))
|
||||||
|
result.Add(new RepoCandidate(Path.GetFileName(dir), dir));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs
Normal file
78
tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests;
|
||||||
|
|
||||||
|
public sealed class RepoScannerTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _root =
|
||||||
|
Path.Combine(Path.GetTempPath(), "repo-scan-" + Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
public RepoScannerTests() => Directory.CreateDirectory(_root);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(_root, recursive: true); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MakeDir(string name)
|
||||||
|
{
|
||||||
|
var p = Path.Combine(_root, name);
|
||||||
|
Directory.CreateDirectory(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_ReturnsSubfoldersWithGitDirectory()
|
||||||
|
{
|
||||||
|
var repo = MakeDir("repo-a");
|
||||||
|
Directory.CreateDirectory(Path.Combine(repo, ".git"));
|
||||||
|
|
||||||
|
var result = RepoScanner.Scan(_root);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("repo-a", result[0].Name);
|
||||||
|
Assert.Equal(repo, result[0].FullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_TreatsDotGitFileAsRepo()
|
||||||
|
{
|
||||||
|
var repo = MakeDir("worktree-repo");
|
||||||
|
File.WriteAllText(Path.Combine(repo, ".git"), "gitdir: ../somewhere");
|
||||||
|
|
||||||
|
var result = RepoScanner.Scan(_root);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("worktree-repo", result[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_IgnoresPlainFolders()
|
||||||
|
{
|
||||||
|
MakeDir("not-a-repo");
|
||||||
|
|
||||||
|
var result = RepoScanner.Scan(_root);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_IsNotRecursive()
|
||||||
|
{
|
||||||
|
var nested = MakeDir(Path.Combine("outer", "inner"));
|
||||||
|
Directory.CreateDirectory(Path.Combine(nested, ".git"));
|
||||||
|
// outer itself has no .git
|
||||||
|
|
||||||
|
var result = RepoScanner.Scan(_root);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_ReturnsEmptyForMissingFolder()
|
||||||
|
{
|
||||||
|
var result = RepoScanner.Scan(Path.Combine(_root, "does-not-exist"));
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user