From c3bf66dbc27ae48bb05f3baa188d7a8676566a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=81=9A=E4=BA=86=E7=9D=A1=E5=A4=A7=E8=A7=89?= <64798754+stakeswky@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:33:04 +0800 Subject: [PATCH] fix: handle fork PRs by fetching via refs/pull/N/head (#962) (#963) When a PR originates from a fork, `git fetch origin ` fails because the branch only exists on the fork's remote. Fix: detect cross-repository PRs via the `isCrossRepository` GraphQL field and fetch using `pull//head:` refspec instead, which is the standard GitHub mechanism for accessing fork PR branches. Changes: - Add `isCrossRepository` and `headRepository` to PR GraphQL query - Add corresponding fields to GitHubPullRequest type - Branch checkout uses pull ref for fork PRs - Update test fixtures with new fields Co-authored-by: User --- src/github/api/queries/github.ts | 7 +++++++ src/github/operations/branch.ts | 20 +++++++++++++++++--- src/github/types.ts | 7 +++++++ test/create-prompt.test.ts | 2 ++ test/data-fetcher.test.ts | 2 ++ test/data-formatter.test.ts | 2 ++ test/pull-request-target.test.ts | 2 ++ 7 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/github/api/queries/github.ts b/src/github/api/queries/github.ts index 7bceb8f..08dc25b 100644 --- a/src/github/api/queries/github.ts +++ b/src/github/api/queries/github.ts @@ -12,6 +12,13 @@ export const PR_QUERY = ` baseRefName headRefName headRefOid + isCrossRepository + headRepository { + owner { + login + } + name + } createdAt updatedAt lastEditedAt diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index b2c145b..4de3546 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -166,9 +166,23 @@ export async function setupBranch( // Validate branch names before use to prevent command injection validateBranchName(branchName); - // Execute git commands to checkout PR branch (dynamic depth based on PR size) - // Using execFileSync instead of shell template literals for security - execGit(["fetch", "origin", `--depth=${fetchDepth}`, branchName]); + // For cross-repository (fork) PRs, fetch via the pull ref since the + // branch only exists on the fork's remote, not on origin. + if (prData.isCrossRepository) { + console.log( + `PR #${entityNumber} is from a fork, fetching via refs/pull/${entityNumber}/head...`, + ); + execGit([ + "fetch", + "origin", + `--depth=${fetchDepth}`, + `pull/${entityNumber}/head:${branchName}`, + ]); + } else { + // Execute git commands to checkout PR branch (dynamic depth based on PR size) + // Using execFileSync instead of shell template literals for security + execGit(["fetch", "origin", `--depth=${fetchDepth}`, branchName]); + } execGit(["checkout", branchName, "--"]); console.log(`Successfully checked out PR branch for PR #${entityNumber}`); diff --git a/src/github/types.ts b/src/github/types.ts index d982620..6ed41e3 100644 --- a/src/github/types.ts +++ b/src/github/types.ts @@ -57,6 +57,13 @@ export type GitHubPullRequest = { baseRefName: string; headRefName: string; headRefOid: string; + isCrossRepository: boolean; + headRepository: { + owner: { + login: string; + }; + name: string; + } | null; createdAt: string; updatedAt?: string; lastEditedAt?: string; diff --git a/test/create-prompt.test.ts b/test/create-prompt.test.ts index cc3f306..f4b3b34 100644 --- a/test/create-prompt.test.ts +++ b/test/create-prompt.test.ts @@ -27,6 +27,8 @@ describe("generatePrompt", () => { baseRefName: "main", headRefName: "feature-branch", headRefOid: "abc123", + isCrossRepository: false, + headRepository: { owner: { login: "testowner" }, name: "testrepo" }, commits: { totalCount: 2, nodes: [ diff --git a/test/data-fetcher.test.ts b/test/data-fetcher.test.ts index 98f52e5..c474264 100644 --- a/test/data-fetcher.test.ts +++ b/test/data-fetcher.test.ts @@ -1006,6 +1006,8 @@ describe("fetchGitHubData integration with time filtering", () => { baseRefName: "main", headRefName: "feature", headRefOid: "abc123", + isCrossRepository: false, + headRepository: { owner: { login: "testowner" }, name: "testrepo" }, createdAt: "2024-01-15T10:00:00Z", updatedAt: "2024-01-15T12:30:00Z", // Edited after trigger lastEditedAt: "2024-01-15T12:30:00Z", // Edited after trigger diff --git a/test/data-formatter.test.ts b/test/data-formatter.test.ts index 4c6b150..375bc58 100644 --- a/test/data-formatter.test.ts +++ b/test/data-formatter.test.ts @@ -24,6 +24,8 @@ describe("formatContext", () => { baseRefName: "main", headRefName: "feature/test", headRefOid: "abc123", + isCrossRepository: false, + headRepository: { owner: { login: "testowner" }, name: "testrepo" }, createdAt: "2023-01-01T00:00:00Z", additions: 50, deletions: 30, diff --git a/test/pull-request-target.test.ts b/test/pull-request-target.test.ts index 6d9cac0..666c1b3 100644 --- a/test/pull-request-target.test.ts +++ b/test/pull-request-target.test.ts @@ -17,6 +17,8 @@ describe("pull_request_target event support", () => { baseRefName: "main", headRefName: "feature-branch", headRefOid: "abc123", + isCrossRepository: false, + headRepository: { owner: { login: "testowner" }, name: "testrepo" }, commits: { totalCount: 2, nodes: [