OPC # 0006: OPC Git Trunk-Based management

This commit is contained in:
amadzarak
2026-04-26 16:31:57 -04:00
parent c78bcf3360
commit c7da1eb017
4 changed files with 414 additions and 149 deletions
+106 -30
View File
@@ -19,13 +19,19 @@ public static class GitEndpoints
// All responses include a repoKey field so the caller knows which repo each commit came from.
// GET /api/git/log?grep=...&limit=25&page=1&repo=all
// page is 1-based. Each repo contributes up to limit*page commits before merge-sort+skip/take.
// branch filters to commits reachable from the named local branch; requires a specific repo (not "all").
// Each result includes a branches[] array listing local branches that contain that commit.
private static IResult GetLog(
IConfiguration config,
string? grep = null,
int limit = 25,
int page = 1,
string repo = "all")
string? grep = null,
int limit = 25,
int page = 1,
string repo = "all",
string? branch = null)
{
if (!string.IsNullOrWhiteSpace(branch) && repo == "all")
return Results.BadRequest("'branch' filter requires a specific 'repo' parameter.");
var repos = repo == "all"
? ResolveAllRepos(config)
: ResolveNamedRepo(config, repo) is { } p
@@ -43,16 +49,30 @@ public static class GitEndpoints
using var r = new Repository(repoPath);
var tips = r.Branches
.Where(b => b.Tip != null)
.Select(b => (GitObject)b.Tip)
.ToList();
var filter = new CommitFilter
CommitFilter filter;
if (!string.IsNullOrWhiteSpace(branch))
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time,
IncludeReachableFrom = tips.Count > 0 ? tips : (object)r.Head,
};
var branchRef = r.Branches[branch];
if (branchRef?.Tip is null)
return Results.BadRequest($"Branch '{branch}' not found in repo '{repoKey}'.");
filter = new CommitFilter
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time,
IncludeReachableFrom = branchRef.Tip,
};
}
else
{
var tips = r.Branches
.Where(b => b.Tip != null)
.Select(b => (GitObject)b.Tip)
.ToList();
filter = new CommitFilter
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time,
IncludeReachableFrom = tips.Count > 0 ? tips : (object)r.Head,
};
}
IEnumerable<Commit> query = r.Commits.QueryBy(filter);
@@ -63,22 +83,42 @@ public static class GitEndpoints
bucket.Add((c.Author.When, repoKey, ToGitCommit(r, c)));
}
var commits = bucket
var pageEntries = bucket
.OrderByDescending(x => x.When)
.Skip((page - 1) * limit)
.Take(limit)
.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();
// Annotate each commit with the local branches whose tip is a descendant of that commit.
var branchMap = new Dictionary<string, List<string>>(StringComparer.Ordinal);
foreach (var grp in pageEntries.GroupBy(x => x.RepoKey))
{
if (!repos.TryGetValue(grp.Key, out var rPath) || !Directory.Exists(rPath)) continue;
using var annotRepo = new Repository(rPath);
var localBranches = annotRepo.Branches.Where(b => !b.IsRemote && b.Tip != null).ToList();
foreach (var (_, _, c) in grp)
{
var target = annotRepo.Lookup<Commit>(c.Hash);
if (target is null) { branchMap[c.Hash] = []; continue; }
branchMap[c.Hash] = localBranches
.Where(b => annotRepo.ObjectDatabase.FindMergeBase(b.Tip, target)?.Sha == target.Sha)
.Select(b => b.FriendlyName)
.ToList();
}
}
var commits = pageEntries.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,
branches = branchMap.TryGetValue(x.Commit.Hash, out var br) ? (IReadOnlyList<string>)br : Array.Empty<string>(),
}).ToList();
return Results.Ok(commits);
}
@@ -127,18 +167,55 @@ public static class GitEndpoints
return Results.NotFound();
}
// GET /api/git/branches
private static IResult GetBranches(IConfiguration config)
// GET /api/git/branches?repo=all
// repo defaults to "default" (single configured repo). Pass "all" to get branches from all
// registered repos. Each result includes repoKey so the caller can group branches by repo.
private static IResult GetBranches(IConfiguration config, string repo = "default")
{
var repoPath = ResolveRepo(config);
if (repo == "all")
{
var allRepos = ResolveAllRepos(config);
if (allRepos.Count == 0)
return Results.Problem("Could not locate any git repositories.");
var allBranches = new List<object>();
foreach (var (repoKey, rPath) in allRepos)
{
if (!Directory.Exists(rPath)) continue;
using var gitRepo = new Repository(rPath);
allBranches.AddRange(gitRepo.Branches
.Where(b => !b.IsRemote && b.Tip != null)
.OrderBy(b => b.FriendlyName)
.Select(b => (object)new
{
repoKey,
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,
}));
}
return Results.Ok(allBranches);
}
var repoPath = repo == "default"
? ResolveRepo(config)
: ResolveNamedRepo(config, repo) ?? 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
var repoLabel = repo == "default" ? "default" : repo;
using var singleRepo = new Repository(repoPath);
var branches = singleRepo.Branches
.Where(b => !b.IsRemote && b.Tip != null)
.OrderBy(b => b.FriendlyName)
.Select(b => new
{
repoKey = repoLabel,
name = b.FriendlyName,
hash = b.Tip.Sha,
shortHash = b.Tip.Sha[..7],
@@ -147,7 +224,6 @@ public static class GitEndpoints
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);