authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-04 08:53:44-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-04 08:53:44-03:00
logf9db42d14fd36e7d21f5dcbf0027f267d53c2989
tree42ca01e44283024844801701c1f599e4ba818fb5
parent253dea360ef0c743c5de8ba04f8412a7eb4694ac
signature Signed by PGP key B5690EEEBB952194

fix(server): avoid inferring forgejo conflicts from mergeability (#15441)


2 files changed, 83 insertions(+), 2 deletions(-)

apps/server/src/pullRequest/forgejoPullRequestJson.ts+2-2
...@@ -135,8 +135,8 @@ export function forgejoChangeRequest(pr: typeof ForgejoPullRequest.Type) {...@@ -135,8 +135,8 @@ export function forgejoChangeRequest(pr: typeof ForgejoPullRequest.Type) {
135 headRepositoryNameWithOwner: pr.head.repo?.full_name ?? null,135 headRepositoryNameWithOwner: pr.head.repo?.full_name ?? null,
136 state: pr.merged ? "merged" : pr.state === "closed" ? "closed" : "open",136 state: pr.merged ? "merged" : pr.state === "closed" ? "closed" : "open",
137 isDraft: pr.draft ?? /^(?:\[WIP\]|WIP:)/i.test(pr.title),137 isDraft: pr.draft ?? /^(?:\[WIP\]|WIP:)/i.test(pr.title),
138 mergeability:138 // A false value also covers drafts and unfinished or failed conflict checks.
139 pr.mergeable === undefined ? "unknown" : pr.mergeable ? "mergeable" : "conflicting",139 mergeability: pr.mergeable === true ? "mergeable" : "unknown",
140 additions: pr.additions ?? 0,140 additions: pr.additions ?? 0,
141 deletions: pr.deletions ?? 0,141 deletions: pr.deletions ?? 0,
142 createdAt: toIsoUtc(pr.created_at),142 createdAt: toIsoUtc(pr.created_at),
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+81
...@@ -199,6 +199,87 @@ it.effect("reads Forgejo checks without repository or viewer requests", () => {...@@ -199,6 +199,87 @@ it.effect("reads Forgejo checks without repository or viewer requests", () => {
199 );199 );
200});200});
201201
202it.effect.each([
203 ["Ready", false, true, false, "mergeable"],
204 ["Draft", true, false, true, "unknown"],
205 ["Blocked", false, false, false, "unknown"],
206 ["Unchecked", false, undefined, false, "unknown"],
207 ["WIP: Legacy draft", undefined, false, true, "unknown"],
208 ["[WIP] Legacy draft", undefined, false, true, "unknown"],
209 ["WIP: Explicitly ready", false, false, false, "unknown"],
210] as const)(
211 "reads Forgejo mergeability for %s (draft=%s, mergeable=%s) across list, summary and detail",
212 ([title, draft, mergeable, isDraft, mergeability]) => {
213 const pr = {
214 number: 42,
215 title,
216 body: "",
217 html_url: "https://forgejo.test/maria/project/pulls/42",
218 user: { login: "maria" },
219 state: "open",
220 merged: false,
221 ...(draft === undefined ? {} : { draft }),
222 ...(mergeable === undefined ? {} : { mergeable }),
223 head: { ref: "feature", sha: "head", repo: null },
224 base: { ref: "main", sha: "base", repo: null },
225 created_at: "2026-09-16T00:00:00Z",
226 updated_at: "2026-09-16T00:00:00Z",
227 closed_at: null,
228 merged_at: null,
229 labels: [],
230 };
231 return Effect.gen(function* () {
232 const provider = yield* ForgejoPullRequestProvider.make;
233 const readSummary = provider.getChangeRequestSummary;
234 if (readSummary === undefined) return yield* Effect.die("summary read missing");
235 const input = { cwd: "/repo", repository: "maria/project", host: "forgejo.test", number: 42 };
236 const list = yield* provider.listChangeRequests({
237 ...input,
238 state: "open",
239 involvement: "all",
240 viewer: "maria",
241 limit: 10,
242 });
243 const summary = yield* readSummary(input);
244 const detail = yield* provider.getChangeRequest(input);
245 assert.strictEqual(list.items.length, 1);
246 for (const result of [list.items[0]!, summary, detail]) {
247 assert.strictEqual(result.mergeability, mergeability);
248 assert.strictEqual(result.isDraft, isDraft);
249 }
250 }).pipe(
251 Effect.provide(
252 Layer.mock(ForgejoCli.ForgejoCli)({
253 api: (input) => {
254 const [path, query] = input.path.split("?");
255 let response: unknown;
256 switch (path) {
257 case "repos/maria/project/pulls":
258 response = new URLSearchParams(query).get("page") === "1" ? [pr] : [];
259 break;
260 case "repos/maria/project/pulls/42":
261 response = pr;
262 break;
263 case "repos/maria/project":
264 response = { full_name: "maria/project", permissions: { push: true, admin: true } };
265 break;
266 case "user":
267 response = pr.user;
268 break;
269 case "repos/maria/project/statuses/head":
270 response = [];
271 break;
272 default:
273 return Effect.die(`Unexpected Forgejo request: ${input.path}`);
274 }
275 return encodeJsonEffect(response).pipe(Effect.orDie, Effect.map(processOutput));
276 },
277 }),
278 ),
279 );
280 },
281);
282
202it.effect("loads Forgejo pull request references from files and commits views", () =>283it.effect("loads Forgejo pull request references from files and commits views", () =>
203 Effect.gen(function* () {284 Effect.gen(function* () {
204 const provider = yield* ForgejoSourceControlProvider.make;285 const provider = yield* ForgejoSourceControlProvider.make;