OPC # 0001: Extract OPC into standalone repo
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user