Prefer exact PR lookups and stale sidebar refresh

Prefer exact linked PR lookup when safe, add stale-while-revalidate for sidebar hosted review metadata, and preserve branch discovery correctness for stale linked PR metadata.
This commit is contained in:
Neil
2026-05-15 22:42:17 -07:00
committed by GitHub
parent 5b88551e3b
commit 4bfd1eb157
6 changed files with 584 additions and 99 deletions
+89 -23
View File
@@ -1302,20 +1302,84 @@ async function getRestPRByNumber(
return mapRestPullRequest(JSON.parse(stdout) as RestPullRequest)
}
async function getPRByNumber(
ownerRepo: OwnerRepo,
number: number,
ghOptions: ReturnType<typeof ghRepoExecOptions>
): Promise<PullRequestLookupData | null> {
try {
const { stdout } = await ghExecFileAsync(
[
'pr',
'view',
String(number),
'--repo',
`${ownerRepo.owner}/${ownerRepo.repo}`,
'--json',
PR_LOOKUP_JSON_FIELDS
],
ghOptions
)
return JSON.parse(stdout) as PullRequestLookupData
} catch (err) {
// Why: deleted or manually edited linked PR metadata should fall back to
// branch discovery; quota/auth/network failures get one cheaper REST exact lookup.
if (isNotFoundGhError(err)) {
return null
}
try {
return await getRestPRByNumber(ownerRepo, number, ghOptions)
} catch (restErr) {
if (isNotFoundGhError(restErr)) {
return null
}
if (!shouldStopAfterExactLookupError(restErr)) {
return null
}
throw restErr
}
}
}
async function exactPRMatchesWorktreeHead(
repoPath: string,
branchName: string,
data: PullRequestLookupData,
connectionId?: string | null
): Promise<boolean> {
if (!branchName || data.headRefName === branchName) {
return true
}
if (connectionId || !data.headRefOid) {
return false
}
try {
const { stdout } = await gitExecFileAsync(['rev-parse', 'HEAD'], { cwd: repoPath })
return stdout.trim() === data.headRefOid
} catch {
return false
}
}
function isNotFoundGhError(err: unknown): boolean {
const stderr = err instanceof Error ? err.message : String(err)
return classifyGhError(stderr).type === 'not_found'
}
function shouldStopAfterExactLookupError(err: unknown): boolean {
const stderr = err instanceof Error ? err.message : String(err)
const type = classifyGhError(stderr).type
return type === 'rate_limited' || type === 'permission_denied' || type === 'network_error'
}
/**
* Get PR info for a given branch using gh CLI.
* Returns null if gh is not installed, or no PR exists for the branch.
*
* When `linkedPRNumber` is provided and the branch lookup yields nothing,
* falls back to looking up the PR by number. This handles "create from PR"
* worktrees, whose branch is a fresh local branch (not the PR's head ref) —
* the branch-keyed lookup misses, but the user still expects the linked PR
* to surface on the worktree card.
* When `linkedPRNumber` is provided and the repo identity is known, starts
* with a direct PR-number lookup. This handles "create from PR" worktrees,
* whose branch is a fresh local branch, and avoids spending a branch-list
* request before asking for the exact PR the worktree already stores.
*/
export async function getPRForBranch(
repoPath: string,
@@ -1332,11 +1396,22 @@ export async function getPRForBranch(
try {
const ownerRepo = await getOwnerRepo(repoPath, connectionId)
let data: PullRequestLookupData | null = null
let exactLinkedData: PullRequestLookupData | null = null
if (ownerRepo && typeof linkedPRNumber === 'number') {
data = await getPRByNumber(ownerRepo, linkedPRNumber, ghOptions)
if (data && !(await exactPRMatchesWorktreeHead(repoPath, branchName, data, connectionId))) {
// Why: linked PR metadata is user-editable. If the stored number still
// resolves but no longer matches this worktree, let branch lookup correct it.
exactLinkedData = data
data = null
}
}
// During a rebase the worktree is in detached HEAD and branch is empty.
// An empty --head filter causes gh to return an arbitrary PR — skip the
// branch lookup and rely on the linkedPR fallback below if available.
if (branchName) {
if (!data && branchName) {
if (ownerRepo) {
try {
const { stdout } = await ghExecFileAsync(
@@ -1375,33 +1450,24 @@ export async function getPRForBranch(
}
}
if (!data && typeof linkedPRNumber === 'number') {
const args = ownerRepo
? [
'pr',
'view',
String(linkedPRNumber),
'--repo',
`${ownerRepo.owner}/${ownerRepo.repo}`,
'--json',
PR_LOOKUP_JSON_FIELDS
]
: ['pr', 'view', String(linkedPRNumber), '--json', PR_LOOKUP_JSON_FIELDS]
if (!data && !ownerRepo && typeof linkedPRNumber === 'number') {
const args = ['pr', 'view', String(linkedPRNumber), '--json', PR_LOOKUP_JSON_FIELDS]
try {
const { stdout } = await ghExecFileAsync(args, ghOptions)
data = JSON.parse(stdout)
} catch (err) {
} catch {
// Why: a stale linkedPRNumber (PR deleted, wrong repo, …) makes
// `gh pr view <number>` reject. Treat that as the no-PR case so
// callers see the historical `null` semantics instead of a thrown
// error every poll cycle.
data =
ownerRepo && !isNotFoundGhError(err)
? await getRestPRByNumber(ownerRepo, linkedPRNumber, ghOptions)
: null
data = null
}
}
if (!data && exactLinkedData) {
data = exactLinkedData
}
if (!data) {
return null
}