Skip to content

Commit e0d0fb3

Browse files
fix(forgejo): renamed repositories keep syncing their pull request links (#32629)
1 parent 5cff012 commit e0d0fb3

2 files changed

Lines changed: 92 additions & 15 deletions

File tree

‎packages/source-control-forgejo/src/server/ForgejoCli.ts‎

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -350,26 +350,47 @@ export const make = Effect.gen(function* () {
350350
readonly body?: string;
351351
}) {
352352
const base = new URL(`${input.baseUrl}/api/v1/`);
353-
const url = new URL(input.path, base);
354-
if (
355-
url.origin !== base.origin ||
356-
!url.pathname.startsWith(base.pathname) ||
357-
url.username ||
358-
url.password
359-
)
353+
const apiUrl = (path: string, from: string) => {
354+
const url = URL.parse(path, from);
355+
return url &&
356+
url.origin === base.origin &&
357+
url.pathname.startsWith(base.pathname) &&
358+
!url.username &&
359+
!url.password
360+
? url
361+
: undefined;
362+
};
363+
const requested = apiUrl(input.path, base.href);
364+
if (!requested)
360365
return yield* new ForgejoCliError({
361366
command: "fj",
362367
cwd: input.cwd,
363368
detail: "Invalid Forgejo API path.",
364369
});
365-
let request = HttpClientRequest.make(input.method ?? "GET")(url.toString()).pipe(
366-
HttpClientRequest.setHeader("Authorization", `token ${input.token}`),
367-
);
368-
if (input.body !== undefined)
369-
request = request.pipe(HttpClientRequest.bodyText(input.body, "application/json"));
370-
const response = yield* httpClient
371-
.execute(request)
372-
.pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }));
370+
let url = requested;
371+
const send = (target: URL) => {
372+
let request = HttpClientRequest.make(input.method ?? "GET")(target.toString()).pipe(
373+
HttpClientRequest.setHeader("Authorization", `token ${input.token}`),
374+
);
375+
if (input.body !== undefined)
376+
request = request.pipe(HttpClientRequest.bodyText(input.body, "application/json"));
377+
return httpClient
378+
.execute(request)
379+
.pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }));
380+
};
381+
let response = yield* send(url);
382+
// Forgejo redirects a renamed repository's API paths before running the handler,
383+
// so the same method and body are re-sent. The token never leaves the API base.
384+
for (let hops = 0; hops < 5; hops++) {
385+
const location = response.headers.location;
386+
const next =
387+
location && [301, 302, 307, 308].includes(response.status)
388+
? apiUrl(location, url.href)
389+
: undefined;
390+
if (!next) break;
391+
url = next;
392+
response = yield* send(url);
393+
}
373394
const status = response.status;
374395
const body =
375396
status === 204 || status === 205

‎packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,62 @@ it.effect("loads later fj review pages when the server caps pages below the requ
17861786
);
17871787
});
17881788

1789+
it.effect("re-sends an fj write with its method and body to a renamed repository's path", () => {
1790+
const requests: string[] = [];
1791+
return Effect.gen(function* () {
1792+
const cli = yield* ForgejoCli.make;
1793+
yield* cli.api({
1794+
cwd: "/repo",
1795+
repository: "https://forgejo.local/maria/old-name",
1796+
path: "repos/maria/old-name/issues/42/comments",
1797+
method: "POST",
1798+
body: { body: "only once" },
1799+
});
1800+
assert.deepStrictEqual(requests, [
1801+
"https://forgejo.local/api/v1/repos/maria/old-name/issues/42/comments",
1802+
"https://forgejo.local/api/v1/repos/maria/new-name/issues/42/comments",
1803+
]);
1804+
}).pipe(
1805+
Effect.provideService(
1806+
FileSystem.FileSystem,
1807+
FileSystem.makeNoop({
1808+
exists: () => Effect.succeed(true),
1809+
readFileString: () =>
1810+
Effect.succeed(
1811+
encodeJson({
1812+
hosts: { "forgejo.local": { type: "Application", token: "test-token" } },
1813+
}),
1814+
),
1815+
}),
1816+
),
1817+
Effect.provideService(
1818+
HttpClient.HttpClient,
1819+
HttpClient.make((request) => {
1820+
requests.push(request.url);
1821+
assert.strictEqual(request.method, "POST");
1822+
assert.strictEqual(request.headers.authorization, "token test-token");
1823+
assert.deepStrictEqual(requestJson(request), { body: "only once" });
1824+
return Effect.succeed(
1825+
HttpClientResponse.fromWeb(
1826+
request,
1827+
requests.length === 1
1828+
? new Response(null, {
1829+
status: 301,
1830+
headers: { location: "/api/v1/repos/maria/new-name/issues/42/comments" },
1831+
})
1832+
: new Response("{}", { status: 201 }),
1833+
),
1834+
);
1835+
}),
1836+
),
1837+
Effect.provide(
1838+
TestSourceControlHost.layer({
1839+
process: { run: () => Effect.succeed(processOutput("")) },
1840+
}),
1841+
),
1842+
);
1843+
});
1844+
17891845
it.effect("handles fj mutation statuses without retrying failures or reading absent bodies", () =>
17901846
Effect.gen(function* () {
17911847
for (const status of [204, 205, 302, 401, 403, 404, 429, 500]) {

0 commit comments

Comments
 (0)