mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* fix(runtime): PR/MR worktree base refuses SSH projects named only by executionHostId Codex review 6 #4: the guard read the legacy connectionId, so a row stamped executionHostId 'ssh:<target>' without it ran git fetch on this machine. * fix(runtime): one gate for local Git on a repo row; base resolution agrees with create Review follow-up: a local row with a stale connectionId and runtime: rows now refuse too (create already refused runtime: rows), through requireRepoLocalGitRoute in repo-execution-host.ts. * fix(runtime): keep base resolution for runtime-stamped rows this server owns Codex follow-up: older servers keep a client-minted runtime:<id> stamp on rows whose files are here, and base resolution worked for them before. requireRepoLocalGitRoute reads the stored host (getStoredRepoExecutionHostId), so they run locally again; SSH rows and any row with a connectionId still refuse.
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
import type { GitPushTarget } from '../../shared/worktree/types'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import { isFolderRepo } from '../../shared/repo-kind'
|
|
import { requireRepoLocalGitRoute } from '../repo-execution-host'
|
|
import { fetchCompareBaseRefWithLocalFallback } from '../git/compare-base-ref-fetch'
|
|
import { gitExecFileAsync } from '../git/runner'
|
|
import { getProjectRefForRemote, getWorkItemByProjectRef } from '../gitlab/client'
|
|
import { getGlabKnownHosts } from '../gitlab/gl-utils'
|
|
import {
|
|
getLocalProjectGitExecOptions,
|
|
getLocalProjectWorktreeGitOptions
|
|
} from '../project-runtime-git-options'
|
|
import type { ProjectRuntimeResolutionStore } from '../local-project-runtime-resolution'
|
|
import { resolveRuntimeGitLabForkMrBase } from './runtime-gitlab-fork-mr-base'
|
|
import { resolveRuntimeGitLabIssueSourceRemote } from './runtime-gitlab-issue-source-remote'
|
|
|
|
export type RuntimeGitLabWorktreeBaseArgs = {
|
|
repoSelector: string
|
|
mrIid: number
|
|
sourceBranch?: string
|
|
targetBranch?: string
|
|
isCrossRepository?: boolean
|
|
}
|
|
|
|
type RuntimeGitLabWorktreeBaseResult =
|
|
| { baseBranch: string; compareBaseRef?: string; pushTarget?: GitPushTarget }
|
|
| { error: string }
|
|
|
|
export async function resolveRuntimeGitLabWorktreeBase(
|
|
args: RuntimeGitLabWorktreeBaseArgs,
|
|
deps: {
|
|
store: ProjectRuntimeResolutionStore | null
|
|
resolveRepo: (selector: string) => Promise<Repo>
|
|
}
|
|
): Promise<RuntimeGitLabWorktreeBaseResult> {
|
|
if (!deps.store) {
|
|
throw new Error('runtime_unavailable')
|
|
}
|
|
let repo: Repo
|
|
try {
|
|
repo = await deps.resolveRepo(args.repoSelector)
|
|
} catch {
|
|
return { error: 'Repo not found' }
|
|
}
|
|
if (isFolderRepo(repo)) {
|
|
return { error: 'Folder mode does not support creating worktrees.' }
|
|
}
|
|
// Why the shared gate: a row may name its SSH host only as `executionHostId`.
|
|
requireRepoLocalGitRoute(repo)
|
|
const localGitExecOptions = getLocalProjectGitExecOptions(deps.store, repo)
|
|
const localWorktreeGitOptions = getLocalProjectWorktreeGitOptions(deps.store, repo)
|
|
const gitExec = (gitArgs: string[]) => gitExecFileAsync(gitArgs, localGitExecOptions)
|
|
let sourceBranch = args.sourceBranch?.trim() ?? ''
|
|
let targetBranch = args.targetBranch?.trim() ?? ''
|
|
let isCrossRepository = args.isCrossRepository === true
|
|
if (!sourceBranch) {
|
|
let discoveryRemote: string
|
|
try {
|
|
discoveryRemote = await resolveRuntimeGitLabIssueSourceRemote(
|
|
repo.path,
|
|
repo.issueSourcePreference,
|
|
localWorktreeGitOptions
|
|
)
|
|
} catch (error) {
|
|
return { error: error instanceof Error ? error.message : 'Could not resolve git remote.' }
|
|
}
|
|
const knownHosts = await getGlabKnownHosts(null, localWorktreeGitOptions)
|
|
const projectRef = await getProjectRefForRemote(
|
|
repo.path,
|
|
discoveryRemote,
|
|
knownHosts,
|
|
null,
|
|
localWorktreeGitOptions
|
|
)
|
|
if (!projectRef) {
|
|
return { error: 'No GitLab project found for this repository.' }
|
|
}
|
|
const item = await getWorkItemByProjectRef(
|
|
repo.path,
|
|
projectRef,
|
|
args.mrIid,
|
|
'mr',
|
|
null,
|
|
localWorktreeGitOptions
|
|
)
|
|
if (!item || item.type !== 'mr') {
|
|
return { error: `MR !${args.mrIid} not found.` }
|
|
}
|
|
sourceBranch = (item.branchName ?? '').trim()
|
|
targetBranch = (item.baseRefName ?? '').trim()
|
|
if (!sourceBranch) {
|
|
return { error: `MR !${args.mrIid} has no source branch.` }
|
|
}
|
|
if (item.isCrossRepository === true) {
|
|
isCrossRepository = true
|
|
}
|
|
}
|
|
let remote: string
|
|
try {
|
|
remote = await resolveRuntimeGitLabIssueSourceRemote(
|
|
repo.path,
|
|
repo.issueSourcePreference,
|
|
localWorktreeGitOptions
|
|
)
|
|
} catch (error) {
|
|
return { error: error instanceof Error ? error.message : 'Could not resolve git remote.' }
|
|
}
|
|
const compareBaseRef = targetBranch ? `refs/remotes/${remote}/${targetBranch}` : undefined
|
|
const fetchRemoteTrackingRef = async (branch: string, ref: string): Promise<void> => {
|
|
await gitExec(['fetch', remote, `+refs/heads/${branch}:${ref}`])
|
|
}
|
|
const fetchCompareBaseRef = (): Promise<boolean> =>
|
|
fetchCompareBaseRefWithLocalFallback({
|
|
compareBaseRef,
|
|
fetchCompareBaseRef: (ref) => fetchRemoteTrackingRef(targetBranch, ref),
|
|
gitExec,
|
|
logLabel: '[runtime:resolveManagedMrBase]',
|
|
logContext: { remote, targetBranch, mrIid: args.mrIid }
|
|
})
|
|
if (isCrossRepository) {
|
|
return resolveRuntimeGitLabForkMrBase({
|
|
repo,
|
|
remote,
|
|
mrIid: args.mrIid,
|
|
localGitExecOptions,
|
|
gitExec,
|
|
fetchCompareBaseRef,
|
|
compareBaseRef
|
|
})
|
|
}
|
|
try {
|
|
await fetchRemoteTrackingRef(sourceBranch, `refs/remotes/${remote}/${sourceBranch}`)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return { error: `Failed to fetch ${remote}/${sourceBranch}: ${message.split('\n')[0]}` }
|
|
}
|
|
const remoteRef = `${remote}/${sourceBranch}`
|
|
try {
|
|
await gitExec(['rev-parse', '--verify', remoteRef])
|
|
} catch {
|
|
return { error: `Remote ref ${remoteRef} does not exist after fetch.` }
|
|
}
|
|
const compareBaseFetched = await fetchCompareBaseRef()
|
|
return {
|
|
baseBranch: remoteRef,
|
|
...(compareBaseFetched ? { compareBaseRef } : {}),
|
|
pushTarget: { remoteName: remote, branchName: sourceBranch }
|
|
}
|
|
}
|