204 lines
7.3 KiB
C#
204 lines
7.3 KiB
C#
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
|
|
);
|
|
}
|
|
}
|