| 1 | using AgentForge.Abstractions; |
| 2 | using AgentForge.Plugin.Sdk; |
| 3 | using AgentForge.Contracts; |
| 4 | using AgentForge.Data; |
| 5 | using AgentForge.Models; |
| 6 | using AgentForge.Services; |
| 7 | |
| 8 | namespace AgentForge.Endpoints; |
| 9 | |
| 10 | internal static class ForgeRepoEndpoints |
| 11 | { |
| 12 | internal static RouteGroupBuilder MapRepoEndpoints(this RouteGroupBuilder api) |
| 13 | { |
| 14 | api.MapGet("/repos", ListRepos); |
| 15 | api.MapPost("/repos", CreateRepo); |
| 16 | api.MapPatch($"/repos/{ForgeRepoRouting.Pattern}", UpdateRepoCatalog); |
| 17 | api.MapGet($"/repos/{ForgeRepoRouting.Pattern}", GetRepo); |
| 18 | return api; |
| 19 | } |
| 20 | |
| 21 | private static IResult ListRepos( |
| 22 | ForgeRepository repository, |
| 23 | GitRepoService git, |
| 24 | ForgeRepoAccessService access, |
| 25 | HttpContext http, |
| 26 | string? group, |
| 27 | string? org) |
| 28 | { |
| 29 | var principal = access.Resolve(http); |
| 30 | var repos = access.FilterReadable(repository.ListRepos(group, org), principal) |
| 31 | .Select(repo => ForgePluginResponses.ToRepoResponse(repo, git, repository)) |
| 32 | .ToList(); |
| 33 | return Results.Ok(new RepoListResponse(repos.Count, repos)); |
| 34 | } |
| 35 | |
| 36 | private static async Task<IResult> CreateRepo( |
| 37 | CreateRepoRequest request, |
| 38 | ForgeRepository repository, |
| 39 | GitRepoService git, |
| 40 | CiWebhookService ci, |
| 41 | ForgeRepoAccessService access, |
| 42 | HttpContext http, |
| 43 | CancellationToken cancellationToken) |
| 44 | { |
| 45 | if (!SlugValidator.IsValid(request.Name)) |
| 46 | throw new ForgeApiException(400, "Invalid repo name. Use lowercase slug: a-z, 0-9, hyphens."); |
| 47 | if (request.DriveMode is not null && !DriveModeExtensions.TryParseSlug(request.DriveMode, out _)) |
| 48 | throw new ForgeApiException(400, "driveMode must be human-driven or agent-driven."); |
| 49 | |
| 50 | var principal = access.Resolve(http); |
| 51 | string? orgId = null; |
| 52 | string? orgSlug = null; |
| 53 | RepoVisibility visibility = RepoVisibility.Private; |
| 54 | |
| 55 | if (!string.IsNullOrWhiteSpace(request.Org)) |
| 56 | { |
| 57 | if (!SlugValidator.IsValid(request.Org)) |
| 58 | throw new ForgeApiException(400, "Invalid org slug."); |
| 59 | var org = repository.GetOrgBySlug(request.Org) |
| 60 | ?? throw new ForgeApiException(404, $"Organization '{request.Org}' not found."); |
| 61 | orgId = org.Id; |
| 62 | orgSlug = org.Slug; |
| 63 | visibility = org.DefaultVisibility; |
| 64 | access.EnsureOrgCatalogAdmin(org.Id, principal); |
| 65 | } |
| 66 | |
| 67 | string? groupId = null; |
| 68 | if (!string.IsNullOrWhiteSpace(request.Group)) |
| 69 | { |
| 70 | if (!ForgeRepoGroupPathValidator.IsValidPath(request.Group)) |
| 71 | throw new ForgeApiException(400, "Invalid group path."); |
| 72 | if (orgId is null) |
| 73 | throw new ForgeApiException(400, "group requires org."); |
| 74 | var group = repository.GetRepoGroupByOrgAndPath(orgId, request.Group) |
| 75 | ?? throw new ForgeApiException(404, $"Repository group '{request.Group}' not found."); |
| 76 | groupId = group.Id; |
| 77 | if (group.DefaultVisibility is not null) |
| 78 | visibility = group.DefaultVisibility.Value; |
| 79 | } |
| 80 | |
| 81 | if (!string.IsNullOrWhiteSpace(request.Visibility)) |
| 82 | { |
| 83 | if (!RepoVisibilityExtensions.TryParseSlug(request.Visibility, out var parsed)) |
| 84 | throw new ForgeApiException(400, "visibility must be public or private."); |
| 85 | visibility = parsed; |
| 86 | } |
| 87 | |
| 88 | var fullName = ForgeRepoPath.BuildOrgRepoName(orgSlug, request.Name); |
| 89 | if (!ForgeRepoPath.IsValidFullName(fullName)) |
| 90 | throw new ForgeApiException(400, "Invalid repository path."); |
| 91 | if (repository.RepoNameExists(fullName)) |
| 92 | throw new ForgeApiException(409, $"Repository '{fullName}' already exists."); |
| 93 | |
| 94 | var driveMode = DriveModeExtensions.ParseSlugOrDefault(request.DriveMode); |
| 95 | git.CreateBareRepository(fullName); |
| 96 | try |
| 97 | { |
| 98 | var entity = repository.InsertRepo( |
| 99 | fullName, |
| 100 | request.Description?.Trim() ?? string.Empty, |
| 101 | driveMode, |
| 102 | groupId, |
| 103 | orgId, |
| 104 | visibility); |
| 105 | var response = ForgePluginResponses.ToRepoResponse(entity, git, repository); |
| 106 | await ci.EmitIopAsync(ForgeIopEvent.ForRepo("repo.created", fullName, response), cancellationToken) |
| 107 | .ConfigureAwait(false); |
| 108 | return Results.Created($"/api/v1/repos/{fullName}", response); |
| 109 | } |
| 110 | catch |
| 111 | { |
| 112 | var path = git.GetBareRepoPath(fullName); |
| 113 | if (Directory.Exists(path)) |
| 114 | Directory.Delete(path, recursive: true); |
| 115 | throw; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | private static IResult GetRepo( |
| 120 | string name, |
| 121 | ForgeRepository repository, |
| 122 | GitRepoService git, |
| 123 | HttpContext http) |
| 124 | { |
| 125 | var repoName = ForgeRepoNames.NormalizeRoute(name); |
| 126 | var repo = ForgePluginEndpoints.RequireRepo(repository, repoName, http); |
| 127 | return Results.Ok(ForgePluginResponses.ToRepoResponse(repo, git, repository)); |
| 128 | } |
| 129 | |
| 130 | private static IResult UpdateRepoCatalog( |
| 131 | string name, |
| 132 | UpdateRepoCatalogRequest request, |
| 133 | ForgeRepository repository, |
| 134 | GitRepoService git, |
| 135 | ForgeRepoAccessService access, |
| 136 | HttpContext http) |
| 137 | { |
| 138 | if (string.IsNullOrWhiteSpace(request.Org)) |
| 139 | throw new ForgeApiException(400, "org is required when updating catalog group."); |
| 140 | if (!SlugValidator.IsValid(request.Org)) |
| 141 | throw new ForgeApiException(400, "Invalid org slug."); |
| 142 | var org = repository.GetOrgBySlug(request.Org) |
| 143 | ?? throw new ForgeApiException(404, $"Organization '{request.Org}' not found."); |
| 144 | if (!string.IsNullOrWhiteSpace(request.Group) && !ForgeRepoGroupPathValidator.IsValidPath(request.Group)) |
| 145 | throw new ForgeApiException(400, "Invalid group path."); |
| 146 | |
| 147 | access.EnsureOrgCatalogAdmin(org.Id, access.Resolve(http)); |
| 148 | |
| 149 | var repoName = ForgeRepoNames.NormalizeRoute(name); |
| 150 | _ = ForgePluginEndpoints.RequireRepo(repository, repoName, http); |
| 151 | var entity = repository.AssignRepoToCatalogGroup(repoName, org.Id, request.Group); |
| 152 | return Results.Ok(ForgePluginResponses.ToRepoResponse(entity, git, repository)); |
| 153 | } |
| 154 | } |
| 155 | |