From 5e969a2b3e450853ef8fca11c200e5c002d7fdd3 Mon Sep 17 00:00:00 2001 From: amadzarak Date: Sun, 26 Apr 2026 13:45:05 -0400 Subject: [PATCH] OPC # 0006: OPC Git Trunk-Based management Co-authored-by: Copilot --- ControlPlane.Api/Services/PromotionService.cs | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/ControlPlane.Api/Services/PromotionService.cs b/ControlPlane.Api/Services/PromotionService.cs index 812acee..a65bf50 100644 --- a/ControlPlane.Api/Services/PromotionService.cs +++ b/ControlPlane.Api/Services/PromotionService.cs @@ -52,6 +52,38 @@ public class PromotionService(IConfiguration config, ILogger l private static Signature MakeSig() => new("OPC Control Plane", "opc@clarity.internal", DateTimeOffset.UtcNow); + // ── Remote URL (config-driven, never reads .git/config URL) ────────────── + + /// + /// Builds the HTTPS remote URL for a named repo entirely from Gitea config. + /// The local clone's .git/config remote URL is irrelevant — this is the authority. + /// + private string GetRemoteUrl(string repoName) + { + var baseUrl = (config["Gitea:BaseUrl"] + ?? throw new InvalidOperationException("Gitea:BaseUrl is not configured.")).TrimEnd('/'); + var owner = config[$"Gitea:Repos:{repoName}:Owner"] ?? config["Gitea:Owner"] + ?? throw new InvalidOperationException($"Gitea owner not configured for '{repoName}'."); + var repoSlug = config[$"Gitea:Repos:{repoName}:Repo"] ?? repoName; + return $"{baseUrl}/{owner}/{repoSlug}.git"; + } + + /// + /// Returns the 'origin' remote after normalising its URL to the config-driven HTTPS URL. + /// If the clone was checked out with SSH (e.g. on a dev machine), this corrects it silently + /// so that LibGit2Sharp — which has no SSH support — always uses HTTPS. + /// + private Remote EnsureRemote(Repository repo, string repoName) + { + var url = GetRemoteUrl(repoName); + var remote = repo.Network.Remotes["origin"]; + if (remote is null) + return repo.Network.Remotes.Add("origin", url); + if (remote.Url != url) + repo.Network.Remotes.Update("origin", r => r.Url = url); + return repo.Network.Remotes["origin"]!; + } + // ── Branch status ──────────────────────────────────────────────────────── /// @@ -72,12 +104,9 @@ public class PromotionService(IConfiguration config, ILogger l // Fetch to get up-to-date remote refs; swallow network errors so status still works offline. try { - var remote = repo.Network.Remotes["origin"]; - if (remote is not null) - { - var refSpecs = remote.FetchRefSpecs.Select(r => r.Specification).ToList(); - repo.Network.Fetch(remote.Name, refSpecs, MakeFetchOptions()); - } + var remote = EnsureRemote(repo, repoName); + var refSpecs = remote.FetchRefSpecs.Select(r => r.Specification).ToList(); + repo.Network.Fetch(remote.Name, refSpecs, MakeFetchOptions()); } catch (Exception ex) { @@ -217,8 +246,7 @@ public class PromotionService(IConfiguration config, ILogger l // 1. Fetch latest remote state for all branches Log(" Fetching origin..."); - var remote = repo.Network.Remotes["origin"] - ?? throw new InvalidOperationException("No 'origin' remote configured."); + var remote = EnsureRemote(repo, repoName); var refSpecs = remote.FetchRefSpecs.Select(r => r.Specification).ToList(); repo.Network.Fetch(remote.Name, refSpecs, MakeFetchOptions()); @@ -332,8 +360,7 @@ public class PromotionService(IConfiguration config, ILogger l try { - var remote = repo.Network.Remotes["origin"] - ?? throw new InvalidOperationException("No 'origin' remote."); + var remote = EnsureRemote(repo, repoName); // Force push — "+" prefix overrides remote reflog repo.Network.Push(remote, $"+refs/heads/{branchName}:refs/heads/{branchName}", MakePushOptions()); } @@ -424,8 +451,7 @@ public class PromotionService(IConfiguration config, ILogger l // 1. Fetch Log(" Fetching origin..."); - var remote = repo.Network.Remotes["origin"] - ?? throw new InvalidOperationException("No 'origin' remote configured."); + var remote = EnsureRemote(repo, repoName); var refSpecs = remote.FetchRefSpecs.Select(r => r.Specification).ToList(); repo.Network.Fetch(remote.Name, refSpecs, MakeFetchOptions()); @@ -553,12 +579,9 @@ public class PromotionService(IConfiguration config, ILogger l // Fetch latest remote refs — swallow network errors so status still works offline. try { - var remote = repo.Network.Remotes["origin"]; - if (remote is not null) - { - var refSpecs = remote.FetchRefSpecs.Select(r => r.Specification).ToList(); - repo.Network.Fetch(remote.Name, refSpecs, MakeFetchOptions()); - } + var remote = EnsureRemote(repo, repoName); + var refSpecs = remote.FetchRefSpecs.Select(r => r.Specification).ToList(); + repo.Network.Fetch(remote.Name, refSpecs, MakeFetchOptions()); } catch (Exception ex) { @@ -672,8 +695,7 @@ public class PromotionService(IConfiguration config, ILogger l repo.Refs.Add($"refs/heads/{branchName}", commit.Sha); - var remote = repo.Network.Remotes["origin"] - ?? throw new InvalidOperationException("No 'origin' remote configured."); + var remote = EnsureRemote(repo, repoName); repo.Network.Push(remote, $"refs/heads/{branchName}:refs/heads/{branchName}", MakePushOptions());