OPC # 0001: Extract OPC into standalone repo

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
amadzarak
2026-04-25 19:17:48 -04:00
parent 7561ac7530
commit 76962a6af4
6 changed files with 326 additions and 65 deletions
+81 -27
View File
@@ -14,37 +14,65 @@ public static class GitEndpoints
return app;
}
// GET /api/git/log?grep=OPC+%23+0001&limit=50
// GET /api/git/log?grep=OPC+%23+0001&limit=50&repo=all
// repo can be "all" (default) or a named key from Git:Repos (e.g. "Clarity", "OPC", "Gateway").
// All responses include a repoKey field so the caller knows which repo each commit came from.
private static IResult GetLog(
IConfiguration config,
string? grep = null,
int limit = 50)
int limit = 50,
string repo = "all")
{
var repoPath = ResolveRepo(config);
if (repoPath is null)
return Results.Problem("Could not locate a git repository. Set Git:RepoRoot in appsettings.");
var repos = repo == "all"
? ResolveAllRepos(config)
: ResolveNamedRepo(config, repo) is { } p
? new Dictionary<string, string> { [repo] = p }
: null;
using var repo = new Repository(repoPath);
if (repos is null || repos.Count == 0)
return Results.Problem("Could not locate any git repositories. Set Git:Repos in appsettings.");
var tips = repo.Branches
.Where(b => b.Tip != null)
.Select(b => (GitObject)b.Tip)
.ToList();
var bucket = new List<(DateTimeOffset When, string RepoKey, GitCommit Commit)>();
var filter = new CommitFilter
foreach (var (repoKey, repoPath) in repos)
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time,
IncludeReachableFrom = tips.Count > 0 ? tips : (object)repo.Head,
};
if (!Directory.Exists(repoPath)) continue;
IEnumerable<Commit> query = repo.Commits.QueryBy(filter);
using var r = new Repository(repoPath);
if (!string.IsNullOrWhiteSpace(grep))
query = query.Where(c => c.Message.Contains(grep, StringComparison.OrdinalIgnoreCase));
var tips = r.Branches
.Where(b => b.Tip != null)
.Select(b => (GitObject)b.Tip)
.ToList();
var commits = query
var filter = new CommitFilter
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time,
IncludeReachableFrom = tips.Count > 0 ? tips : (object)r.Head,
};
IEnumerable<Commit> query = r.Commits.QueryBy(filter);
if (!string.IsNullOrWhiteSpace(grep))
query = query.Where(c => c.Message.Contains(grep, StringComparison.OrdinalIgnoreCase));
foreach (var c in query.Take(limit))
bucket.Add((c.Author.When, repoKey, ToGitCommit(r, c)));
}
var commits = bucket
.OrderByDescending(x => x.When)
.Take(limit)
.Select(c => ToGitCommit(repo, c))
.Select(x => new
{
repoKey = x.RepoKey,
hash = x.Commit.Hash,
shortHash = x.Commit.ShortHash,
author = x.Commit.Author,
date = x.Commit.Date,
subject = x.Commit.Subject,
files = x.Commit.Files,
})
.ToList();
return Results.Ok(commits);
@@ -114,36 +142,39 @@ public static class GitEndpoints
return Results.Ok(branches);
}
// GET /api/git/branch-coverage?commits=hash1,hash2,hash3
// Returns each local branch and whether it contains ALL of the given commits.
private static IResult GetBranchCoverage(IConfiguration config, string? commits = null)
// GET /api/git/branch-coverage?commits=hash1,hash2,hash3&repo=OPC
// repo defaults to the single configured repo. Pass a named key from Git:Repos to
// query a specific repo — the backend silently ignores hashes it cannot find.
private static IResult GetBranchCoverage(IConfiguration config, string? commits = null, string? repo = null)
{
if (string.IsNullOrWhiteSpace(commits)) return Results.Ok(Array.Empty<object>());
var hashes = commits.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (hashes.Length == 0) return Results.Ok(Array.Empty<object>());
var repoPath = ResolveRepo(config);
var repoPath = repo is not null
? ResolveNamedRepo(config, repo) ?? ResolveRepo(config)
: ResolveRepo(config);
if (repoPath is null)
return Results.Problem("Could not locate a git repository.");
using var repo = new Repository(repoPath);
using var r = new Repository(repoPath);
var targetCommits = hashes
.Select(h => repo.Lookup<Commit>(h))
.Select(h => r.Lookup<Commit>(h))
.Where(c => c is not null)
.ToList();
if (targetCommits.Count == 0) return Results.Ok(Array.Empty<object>());
var result = repo.Branches
var result = r.Branches
.Where(b => !b.IsRemote && b.Tip != null)
.Select(b =>
{
var contains = targetCommits.All(tc =>
{
// If merge base of branch tip and target == target, then target is an ancestor
var mergeBase = repo.ObjectDatabase.FindMergeBase(b.Tip, tc!);
var mergeBase = r.ObjectDatabase.FindMergeBase(b.Tip, tc!);
return mergeBase?.Sha == tc!.Sha;
});
return new
@@ -162,6 +193,29 @@ public static class GitEndpoints
// ── Helpers ───────────────────────────────────────────────────────────────
/// Resolves a named repo from the Git:Repos registry.
private static string? ResolveNamedRepo(IConfiguration config, string repoKey)
{
var path = config[$"Git:Repos:{repoKey}"];
return !string.IsNullOrWhiteSpace(path) && Directory.Exists(path) ? path : null;
}
/// Returns all repos from the Git:Repos registry that exist on disk.
/// Falls back to the single configured repo if the registry is empty.
private static IReadOnlyDictionary<string, string> ResolveAllRepos(IConfiguration config)
{
var result = new Dictionary<string, string>();
foreach (var child in config.GetSection("Git:Repos").GetChildren())
{
var path = child.Value;
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
result[child.Key] = path;
}
if (result.Count == 0 && ResolveRepo(config) is { } single)
result["default"] = single;
return result;
}
/// Resolves the repo root: explicit config overrides, otherwise auto-discover
/// from the running assembly directory upward via LibGit2Sharp.
private static string? ResolveRepo(IConfiguration config)
+9 -4
View File
@@ -10,12 +10,17 @@
"ApiKey": "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"
},
"Git": {
"RepoRoot": "C:\\Users\\amadzarak\\source\\repos\\Clarity"
"RepoRoot": "C:\\Users\\amadzarak\\source\\repos\\ClarityStack\\OPC",
"Repos": {
"Clarity": "C:\\Users\\amadzarak\\source\\repos\\ClarityStack\\Clarity",
"OPC": "C:\\Users\\amadzarak\\source\\repos\\ClarityStack\\OPC",
"Gateway": "C:\\Users\\amadzarak\\source\\repos\\ClarityStack\\gateway"
}
},
"Gitea": {
"BaseUrl": "https://opc.clarity.test",
"Owner": "Clarity",
"Repo": "Clarity",
"Token": "2ef325f682915c5959bf6a0dc73cec7034fcd2a2"
"Owner": "ClarityStack",
"Repo": "OPC",
"Token": "fcf9f66415754fb639a8343e3904e06b1d78c646"
}
}