Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions packages/source-control-forgejo/src/server/ForgejoCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,26 +350,47 @@ export const make = Effect.gen(function* () {
readonly body?: string;
}) {
const base = new URL(`${input.baseUrl}/api/v1/`);
const url = new URL(input.path, base);
if (
url.origin !== base.origin ||
!url.pathname.startsWith(base.pathname) ||
url.username ||
url.password
)
const apiUrl = (path: string, from: string) => {
const url = URL.parse(path, from);
return url &&
url.origin === base.origin &&
url.pathname.startsWith(base.pathname) &&
!url.username &&
!url.password
? url
: undefined;
};
const requested = apiUrl(input.path, base.href);
if (!requested)
return yield* new ForgejoCliError({
command: "fj",
cwd: input.cwd,
detail: "Invalid Forgejo API path.",
});
let request = HttpClientRequest.make(input.method ?? "GET")(url.toString()).pipe(
HttpClientRequest.setHeader("Authorization", `token ${input.token}`),
);
if (input.body !== undefined)
request = request.pipe(HttpClientRequest.bodyText(input.body, "application/json"));
const response = yield* httpClient
.execute(request)
.pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }));
let url = requested;
const send = (target: URL) => {
let request = HttpClientRequest.make(input.method ?? "GET")(target.toString()).pipe(
HttpClientRequest.setHeader("Authorization", `token ${input.token}`),
);
if (input.body !== undefined)
request = request.pipe(HttpClientRequest.bodyText(input.body, "application/json"));
return httpClient
.execute(request)
.pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }));
};
let response = yield* send(url);
// Forgejo redirects a renamed repository's API paths before running the handler,
// so the same method and body are re-sent. The token never leaves the API base.
for (let hops = 0; hops < 5; hops++) {
const location = response.headers.location;
const next =
location && [301, 302, 307, 308].includes(response.status)
? apiUrl(location, url.href)
: undefined;
if (!next) break;
url = next;
response = yield* send(url);
}
const status = response.status;
const body =
status === 204 || status === 205
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,62 @@ it.effect("loads later fj review pages when the server caps pages below the requ
);
});

it.effect("re-sends an fj write with its method and body to a renamed repository's path", () => {
const requests: string[] = [];
return Effect.gen(function* () {
const cli = yield* ForgejoCli.make;
yield* cli.api({
cwd: "/repo",
repository: "https://forgejo.local/maria/old-name",
path: "repos/maria/old-name/issues/42/comments",
method: "POST",
body: { body: "only once" },
});
assert.deepStrictEqual(requests, [
"https://forgejo.local/api/v1/repos/maria/old-name/issues/42/comments",
"https://forgejo.local/api/v1/repos/maria/new-name/issues/42/comments",
]);
}).pipe(
Effect.provideService(
FileSystem.FileSystem,
FileSystem.makeNoop({
exists: () => Effect.succeed(true),
readFileString: () =>
Effect.succeed(
encodeJson({
hosts: { "forgejo.local": { type: "Application", token: "test-token" } },
}),
),
}),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
requests.push(request.url);
assert.strictEqual(request.method, "POST");
assert.strictEqual(request.headers.authorization, "token test-token");
assert.deepStrictEqual(requestJson(request), { body: "only once" });
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
requests.length === 1
? new Response(null, {
status: 301,
headers: { location: "/api/v1/repos/maria/new-name/issues/42/comments" },
})
: new Response("{}", { status: 201 }),
),
);
}),
),
Effect.provide(
TestSourceControlHost.layer({
process: { run: () => Effect.succeed(processOutput("")) },
}),
),
);
});

it.effect("handles fj mutation statuses without retrying failures or reading absent bodies", () =>
Effect.gen(function* () {
for (const status of [204, 205, 302, 401, 403, 404, 429, 500]) {
Expand Down
Loading