mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
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>
178 lines
4.9 KiB
TypeScript
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()
|
|
}
|
|
}
|