OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user