diff --git a/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts b/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts index 111ffbd1537c3..588734b754f50 100644 --- a/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts +++ b/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts @@ -276,6 +276,42 @@ describe("GitCafeSourceControlProvider", () => { ); }); + it.effect("keeps a GitHub fork's pull request off the repository's own branch", () => { + const client = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response( + encodeJson({ + number: 7, + title: "Change", + state: "open", + draft: false, + sourceBranch: "main", + targetBranch: "main", + headOid: null, + updatedAt: null, + sourceRepo: null, + isCrossFork: true, + closedAt: null, + mergedAt: null, + }), + ), + ), + ), + ); + return Effect.gen(function* () { + const provider = yield* GitCafeSourceControlProvider.make; + const pull = yield* provider.getChangeRequest({ + cwd: "/repo", + reference: "https://git.cafe/team/project/pulls/7", + }); + assert.strictEqual(pull.isCrossRepository, true); + assert.strictEqual(pull.headRepositoryNameWithOwner, undefined); + assert.strictEqual(pull.headRepositoryOwnerLogin, undefined); + }).pipe(Effect.provide(layerFor(client))); + }); + it.effect("opens a pull request from a fork over REST with the body file's text", () => { const sent: Array<{ method: string; url: string; body: unknown }> = []; const client = HttpClient.make((request) => { diff --git a/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.ts b/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.ts index e02ebd0b45527..8800622987f55 100644 --- a/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.ts +++ b/packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.ts @@ -139,6 +139,7 @@ const PullDetail = Schema.Struct({ sourceRepo: Schema.NullOr( Schema.Struct({ owner: TrimmedNonEmptyString, name: TrimmedNonEmptyString }), ), + isCrossFork: Schema.optional(Schema.Boolean), closedAt: Schema.NullOr(Schema.String), mergedAt: Schema.NullOr(Schema.String), }); @@ -265,6 +266,8 @@ export const make = Effect.gen(function* () { PullDetail, ); const owner = target.repository.split("/")[0] ?? null; + // Pulls synced from GitHub forks report isCrossFork without naming the fork. + const unnamedFork = pull.sourceRepo === null && pull.isCrossFork === true; return { provider: SourceControlProviderKind.make("gitcafe"), number: pull.number, @@ -278,16 +281,22 @@ export const make = Effect.gen(function* () { closedAt: pull.closedAt, mergedAt: pull.mergedAt, updatedAt: pull.updatedAt, - isCrossRepository: pull.sourceRepo !== null, - headRepositoryNameWithOwner: pull.sourceRepo - ? `${pull.sourceRepo.owner}/${pull.sourceRepo.name}` - : target.repository, - headRepositoryOwnerLogin: pull.sourceRepo?.owner ?? owner, + isCrossRepository: pull.sourceRepo !== null || unnamedFork, + ...(unnamedFork + ? {} + : { + headRepositoryNameWithOwner: pull.sourceRepo + ? `${pull.sourceRepo.owner}/${pull.sourceRepo.name}` + : target.repository, + headRepositoryOwnerLogin: pull.sourceRepo?.owner ?? owner, + }), } satisfies ChangeRequest; }); return SourceControlProvider.SourceControlProvider.of({ kind: SourceControlProviderKind.make("gitcafe"), + // Fork pulls share bare branch names; read past them to this repository's own pull. + headBranchProbe: ({ headSelectors }) => ({ headSelectors, limit: 20 }), repositoryNameFromRemoteUrl: (url) => GitCafeHosts.parseGitCafeRemote(url)?.repository ?? null, getChangeRequest: getPull, listChangeRequests: Effect.fn("GitCafeSourceControlProvider.listChangeRequests")(