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 {
1212 make,
1313} 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
1568for (const operation of [
1669 "getMergeability",
1770 "listChecks",
packages/source-control-bitbucket/src/server/BitbucketPullRequestProvider.ts+1
......@@ -98,6 +98,7 @@ function toChangeRequest(pullRequest: BitbucketPullRequest): ProviderChangeReque
9898 url: pullRequest.url,
9999 author: pullRequest.author,
100100 headBranch: pullRequest.headBranch,
101 ...(pullRequest.headSha ? { headSha: pullRequest.headSha } : {}),
101102 ...(pullRequest.headRepositoryNameWithOwner
102103 ? { headRepositoryNameWithOwner: pullRequest.headRepositoryNameWithOwner }
103104 : {}),
packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.test.ts+28-2
......@@ -24,8 +24,12 @@ function pullRequest(overrides: Record<string, unknown> = {}): Record<string, un
2424 created_on: "2026-06-16T05:04:32.258456+00:00",
2525 updated_on: "2026-06-16T05:04:33.750542+00:00",
2626 author: { display_name: "Bilal Hassan", nickname: "bilal", type: "user" },
27 source: { branch: { name: "feat/page" }, repository: { full_name: "fork/web" } },
28 destination: { branch: { name: "master" } },
27 source: {
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" } },
2933 links: { html: { href: "https://bitbucket.org/acme/web/pull-requests/897" } },
3034 ...overrides,
3135 };
......@@ -52,6 +56,7 @@ describe("decodePullRequestPageJson", () => {
5256 url: "https://bitbucket.org/acme/web/pull-requests/897",
5357 author: { login: "bilal", name: "Bilal Hassan" },
5458 headBranch: "feat/page",
59 headSha: "9f3a7c2d8b1e6f045a9c3d7e2b8f1a6c0d4e5f93",
5560 headRepositoryNameWithOwner: "fork/web",
5661 baseBranch: "master",
5762 state: "open",
......@@ -106,6 +111,27 @@ describe("decodePullRequestPageJson", () => {
106111});
107112
108113describe("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
109135 it("reads reviewers as review requests", () => {
110136 const decoded = expectSuccess(
111137 decodePullRequestJson(
packages/source-control-bitbucket/src/server/bitbucketPullRequestJson.ts+5
......@@ -52,6 +52,9 @@ const RawUserSchema = Schema.Struct({
5252 */
5353const RawBranchSchema = Schema.Struct({
5454 branch: Schema.Struct({ name: TrimmedNonEmptyString }),
55 commit: Schema.optional(
56 Schema.NullOr(Schema.Struct({ hash: Schema.optional(Schema.NullOr(Schema.String)) })),
57 ),
5558 repository: Schema.optional(Schema.NullOr(Schema.Struct({ full_name: TrimmedNonEmptyString }))),
5659});
5760
......@@ -178,6 +181,7 @@ export interface BitbucketPullRequest {
178181 readonly url: string;
179182 readonly author: PullRequestActor | null;
180183 readonly headBranch: string;
184 readonly headSha: string | null;
181185 readonly headRepositoryNameWithOwner: string | null;
182186 readonly baseBranch: string;
183187 readonly state: PullRequestState;
......@@ -293,6 +297,7 @@ function toPullRequest(raw: Schema.Schema.Type<typeof RawPullRequestSchema>): Bi
293297 url: raw.links.html.href,
294298 author: toActor(raw.author),
295299 headBranch: raw.source.branch.name,
300 headSha: trimmed(raw.source.commit?.hash),
296301 headRepositoryNameWithOwner: raw.source.repository?.full_name ?? null,
297302 baseBranch: raw.destination.branch.name,
298303 state: toState(raw),