65a6b4afaf
Co-authored-by: Copilot <copilot@github.com>
261 lines
11 KiB
C#
261 lines
11 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using ControlPlane.Core.Models;
|
|
|
|
namespace ControlPlane.Api.Services;
|
|
|
|
/// <summary>
|
|
/// Thin wrapper around the Gitea REST API v1.
|
|
/// Configured via Gitea__BaseUrl, Gitea__Owner, and Gitea__Token in appsettings.
|
|
/// </summary>
|
|
public class GiteaService
|
|
{
|
|
private readonly HttpClient _http;
|
|
private readonly string _defaultOwner;
|
|
private readonly string _defaultRepo;
|
|
private readonly IConfiguration _cfg;
|
|
private readonly ILogger<GiteaService> _log;
|
|
|
|
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
|
|
|
|
public GiteaService(IHttpClientFactory factory, IConfiguration cfg, ILogger<GiteaService> log)
|
|
{
|
|
_log = log;
|
|
_cfg = cfg;
|
|
_defaultOwner = cfg["Gitea:Owner"] ?? "ClarityStack";
|
|
_defaultRepo = cfg["Gitea:Repo"] ?? "OPC";
|
|
|
|
var baseUrl = cfg["Gitea:BaseUrl"] ?? "https://opc.clarity.test";
|
|
var token = cfg["Gitea:Token"] ?? string.Empty;
|
|
|
|
_http = factory.CreateClient("gitea");
|
|
_http.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/api/v1/");
|
|
_http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
|
|
if (!string.IsNullOrWhiteSpace(token))
|
|
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token);
|
|
}
|
|
|
|
// ── Repo resolution ───────────────────────────────────────────────────────
|
|
|
|
/// Returns (owner, repo) for the given key, falling back to the defaults.
|
|
private (string Owner, string Repo) ResolveOwnerRepo(string? repoKey = null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(repoKey))
|
|
{
|
|
var owner = _cfg[$"Gitea:Repos:{repoKey}:Owner"];
|
|
var repo = _cfg[$"Gitea:Repos:{repoKey}:Repo"];
|
|
if (!string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repo))
|
|
return (owner, repo);
|
|
}
|
|
return (_defaultOwner, _defaultRepo);
|
|
}
|
|
|
|
// ── Repos ─────────────────────────────────────────────────────────────────
|
|
|
|
public async Task<GiteaRepo?> GetRepoAsync(string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<GiteaRepo>($"repos/{owner}/{repo}", JsonOpts, ct);
|
|
}
|
|
catch (Exception ex) { _log.LogWarning(ex, "Gitea GetRepo failed"); return null; }
|
|
}
|
|
|
|
// ── Branches ──────────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<GiteaBranch>> ListBranchesAsync(string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<List<GiteaBranch>>(
|
|
$"repos/{owner}/{repo}/branches?limit=50", JsonOpts, ct) ?? [];
|
|
}
|
|
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListBranches failed"); return []; }
|
|
}
|
|
|
|
/// Returns branches from all registered repos, tagged with repoKey.
|
|
public async Task<List<(string RepoKey, GiteaBranch Branch)>> ListAllBranchesAsync(CancellationToken ct = default)
|
|
{
|
|
var result = new List<(string, GiteaBranch)>();
|
|
var section = _cfg.GetSection("Gitea:Repos");
|
|
var keys = section.GetChildren().Select(c => c.Key).ToList();
|
|
if (keys.Count == 0) keys = ["default"];
|
|
|
|
await Task.WhenAll(keys.Select(async key =>
|
|
{
|
|
var branches = await ListBranchesAsync(key == "default" ? null : key, ct);
|
|
lock (result) result.AddRange(branches.Select(b => (key, b)));
|
|
}));
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<GiteaBranch?> CreateBranchAsync(CreateBranchRequest req, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(req.RepoKey);
|
|
// Slugify: "OPC # 0032" + title → "feature/OPC-0032-git-workflow-integration"
|
|
var slug = SlugifyTitle(req.OpcTitle);
|
|
var num = req.OpcNumber.Replace("OPC # ", "OPC-").Replace(" ", "");
|
|
var branchName = $"feature/{num}-{slug}";
|
|
|
|
var body = JsonSerializer.Serialize(new
|
|
{
|
|
new_branch_name = branchName,
|
|
old_branch_name = req.From,
|
|
}, JsonOpts);
|
|
|
|
var res = await _http.PostAsync(
|
|
$"repos/{owner}/{repo}/branches",
|
|
new StringContent(body, Encoding.UTF8, "application/json"), ct);
|
|
|
|
if (!res.IsSuccessStatusCode)
|
|
{
|
|
var err = await res.Content.ReadAsStringAsync(ct);
|
|
_log.LogWarning("Gitea CreateBranch failed {Status}: {Error}", res.StatusCode, err);
|
|
return null;
|
|
}
|
|
|
|
return await res.Content.ReadFromJsonAsync<GiteaBranch>(JsonOpts, ct);
|
|
}
|
|
|
|
// ── Pull Requests ─────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<GiteaPullRequest>> ListPullRequestsAsync(
|
|
string state = "open", string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<List<GiteaPullRequest>>(
|
|
$"repos/{owner}/{repo}/pulls?state={state}&limit=50", JsonOpts, ct) ?? [];
|
|
}
|
|
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListPRs failed"); return []; }
|
|
}
|
|
|
|
public async Task<GiteaPullRequest?> GetPullRequestAsync(long number, string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<GiteaPullRequest>(
|
|
$"repos/{owner}/{repo}/pulls/{number}", JsonOpts, ct);
|
|
}
|
|
catch (Exception ex) { _log.LogWarning(ex, "Gitea GetPR failed"); return null; }
|
|
}
|
|
|
|
public async Task<GiteaPullRequest?> CreatePullRequestAsync(
|
|
CreatePullRequestRequest req, string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
var body = JsonSerializer.Serialize(new
|
|
{
|
|
title = req.Title,
|
|
head = req.Head,
|
|
@base = req.Base,
|
|
body = req.Body,
|
|
}, JsonOpts);
|
|
|
|
var res = await _http.PostAsync(
|
|
$"repos/{owner}/{repo}/pulls",
|
|
new StringContent(body, Encoding.UTF8, "application/json"), ct);
|
|
|
|
if (!res.IsSuccessStatusCode)
|
|
{
|
|
var err = await res.Content.ReadAsStringAsync(ct);
|
|
_log.LogWarning("Gitea CreatePR failed {Status}: {Error}", res.StatusCode, err);
|
|
return null;
|
|
}
|
|
|
|
return await res.Content.ReadFromJsonAsync<GiteaPullRequest>(JsonOpts, ct);
|
|
}
|
|
|
|
// ── Tags ──────────────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<GiteaTag>> ListTagsAsync(string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<List<GiteaTag>>(
|
|
$"repos/{owner}/{repo}/tags?limit=20", JsonOpts, ct) ?? [];
|
|
}
|
|
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListTags failed"); return []; }
|
|
}
|
|
|
|
public async Task<GiteaTag?> CreateTagAsync(CreateTagRequest req, string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
var body = JsonSerializer.Serialize(new
|
|
{
|
|
tag_name = req.TagName,
|
|
message = req.Message,
|
|
target = req.CommitSha,
|
|
}, JsonOpts);
|
|
|
|
var res = await _http.PostAsync(
|
|
$"repos/{owner}/{repo}/tags",
|
|
new StringContent(body, Encoding.UTF8, "application/json"), ct);
|
|
|
|
if (!res.IsSuccessStatusCode)
|
|
{
|
|
var err = await res.Content.ReadAsStringAsync(ct);
|
|
_log.LogWarning("Gitea CreateTag failed {Status}: {Error}", res.StatusCode, err);
|
|
return null;
|
|
}
|
|
|
|
return await res.Content.ReadFromJsonAsync<GiteaTag>(JsonOpts, ct);
|
|
}
|
|
|
|
// ── Webhooks ──────────────────────────────────────────────────────────────
|
|
|
|
public async Task<List<GiteaWebhook>> ListWebhooksAsync(string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
try
|
|
{
|
|
return await _http.GetFromJsonAsync<List<GiteaWebhook>>(
|
|
$"repos/{owner}/{repo}/hooks", JsonOpts, ct) ?? [];
|
|
}
|
|
catch (Exception ex) { _log.LogWarning(ex, "Gitea ListWebhooks failed"); return []; }
|
|
}
|
|
|
|
public async Task<GiteaWebhook?> RegisterWebhookAsync(
|
|
CreateWebhookRequest req, string? repoKey = null, CancellationToken ct = default)
|
|
{
|
|
var (owner, repo) = ResolveOwnerRepo(repoKey);
|
|
var body = JsonSerializer.Serialize(new
|
|
{
|
|
type = "gitea",
|
|
active = true,
|
|
config = new { url = req.TargetUrl, content_type = "json" },
|
|
events = req.Events,
|
|
}, JsonOpts);
|
|
|
|
var res = await _http.PostAsync(
|
|
$"repos/{owner}/{repo}/hooks",
|
|
new StringContent(body, Encoding.UTF8, "application/json"), ct);
|
|
|
|
if (!res.IsSuccessStatusCode)
|
|
{
|
|
var err = await res.Content.ReadAsStringAsync(ct);
|
|
_log.LogWarning("Gitea RegisterWebhook failed {Status}: {Error}", res.StatusCode, err);
|
|
return null;
|
|
}
|
|
|
|
return await res.Content.ReadFromJsonAsync<GiteaWebhook>(JsonOpts, ct);
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
private static string SlugifyTitle(string title) =>
|
|
System.Text.RegularExpressions.Regex
|
|
.Replace(title.ToLowerInvariant(), @"[^a-z0-9]+", "-")
|
|
.Trim('-')[..Math.Min(40, title.Length)];
|
|
}
|