Forge
csharp3407750f
1using AgentForge.Abstractions;
2using AgentForge.Plugin.Sdk;
3using AgentForge.Contracts;
4using AgentForge.Data;
5using AgentForge.Models;
6using AgentForge.Services;
7
8namespace AgentForge.Endpoints;
9
10internal 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
View only · write via MCP/CIDE