authorgravatar for pawel@mikolajczuk.inPaweł Mikołajczuk <pawel@mikolajczuk.in> 2026-10-13 11:48:37+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-12 19:48:37-05:00
log8110edfddd3fb4d7fd2e0c9d29256c3a1b9da5c8
tree3d1f6f16d1ac207215ed35bcf319a4f6e5787f07
parent017955206a47b08c7caaf10b75785d38456942e7
signature Signed by PGP key B5690EEEBB952194

fix(bitbucket): preserve PR head commits for review guides (#18402)


4 files changed, 87 insertions(+), 2 deletions(-)

packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.test.ts+53
...@@ -12,6 +12,59 @@ import {...@@ -12,6 +12,59 @@ import {
12 make,12 make,
13} from "./BitbucketPullRequestProvider.ts";13} from "./BitbucketPullRequestProvider.ts";
1414
15it.effect.each([
16 {
17 metadata: "source commit hash",
18 commit: { hash: "9f3a7c2d8b1e6f045a9c3d7e2b8f1a6c0d4e5f93" },
19 headSha: "9f3a7c2d8b1e6f045a9c3d7e2b8f1a6c0d4e5f93",
20 },
21 { metadata: "absent commit", commit: undefined, headSha: undefined },
22 { metadata: "null commit", commit: null, headSha: undefined },
23 { metadata: "absent hash", commit: {}, headSha: undefined },
24 { metadata: "null hash", commit: { hash: null }, headSha: undefined },
25])("reads change request details with $metadata", ({ commit, headSha }) =>
26 Effect.gen(function* () {
27 const pullRequest = Result.getOrThrow(
28 decodePullRequestJson(
29 JSON.stringify({
30 id: 897,
31 title: "Add trustabl-pipe",
32 state: "OPEN",
33 source: {
34 branch: { name: "feat/page" },
35 commit,
36 repository: { full_name: "fork/web" },
37 },
38 destination: { branch: { name: "master" }, commit: { hash: "base-commit" } },
39 created_on: "2026-06-16T05:04:32.258456+00:00",
40 updated_on: "2026-06-16T05:04:33.750542+00:00",
41 links: { html: { href: "https://bitbucket.org/acme/web/pull-requests/897" } },
42 }),
43 ),
44 );
45 const provider = yield* make.pipe(
46 Effect.provide(
47 Layer.mock(BitbucketPullRequestApi.BitbucketPullRequestApi)({
48 getPullRequest: () => Effect.succeed(pullRequest),
49 getDiffStat: () => Effect.succeed({ additions: 0, deletions: 0, changedFiles: 0 }),
50 getMergeability: () => Effect.succeed("unknown"),
51 listChecks: () => Effect.succeed([]),
52 getRepositoryPermission: () => Effect.succeed(true),
53 }),
54 ),
55 );
56 const detail = yield* provider.getChangeRequest({
57 cwd: "/repo",
58 repository: "acme/web",
59 host: "bitbucket.org",
60 number: 897,
61 });
62
63 expect(detail).toMatchObject({ number: 897, headBranch: "feat/page" });
64 expect(detail.headSha).toBe(headSha);
65 }),
66);
67
15for (const operation of [68for (const operation of [
16 "getMergeability",69 "getMergeability",
17 "listChecks",70 "listChecks",
packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts+1
...@@ -98,6 +98,7 @@ function toChangeRequest(pullRequest: BitbucketPullRequest): ProviderChangeReque...@@ -98,6 +98,7 @@ function toChangeRequest(pullRequest: BitbucketPullRequest): ProviderChangeReque
98 url: pullRequest.url,98 url: pullRequest.url,
99 author: pullRequest.author,99 author: pullRequest.author,
100 headBranch: pullRequest.headBranch,100 headBranch: pullRequest.headBranch,
101 ...(pullRequest.headSha ? { headSha: pullRequest.headSha } : {}),
101 ...(pullRequest.headRepositoryNameWithOwner102 ...(pullRequest.headRepositoryNameWithOwner
102 ? { headRepositoryNameWithOwner: pullRequest.headRepositoryNameWithOwner }103 ? { headRepositoryNameWithOwner: pullRequest.headRepositoryNameWithOwner }
103 : {}),104 : {}),
packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.test.ts+28-2
...@@ -24,8 +24,12 @@ function pullRequest(overrides: Record<string, unknown> = {}): Record<string, un...@@ -24,8 +24,12 @@ function pullRequest(overrides: Record<string, unknown> = {}): Record<string, un
24 created_on: "2026-06-16T05:04:32.258456+00:00",24 created_on: "2026-06-16T05:04:32.258456+00:00",
25 updated_on: "2026-06-16T05:04:33.750542+00:00",25 updated_on: "2026-06-16T05:04:33.750542+00:00",
26 author: { display_name: "Bilal Hassan", nickname: "bilal", type: "user" },26 author: { display_name: "Bilal Hassan", nickname: "bilal", type: "user" },
27 source: { branch: { name: "feat/page" }, repository: { full_name: "fork/web" } },27 source: {
28 destination: { branch: { name: "master" } },28 branch: { name: "feat/page" },
29 commit: { hash: "9f3a7c2d8b1e6f045a9c3d7e2b8f1a6c0d4e5f93", type: "commit" },
30 repository: { full_name: "fork/web" },
31 },
32 destination: { branch: { name: "master" }, commit: { hash: "base-commit" } },
29 links: { html: { href: "https://bitbucket.org/acme/web/pull-requests/897" } },33 links: { html: { href: "https://bitbucket.org/acme/web/pull-requests/897" } },
30 ...overrides,34 ...overrides,
31 };35 };
...@@ -52,6 +56,7 @@ describe("decodePullRequestPageJson", () => {...@@ -52,6 +56,7 @@ describe("decodePullRequestPageJson", () => {
52 url: "https://bitbucket.org/acme/web/pull-requests/897",56 url: "https://bitbucket.org/acme/web/pull-requests/897",
53 author: { login: "bilal", name: "Bilal Hassan" },57 author: { login: "bilal", name: "Bilal Hassan" },
54 headBranch: "feat/page",58 headBranch: "feat/page",
59 headSha: "9f3a7c2d8b1e6f045a9c3d7e2b8f1a6c0d4e5f93",
55 headRepositoryNameWithOwner: "fork/web",60 headRepositoryNameWithOwner: "fork/web",
56 baseBranch: "master",61 baseBranch: "master",
57 state: "open",62 state: "open",
...@@ -106,6 +111,27 @@ describe("decodePullRequestPageJson", () => {...@@ -106,6 +111,27 @@ describe("decodePullRequestPageJson", () => {
106});111});
107112
108describe("decodePullRequestJson", () => {113describe("decodePullRequestJson", () => {
114 it("preserves the source commit hash as the head SHA", () => {
115 const decoded = expectSuccess(decodePullRequestJson(JSON.stringify(pullRequest())));
116
117 expect(decoded).toMatchObject({ headSha: "9f3a7c2d8b1e6f045a9c3d7e2b8f1a6c0d4e5f93" });
118 });
119
120 it.each([
121 { metadata: "absent commit", commit: undefined },
122 { metadata: "null commit", commit: null },
123 { metadata: "absent hash", commit: {} },
124 { metadata: "null hash", commit: { hash: null } },
125 ])("reads detail and list payloads with $metadata", ({ commit }) => {
126 const raw = pullRequest({ source: { branch: { name: "feat/page" }, commit } });
127 const detail = expectSuccess(decodePullRequestJson(JSON.stringify(raw)));
128 const list = expectSuccess(decodePullRequestPageJson(page([raw])));
129
130 expect(detail).toMatchObject({ number: 897, headBranch: "feat/page", headSha: null });
131 expect(list.items).toHaveLength(1);
132 expect(list.items[0]).toMatchObject({ number: 897, headBranch: "feat/page", headSha: null });
133 });
134
109 it("reads reviewers as review requests", () => {135 it("reads reviewers as review requests", () => {
110 const decoded = expectSuccess(136 const decoded = expectSuccess(
111 decodePullRequestJson(137 decodePullRequestJson(
packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.ts+5
...@@ -52,6 +52,9 @@ const RawUserSchema = Schema.Struct({...@@ -52,6 +52,9 @@ const RawUserSchema = Schema.Struct({
52 */52 */
53const RawBranchSchema = Schema.Struct({53const RawBranchSchema = Schema.Struct({
54 branch: Schema.Struct({ name: TrimmedNonEmptyString }),54 branch: Schema.Struct({ name: TrimmedNonEmptyString }),
55 commit: Schema.optional(
56 Schema.NullOr(Schema.Struct({ hash: Schema.optional(Schema.NullOr(Schema.String)) })),
57 ),
55 repository: Schema.optional(Schema.NullOr(Schema.Struct({ full_name: TrimmedNonEmptyString }))),58 repository: Schema.optional(Schema.NullOr(Schema.Struct({ full_name: TrimmedNonEmptyString }))),
56});59});
5760
...@@ -178,6 +181,7 @@ export interface BitbucketPullRequest {...@@ -178,6 +181,7 @@ export interface BitbucketPullRequest {
178 readonly url: string;181 readonly url: string;
179 readonly author: PullRequestActor | null;182 readonly author: PullRequestActor | null;
180 readonly headBranch: string;183 readonly headBranch: string;
184 readonly headSha: string | null;
181 readonly headRepositoryNameWithOwner: string | null;185 readonly headRepositoryNameWithOwner: string | null;
182 readonly baseBranch: string;186 readonly baseBranch: string;
183 readonly state: PullRequestState;187 readonly state: PullRequestState;
...@@ -293,6 +297,7 @@ function toPullRequest(raw: Schema.Schema.Type<typeof RawPullRequestSchema>): Bi...@@ -293,6 +297,7 @@ function toPullRequest(raw: Schema.Schema.Type<typeof RawPullRequestSchema>): Bi
293 url: raw.links.html.href,297 url: raw.links.html.href,
294 author: toActor(raw.author),298 author: toActor(raw.author),
295 headBranch: raw.source.branch.name,299 headBranch: raw.source.branch.name,
300 headSha: trimmed(raw.source.commit?.hash),
296 headRepositoryNameWithOwner: raw.source.repository?.full_name ?? null,301 headRepositoryNameWithOwner: raw.source.repository?.full_name ?? null,
297 baseBranch: raw.destination.branch.name,302 baseBranch: raw.destination.branch.name,
298 state: toState(raw),303 state: toState(raw),