From dfd06aec5894829f715dac7d8584f62bfe7588c4 Mon Sep 17 00:00:00 2001 From: Kalven Schraut Date: Sat, 5 Sep 2026 00:02:02 -0500 Subject: [PATCH] fix(pull-requests): keep incomplete Gitea access conservative --- .../pullRequest/GiteaPullRequestApi.test.ts | 52 ++++++++++++++----- .../src/pullRequest/GiteaPullRequestApi.ts | 18 +++---- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts index 85c5c61c2ea6..6d5bdc394c27 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.test.ts @@ -144,12 +144,30 @@ it.effect("keeps a search hydration transport failure fatal", () => ); layer("GiteaPullRequestApi", (it) => { - it.effect("reconstructs auto-merge from the timeline when discovery is unavailable", () => Effect.gen(function* () { - mockedRequest.mockReturnValueOnce(Effect.fail(new GiteaApi.GiteaApiError({operation: "getFeatures", reason: "failed", detail: "temporarily unavailable"}))).mockReturnValueOnce(Effect.succeed(response([{id: 1, type: "pull_scheduled_merge"}]))); - const api = yield* GiteaPullRequestApi.make; - expect(yield* api.getAutoMergeEnabled({host: "forge.example.test", repository: "acme/web", number: 7})).toBe(true); - expect(callAt(1).path).toContain("/timeline?"); - })); + it.effect("reconstructs auto-merge from the timeline when discovery is unavailable", () => + Effect.gen(function* () { + mockedRequest + .mockReturnValueOnce( + Effect.fail( + new GiteaApi.GiteaApiError({ + operation: "getFeatures", + reason: "failed", + detail: "temporarily unavailable", + }), + ), + ) + .mockReturnValueOnce(Effect.succeed(response([{ id: 1, type: "pull_scheduled_merge" }]))); + const api = yield* GiteaPullRequestApi.make; + 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 @@ -821,7 +839,7 @@ layer("GiteaPullRequestApi", (it) => { }), ); - it.effect("does not turn omitted repository permissions into a denial", () => + it.effect("does not advertise writes or merge methods from incomplete repository settings", () => Effect.gen(function* () { mockedRequest.mockReturnValueOnce(Effect.succeed(response({}))); const api = yield* GiteaPullRequestApi.make; @@ -831,13 +849,13 @@ layer("GiteaPullRequestApi", (it) => { }); expect(access).toEqual({ - canWrite: true, + canWrite: false, mergeCapabilities: { - merge: true, - squash: true, - rebase: true, + merge: false, + squash: false, + rebase: false, }, - updateMethods: ["merge", "rebase"], + updateMethods: [], }); }), ); @@ -1042,6 +1060,16 @@ layer("GiteaPullRequestApi", (it) => { }), ); + it.effect("does not request commit statuses without a head revision", () => + Effect.gen(function* () { + const api = yield* GiteaPullRequestApi.make; + expect( + yield* api.listChecks({ host: "forge.example.test", repository: "acme/web", sha: "" }), + ).toEqual([]); + expect(mockedRequest).not.toHaveBeenCalled(); + }), + ); + it.effect("reads every capped page of commit statuses and keeps the newest context", () => Effect.gen(function* () { mockedRequest diff --git a/apps/server/src/pullRequest/GiteaPullRequestApi.ts b/apps/server/src/pullRequest/GiteaPullRequestApi.ts index 3307ee7c70a6..5a0c6fb6fda7 100644 --- a/apps/server/src/pullRequest/GiteaPullRequestApi.ts +++ b/apps/server/src/pullRequest/GiteaPullRequestApi.ts @@ -1082,20 +1082,15 @@ export const make = Effect.gen(function* () { }); const repo = yield* decode(operation, RawRepository, response); return { - // An omitted permission block is unknown rather than a denial. Gitea will still enforce - // the write, while hiding it here would leave an entitled viewer with no route to try. - canWrite: - repo.permissions == null || - repo.permissions.push === true || - repo.permissions.admin === true, + canWrite: repo.permissions?.push === true || repo.permissions?.admin === true, mergeCapabilities: { - merge: repo.allow_merge_commits ?? true, - squash: repo.allow_squash_merge ?? true, - rebase: repo.allow_rebase ?? true, + merge: repo.allow_merge_commits === true, + squash: repo.allow_squash_merge === true, + rebase: repo.allow_rebase === true, }, updateMethods: [ - ...(repo.allow_merge_update !== false ? (["merge"] as const) : []), - ...(repo.allow_rebase_update !== false ? (["rebase"] as const) : []), + ...(repo.allow_merge_update === true ? (["merge"] as const) : []), + ...(repo.allow_rebase_update === true ? (["rebase"] as const) : []), ], }; }, @@ -1337,6 +1332,7 @@ export const make = Effect.gen(function* () { sha: string; }) { const operation = "listChecks"; + if (input.sha.trim() === "") return []; const statuses: Array = []; let path = query( `${basePath(input.repository)}/commits/${encodeURIComponent(input.sha)}/status`,