Forge
csharp3407750f
1using AgentForge.Plugin.Sdk;
2using AgentForge.Abstractions;
3using AgentForge.Contracts;
4using AgentForge.Data;
5using AgentForge.Models;
6using AgentForge.Services;
7
8namespace AgentForge.Plugin.MergeRequest.Endpoints;
9
10internal static class ForgeMergeRequestEndpoints
11{
12 internal static RouteGroupBuilder MapMergeRequestEndpoints(this RouteGroupBuilder api)
13 {
14 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/merge-requests", ListMergeRequests);
15 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/merge-requests", CreateMergeRequest);
16 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}", GetMergeRequest);
17 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}/ci", GetCiStatus);
18 api.MapGet($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}/diff", GetMergeRequestDiff);
19 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}/comments", AddMergeRequestComment);
20 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}/accept", AcceptMergeRequest);
21 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}/reject", RejectMergeRequest);
22 api.MapPost($"/repos/{ForgeRepoRouting.Pattern}/merge-requests/{{number:int}}/handoff", HandoffMergeRequest);
23 return api;
24 }
25
26 private static IResult ListMergeRequests(
27 string name,
28 HttpContext http,
29 ForgeRepository repository,
30 ForgeUrls urls)
31 {
32 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
33 var mergeRequests = repository.ListMergeRequests(repo.Id)
34 .Select(mr => ForgePluginResponses.ToMergeRequestResponse(mr, repo.Name, urls))
35 .ToList();
36 return Results.Ok(new MergeRequestListResponse(mergeRequests.Count, mergeRequests));
37 }
38
39 private static async Task<IResult> CreateMergeRequest(
40 string name,
41 CreateMergeRequestRequest request,
42 HttpContext context,
43 ForgeRepository repository,
44 ForgeUrls urls,
45 CiWebhookService ci,
46 CancellationToken cancellationToken)
47 {
48 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
49 var title = request.Title?.Trim() ?? string.Empty;
50 var sourceBranch = request.SourceBranch?.Trim() ?? string.Empty;
51 var targetBranch = string.IsNullOrWhiteSpace(request.TargetBranch) ? "main" : request.TargetBranch.Trim();
52
53 if (title.Length == 0)
54 throw new ForgeApiException(400, "Merge request title is required.");
55 if (sourceBranch.Length == 0)
56 throw new ForgeApiException(400, "sourceBranch is required.");
57
58 var createdBy = ForgeActorResolver.ResolveActor(context, request.Author);
59 var mergeRequest = repository.InsertMergeRequest(
60 repo.Id,
61 title,
62 sourceBranch,
63 targetBranch,
64 request.Anchors,
65 createdBy);
66 var response = ForgePluginResponses.ToMergeRequestResponse(mergeRequest, repo.Name, urls);
67 await ci.EmitIopAsync(
68 ForgeIopEvent.ForMr("mr.opened", repo.Name, mergeRequest.Number, response, createdBy, mergeRequest.Anchors),
69 cancellationToken).ConfigureAwait(false);
70 return Results.Created(
71 $"/api/v1/repos/{name}/merge-requests/{mergeRequest.Number}",
72 response);
73 }
74
75 private static IResult GetMergeRequest(
76 string name,
77 int number,
78 HttpContext http,
79 ForgeRepository repository,
80 ForgeUrls urls)
81 {
82 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
83 var mergeRequest = repository.GetMergeRequestByNumber(repo.Id, number)
84 ?? throw new ForgeApiException(404, $"Merge request #{number} not found.");
85 var comments = repository.ListMergeRequestComments(mergeRequest.Id)
86 .Select(ForgePluginResponses.ToCommentResponse)
87 .ToList();
88 return Results.Ok(ForgePluginResponses.ToMergeRequestDetailResponse(mergeRequest, repo.Name, urls, comments));
89 }
90
91 private static async Task<IResult> AddMergeRequestComment(
92 string name,
93 int number,
94 AddMergeRequestCommentRequest request,
95 HttpContext context,
96 ForgeRepository repository,
97 CiWebhookService ci,
98 CancellationToken cancellationToken)
99 {
100 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
101 var body = request.Body?.Trim() ?? string.Empty;
102 if (body.Length == 0)
103 throw new ForgeApiException(400, "Comment body is required.");
104
105 var author = ForgeActorResolver.ResolveCommentAuthor(context, request.Author);
106 var comment = repository.InsertMergeRequestComment(repo.Id, number, body, author);
107 var response = ForgePluginResponses.ToCommentResponse(comment);
108 await ci.EmitIopAsync(
109 ForgeIopEvent.ForMr("mr.comment", repo.Name, number, response, author),
110 cancellationToken).ConfigureAwait(false);
111 return Results.Created(
112 $"/api/v1/repos/{name}/merge-requests/{number}/comments/{comment.Id}",
113 response);
114 }
115
116 private static IResult GetCiStatus(
117 string name,
118 int number,
119 HttpContext http,
120 ForgeRepository repository)
121 {
122 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
123 _ = repository.GetMergeRequestByNumber(repo.Id, number)
124 ?? throw new ForgeApiException(404, $"Merge request #{number} not found.");
125
126 return Results.Ok(ForgePluginResponses.ToCiStatusResponse(repository.GetLatestCiRun(repo.Id, number)));
127 }
128
129 private static IResult GetMergeRequestDiff(
130 string name,
131 int number,
132 HttpContext http,
133 ForgeRepository repository,
134 MergeRequestService mergeRequests)
135 {
136 var repo = ForgePluginEndpoints.RequireRepo(repository, name, http);
137 return Results.Ok(mergeRequests.GetDiff(repo, number));
138 }
139
140 private static async Task<IResult> AcceptMergeRequest(
141 string name,
142 int number,
143 MergeRequestActionRequest? request,
144 HttpContext context,
145 ForgeRepository repository,
146 MergeRequestService mergeRequests,
147 CancellationToken cancellationToken)
148 {
149 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
150 var response = await mergeRequests.AcceptAsync(
151 repo,
152 number,
153 ForgePluginHttpAuth.GetApiToken(context),
154 request?.Comment,
155 cancellationToken).ConfigureAwait(false);
156 return Results.Ok(response);
157 }
158
159 private static async Task<IResult> RejectMergeRequest(
160 string name,
161 int number,
162 MergeRequestActionRequest? request,
163 HttpContext context,
164 ForgeRepository repository,
165 MergeRequestService mergeRequests,
166 CancellationToken cancellationToken)
167 {
168 var repo = ForgePluginEndpoints.RequireRepo(repository, name, context);
169 var response = await mergeRequests.RejectAsync(
170 repo,
171 number,
172 ForgePluginHttpAuth.GetApiToken(context),
173 request?.Comment,
174 cancellationToken).ConfigureAwait(false);
175 return Results.Ok(response);
176 }
177
178 private static async Task<IResult> HandoffMergeRequest(
179 string name,
180 int number,
181 HandoffMergeRequestRequest request,
182 HttpContext context,
183 ForgeRepository repository,
184 MergeRequestService mergeRequests,
185 ForgeUrls urls,
186 CiWebhookService ci,
187 CancellationToken cancellationToken)
188 {
189 var sourceRepo = ForgePluginEndpoints.RequireRepo(repository, name, context);
190 var targetName = request.TargetRepo?.Trim() ?? string.Empty;
191 if (targetName.Length == 0)
192 throw new ForgeApiException(400, "targetRepo is required.");
193
194 var targetRepo = ForgePluginEndpoints.RequireRepo(repository, targetName, context);
195
196 var createdBy = ForgeActorResolver.ResolveActor(context, requestedAuthor: null);
197 var response = mergeRequests.HandoffToRepo(sourceRepo, number, targetRepo, createdBy, urls);
198 await ci.EmitIopAsync(
199 ForgeIopEvent.ForMr(
200 "mr.handed_off",
201 targetRepo.Name,
202 response.Number,
203 new { source = new { repo = sourceRepo.Name, number }, mergeRequest = response },
204 createdBy),
205 cancellationToken).ConfigureAwait(false);
206 return Results.Created(
207 $"/api/v1/repos/{targetRepo.Name}/merge-requests/{response.Number}",
208 response);
209 }
210}
211
View only · write via MCP/CIDE