OPC # 0001: Extract OPC into standalone repo

This commit is contained in:
amadzarak
2026-04-25 17:26:42 -04:00
commit 42383bdc03
170 changed files with 21365 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
using ControlPlane.Core.Models;
using LibGit2Sharp;
namespace ControlPlane.Api.Endpoints;
public static class GitEndpoints
{
public static IEndpointRouteBuilder MapGitEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/api/git/log", GetLog);
app.MapGet("/api/git/commits/{hash}", GetCommit);
app.MapGet("/api/git/branches", GetBranches);
app.MapGet("/api/git/branch-coverage", GetBranchCoverage);
return app;
}
// GET /api/git/log?grep=OPC+%23+0001&limit=50
private static IResult GetLog(
IConfiguration config,
string? grep = null,
int limit = 50)
{
var repoPath = ResolveRepo(config);
if (repoPath is null)
return Results.Problem("Could not locate a git repository. Set Git:RepoRoot in appsettings.");
using var repo = new Repository(repoPath);
var tips = repo.Branches
.Where(b => b.Tip != null)
.Select(b => (GitObject)b.Tip)
.ToList();
var filter = new CommitFilter
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time,
IncludeReachableFrom = tips.Count > 0 ? tips : (object)repo.Head,
};
IEnumerable<Commit> query = repo.Commits.QueryBy(filter);
if (!string.IsNullOrWhiteSpace(grep))
query = query.Where(c => c.Message.Contains(grep, StringComparison.OrdinalIgnoreCase));
var commits = query
.Take(limit)
.Select(c => ToGitCommit(repo, c))
.ToList();
return Results.Ok(commits);
}
// GET /api/git/commits/{hash}
private static IResult GetCommit(string hash, IConfiguration config)
{
var repoPath = ResolveRepo(config);
if (repoPath is null)
return Results.Problem("Could not locate a git repository.");
using var repo = new Repository(repoPath);
var commit = repo.Lookup<Commit>(hash);
if (commit is null) return Results.NotFound();
var parentTree = commit.Parents.FirstOrDefault()?.Tree;
var changes = repo.Diff.Compare<TreeChanges>(parentTree, commit.Tree);
var patch = repo.Diff.Compare<Patch>(parentTree, commit.Tree);
var files = changes.Select(c => new
{
path = c.Path,
oldPath = c.OldPath,
status = c.Status.ToString(),
additions = patch[c.Path]?.LinesAdded ?? 0,
deletions = patch[c.Path]?.LinesDeleted ?? 0,
patch = patch[c.Path]?.Patch ?? string.Empty,
}).ToList();
return Results.Ok(new
{
hash = commit.Sha,
shortHash = commit.Sha[..7],
author = commit.Author.Name,
email = commit.Author.Email,
date = commit.Author.When.ToString("yyyy-MM-dd HH:mm:ss zzz"),
subject = commit.MessageShort,
body = commit.Message,
files,
});
}
// GET /api/git/branches
private static IResult GetBranches(IConfiguration config)
{
var repoPath = ResolveRepo(config);
if (repoPath is null)
return Results.Problem("Could not locate a git repository.");
using var repo = new Repository(repoPath);
var branches = repo.Branches
.Where(b => !b.IsRemote && b.Tip != null)
.Select(b => new
{
name = b.FriendlyName,
hash = b.Tip.Sha,
shortHash = b.Tip.Sha[..7],
subject = b.Tip.MessageShort,
author = b.Tip.Author.Name,
date = b.Tip.Author.When.ToString("yyyy-MM-dd HH:mm:ss zzz"),
isHead = b.IsCurrentRepositoryHead,
})
.OrderBy(b => b.name)
.ToList();
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)
{
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);
if (repoPath is null)
return Results.Problem("Could not locate a git repository.");
using var repo = new Repository(repoPath);
var targetCommits = hashes
.Select(h => repo.Lookup<Commit>(h))
.Where(c => c is not null)
.ToList();
if (targetCommits.Count == 0) return Results.Ok(Array.Empty<object>());
var result = repo.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!);
return mergeBase?.Sha == tc!.Sha;
});
return new
{
branch = b.FriendlyName,
contains,
tipHash = b.Tip.Sha[..7],
isHead = b.IsCurrentRepositoryHead,
};
})
.OrderBy(b => b.branch)
.ToList();
return Results.Ok(result);
}
// ── Helpers ───────────────────────────────────────────────────────────────
/// Resolves the repo root: explicit config overrides, otherwise auto-discover
/// from the running assembly directory upward via LibGit2Sharp.
private static string? ResolveRepo(IConfiguration config)
{
var configured = config["Git:RepoRoot"] ?? config["Docker:RepoRoot"];
if (!string.IsNullOrWhiteSpace(configured) && Directory.Exists(configured))
return configured;
// Auto-discover: walk up from the app's own directory
var startPath = AppContext.BaseDirectory;
var discovered = Repository.Discover(startPath);
if (discovered is null) return null;
// Repository.Discover returns the .git directory path; get the working dir
using var probe = new Repository(discovered);
return probe.Info.WorkingDirectory;
}
private static GitCommit ToGitCommit(Repository repo, Commit c)
{
string[] files;
try
{
var parentTree = c.Parents.FirstOrDefault()?.Tree;
var changes = repo.Diff.Compare<TreeChanges>(parentTree, c.Tree);
files = changes.Select(ch => ch.Path).ToArray();
}
catch { files = []; }
return new GitCommit(
Hash: c.Sha,
ShortHash: c.Sha[..7],
Author: c.Author.Name,
Date: c.Author.When.ToString("yyyy-MM-dd HH:mm:ss zzz"),
Subject: c.MessageShort,
Files: files
);
}
}