Skip to content
Open
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
33 changes: 33 additions & 0 deletions apps/server/src/pullRequest/GiteaPullRequestApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,39 @@ layer("GiteaPullRequestApi", (it) => {
expect(yield* api.getAutoMergeEnabled({host: "forge.example.test", repository: "acme/web", number: 7})).toBe(true);
expect(callAt(1).path).toContain("/timeline?");
}));
it.effect("opens a native revert PR only on an advertising Gitea server", () =>
Effect.gen(function* () {
mockedRequest
.mockReturnValueOnce(Effect.succeed(response({ features: ["pull-revert"] })))
.mockReturnValueOnce(Effect.succeed(response(rawPullRequest(8))));
const api = yield* GiteaPullRequestApi.make;
yield* api.runAction({
host: "forge.example.test",
repository: "acme/web",
number: 7,
action: "revert",
});
expect(callAt(1)).toMatchObject({ method: "POST", path: "/repos/acme/web/pulls/7/revert" });
expect(mockedRequest.mock.calls).toHaveLength(2);
}),
);
it.effect("does not attempt a revert on stock Gitea", () =>
Effect.gen(function* () {
mockedRequest.mockReturnValueOnce(Effect.succeed(response({ features: [] })));
const api = yield* GiteaPullRequestApi.make;
const error = yield* api
.runAction({
host: "forge.example.test",
repository: "acme/web",
number: 7,
action: "revert",
})
.pipe(Effect.flip);
expect(error.detail).toContain("does not expose native pull request reverts");
expect(mockedRequest.mock.calls.every(([call]) => call.method === "GET")).toBe(true);
}),
);

it.effect("approves only the current pull request's waiting workflow runs", () =>
Effect.gen(function* () {
const pull = rawPullRequest(7);
Expand Down
25 changes: 19 additions & 6 deletions apps/server/src/pullRequest/GiteaPullRequestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1631,12 +1631,25 @@ export const make = Effect.gen(function* () {
},
);

const unsupportedAction = (action: string) =>
new GiteaPullRequestApiError({
operation: "runAction",
reason: "failed",
detail: `Gitea does not expose a reliable ${action} operation through this API.`,
const revertPullRequest = Effect.fn("GiteaPullRequestApi.revertPullRequest")(function* (input: {
host: string;
repository: string;
number: number;
}) {
yield* validateHost(input.host);
if (!(yield* getFeatures).includes("pull-revert"))
return yield* new GiteaPullRequestApiError({
operation: "revertPullRequest",
reason: "failed",
detail: "This Gitea server does not expose native pull request reverts.",
});
return yield* write({
operation: "revertPullRequest",
...input,
method: "POST",
path: `${basePath(input.repository)}/pulls/${input.number}/revert`,
});
});

return GiteaPullRequestApi.of({
getFeatures: () => getFeatures,
Expand Down Expand Up @@ -1833,7 +1846,7 @@ export const make = Effect.gen(function* () {
case "approve-workflows":
return approveWorkflows(input);
case "revert":
return Effect.fail(unsupportedAction(input.action));
return revertPullRequest(input);
}
},
updatePullRequest: (input) =>
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/pullRequest/GiteaPullRequestProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,19 @@ describe("giteaProviderFailure", () => {
).toEqual({ reason: "rate-limited", retryAt: 1234 });
});
});

describe("native revert permission", () => {
it("requires write access and the advertised native endpoint", () => {
const input = { canWrite: true, ownsPullRequest: false, updateMethods: [] as const };
expect(giteaViewerPermissions(input).actions).not.toContain("revert");
expect(giteaViewerPermissions({ ...input, revertSupported: true }).actions).toContain("revert");
expect(
giteaViewerPermissions({
...input,
canWrite: false,
ownsPullRequest: true,
revertSupported: true,
}).actions,
).not.toContain("revert");
});
});
10 changes: 9 additions & 1 deletion apps/server/src/pullRequest/GiteaPullRequestProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const CAPABILITIES: PullRequestCapabilities = {
"enable-auto-merge",
"disable-auto-merge",
"approve-workflows",
"revert",
],
mergeMethods: ["merge", "squash", "rebase"],
updateMethods: ["merge", "rebase"],
Expand Down Expand Up @@ -66,11 +67,13 @@ export function giteaProviderFailure(
export function giteaViewerPermissions(input: {
readonly canWrite: boolean;
readonly workflowApprovalSupported?: boolean;
readonly revertSupported?: boolean;
readonly ownsPullRequest: boolean;
readonly updateMethods: ReadonlyArray<"merge" | "rebase">;
}): PullRequestViewerPermissions {
return {
actions: CAPABILITIES.actions.filter((action) => {
if (action === "revert") return input.canWrite && input.revertSupported === true;
if (action === "approve-workflows")
return input.canWrite && input.workflowApprovalSupported === true;
if (action === "ready" || action === "draft" || action === "close" || action === "reopen")
Expand Down Expand Up @@ -137,10 +140,12 @@ export const make = Effect.gen(function* () {
readonly viewer: string;
readonly author: string | undefined;
readonly workflowApprovalSupported?: boolean;
readonly revertSupported?: boolean;
}) =>
giteaViewerPermissions({
canWrite: input.access.canWrite,
workflowApprovalSupported: input.workflowApprovalSupported,
revertSupported: input.revertSupported,
ownsPullRequest:
input.author !== undefined && input.author.toLowerCase() === input.viewer.toLowerCase(),
updateMethods: input.access.updateMethods,
Expand Down Expand Up @@ -190,10 +195,11 @@ export const make = Effect.gen(function* () {
api
.getWorkflowApprovals(input)
.pipe(Effect.orElseSucceed(() => ({ supported: false, runs: [] }))),
api.getFeatures().pipe(Effect.orElseSucceed(() => [])),
],
{ concurrency: 4 },
).pipe(
Effect.flatMap(([pullRequest, access, viewer, autoMergeEnabled, workflows]) =>
Effect.flatMap(([pullRequest, access, viewer, autoMergeEnabled, workflows, features]) =>
api.listChecks({ ...input, sha: pullRequest.headSha }).pipe(
Effect.orElseSucceed(() => []),
Effect.map((checks): ProviderChangeRequestDetail => ({
Expand Down Expand Up @@ -224,6 +230,7 @@ export const make = Effect.gen(function* () {
viewer,
author: pullRequest.author?.login,
workflowApprovalSupported: workflows.supported,
revertSupported: features.includes("pull-revert"),
}),
})),
),
Expand Down Expand Up @@ -332,6 +339,7 @@ export const make = Effect.gen(function* () {
viewer,
author: pullRequest.author?.login,
workflowApprovalSupported: features.includes("actions-run-approve"),
revertSupported: features.includes("pull-revert"),
}),
),
),
Expand Down