Files
orca/src/main/github/client/fetch/get-work-item.ts
T
NeilandKatsuma Takehisa ce07786266 Keep opened issue details and edits in the selected repository (#24729)
Keeps a fork issue’s details, metadata and edits bound to the repository the user opened, including same-number issues in fork and upstream. Repairs selected-assignee leakage and delayed failed edits repainting another issue.

Fixes #24378

Incorporates and cross-reviews contributor PR #24379, including its source-resolver correction and regression material. Covers the contributor PR’s Project-row identity and retained-dialog mutation findings. The final published head passes focused tests, hidden macOS rendering and current CI; the callback-timing bot thread has an evidence-based response.

Co-authored-by: Katsuma Takehisa <k.takehisa@nissogr.com>
2026-10-03 23:19:55 -07:00

178 lines
4.9 KiB
TypeScript

import type { IssueSourcePreference } from '../../../../shared/repo-types'
import { acquire, release, classifyGhError, type LocalGitExecOptions } from '../../gh-utils'
import {
resolveGitHubApiRepository,
resolveGitHubApiRepositoryCandidates,
resolveIssueGitHubApiRepositorySource,
type GitHubApiRepository
} from '../../github-api-repository'
import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key'
import type { MainWorkItem } from './../map/work-item-field-coercion'
import {
fetchIssueWorkItem,
fetchPullRequestWorkItem,
fetchPullRequestWorkItemFromCandidates
} from './work-item-fetch'
export async function getWorkItem(
repoPath: string,
number: number,
type?: 'issue' | 'pr',
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {},
preference?: IssueSourcePreference
): Promise<MainWorkItem | null> {
const { item } = await getWorkItemWithRepository(
repoPath,
number,
type,
connectionId,
localGitOptions,
preference
)
return item
}
export type WorkItemRepositoryLookup = {
item: MainWorkItem | null
repository: GitHubApiRepository | null
}
export async function getWorkItemWithRepository(
repoPath: string,
number: number,
type?: 'issue' | 'pr',
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {},
preference?: IssueSourcePreference
): Promise<WorkItemRepositoryLookup> {
await acquire()
try {
// Why: listWorkItems uses resolveIssueGitHubApiRepositorySource; open-by-number
// must share that preference so origin/upstream toggles cannot disagree.
if (type === 'issue') {
const { source } = await resolveIssueGitHubApiRepositorySource(
repoPath,
preference,
connectionId,
localGitOptions
)
// Why: explicit origin with no origin identity must not bare-lookup ambient gh
// (same fail-closed rule as origin-pinned PR candidate resolution).
if (!source && preference === 'origin') {
return { item: null, repository: null }
}
return {
item: await fetchIssueWorkItem(repoPath, source, number, connectionId, localGitOptions),
repository: source
}
}
if (type === 'pr') {
return {
item: await fetchPullRequestWorkItemFromCandidates(
repoPath,
number,
connectionId,
localGitOptions,
preference
),
repository: null
}
}
try {
const { source } = await resolveIssueGitHubApiRepositorySource(
repoPath,
preference,
connectionId,
localGitOptions
)
if (source || preference !== 'origin') {
const issue = await fetchIssueWorkItem(
repoPath,
source,
number,
connectionId,
localGitOptions
)
if (issue) {
return { item: issue, repository: source }
}
}
} catch (err) {
// Why: only fall through to PR #N on a genuine 404; re-throw transient errors so a flake can't surface an unrelated PR.
const stderr = err instanceof Error ? err.message : String(err)
if (classifyGhError(stderr).type !== 'not_found') {
throw err
}
}
return {
item: await fetchPullRequestWorkItemFromCandidates(
repoPath,
number,
connectionId,
localGitOptions,
preference
),
repository: null
}
} catch {
return { item: null, repository: null }
} finally {
release()
}
}
export async function getWorkItemByOwnerRepo(
repoPath: string,
ownerRepo: GitHubApiRepository,
number: number,
type: 'issue' | 'pr',
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<MainWorkItem | null> {
const requestedHost = ownerRepo.host?.trim().toLowerCase()
const requestedRepository = requestedHost
? { ...ownerRepo, host: requestedHost }
: await resolveGitHubApiRepository(repoPath, ownerRepo, connectionId, localGitOptions)
if (!requestedRepository) {
return null
}
const { candidates } = await resolveGitHubApiRepositoryCandidates(
repoPath,
connectionId,
localGitOptions
)
const requestedKey = githubRepoIdentityKey(requestedRepository)
const matchedRepository = candidates.find(
(candidate) => githubRepoIdentityKey(candidate) === requestedKey
)
// Why: this lookup is reachable from pasted links. Restricting it to a
// configured remote prevents gh from sending credentials to an arbitrary host.
if (!matchedRepository) {
return null
}
await acquire()
try {
if (type === 'issue') {
return await fetchIssueWorkItem(
repoPath,
matchedRepository,
number,
connectionId,
localGitOptions
)
}
return await fetchPullRequestWorkItem(
repoPath,
matchedRepository,
number,
connectionId,
localGitOptions
)
} catch {
return null
} finally {
release()
}
}