Git-Status Endpoint mit Outgoing-Commits und Dashboard-Badges

- GET /api/sessions/{id}/git-status: Parsed git status --porcelain in add/edit/delete Listen
- Outgoing-Commits: Zählt unpushed Commits via git rev-list --count @{u}..HEAD
- Dashboard: Zeigt Git-Badges asynchron pro Session (+N, ~N, -N, ↑N)
- CSS: Farbige Badges (grün=add, orange=edit, rot=delete, blau=outgoing)
This commit is contained in:
Meleki committed 2026-02-28 23:23:52 +01:00
1 parent 166465fd54
commit ac150af4c5
4 files changed
+195

No files matched your search

+129
View File
@@ -23,6 +23,7 @@ public static class SessionEndpoint
app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt);
app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming);
app.MapPost("/api/sessions/{sessionId}/exec", HandleExec);
app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus);
}
private static async Task HandleList(HttpContext ctx, DatabaseService db)
@@ -264,5 +265,133 @@ public static class SessionEndpoint
}
}
private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db)
{
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var session = await db.GetSession(sessionId);
if (session == null)
{
ctx.Response.StatusCode = 404;
return;
}
var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : null;
if (string.IsNullOrEmpty(workingDir))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { success = false, error = "Kein Workspace konfiguriert" });
return;
}
try
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = "status --porcelain",
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proc = Process.Start(psi);
if (proc == null)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { success = false, error = "Git-Prozess konnte nicht gestartet werden" });
return;
}
var stdout = await proc.StandardOutput.ReadToEndAsync();
var stderr = await proc.StandardError.ReadToEndAsync();
await proc.WaitForExitAsync();
if (proc.ExitCode != 0)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { success = false, error = stderr.Trim() });
return;
}
var add = new List<string>();
var edit = new List<string>();
var delete = new List<string>();
var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Length < 4) continue;
var x = line[0]; // index status
var y = line[1]; // working tree status
var file = line.Substring(3);
// Bei Renames: "R old -> new" => nur den neuen Namen nehmen
var arrowIdx = file.IndexOf(" -> ", StringComparison.Ordinal);
if (arrowIdx >= 0)
file = file.Substring(arrowIdx + 4);
if (x == '?' && y == '?')
{
add.Add(file);
}
else if (x == 'D' || y == 'D')
{
delete.Add(file);
}
else if (x == 'A')
{
add.Add(file);
}
else
{
edit.Add(file);
}
}
// Outgoing commits zählen (nur wenn Remote existiert)
int outgoing = 0;
try
{
var outPsi = new ProcessStartInfo
{
FileName = "git",
Arguments = "rev-list --count @{u}..HEAD",
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var outProc = Process.Start(outPsi);
if (outProc != null)
{
var outResult = (await outProc.StandardOutput.ReadToEndAsync()).Trim();
await outProc.WaitForExitAsync();
if (outProc.ExitCode == 0 && int.TryParse(outResult, out var count))
outgoing = count;
}
}
catch
{
// Kein Remote oder kein Upstream – outgoing bleibt 0
}
await ctx.Response.WriteAsJsonAsync(new { success = true, add, edit, delete, outgoing });
}
catch (Exception ex)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { success = false, error = ex.Message });
}
}
}