Files
orca/src/main/runtime/runtime-git-generation-commands.ts
T
Neil d456549c22 refactor runtime Git bridges (#16189)
* refactor runtime git bridges

* fix(test): avoid computed namespace import access in runtime Git client contract test
2026-08-25 00:54:43 -07:00

291 lines
11 KiB
TypeScript

import type { CommitMessageDraftContext } from '../../shared/commit-message-generation'
import { getCommitMessageModelDiscoveryHostKey } from '../../shared/commit-message-host-key'
import type { HostedReviewProvider } from '../../shared/hosted-review'
import { withLinkedIssueDraftContext } from '../../shared/source-control-ai-action-variables'
import type { TuiAgent } from '../../shared/tui-agent'
import { gitExecFileAsync } from '../git/runner'
import { getStagedCommitContext } from '../git/status'
import {
getSshGitProvider,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE
} from '../providers/ssh-git-dispatch'
import { loadPullRequestLinkedIssue } from '../source-control/pull-request-linked-issue'
import { resolveHostedReviewBodyForGeneration } from '../source-control/pull-request-template'
import { prepareLocalCommitMessageAgentEnv } from '../text-generation/commit-message-agent-environment'
import {
cancelGenerateCommitMessageLocal,
cancelGeneratePullRequestFieldsLocal,
discoverCommitMessageModelsLocal,
discoverCommitMessageModelsRemote,
generateCommitMessageFromContext,
generatePullRequestFieldsFromContext,
resolveCommitMessageSettings,
type DiscoverCommitMessageModelsResult,
type GenerateCommitMessageResult,
type GeneratePullRequestFieldsResult
} from '../text-generation/commit-message-text-generation'
import { getPullRequestDraftContext } from '../text-generation/pull-request-context'
import { localGitOptionsForTarget, type RuntimeGitCommandHost } from './runtime-git-command-target'
import {
getRuntimeGitGenerationSettings,
linkedIssueForTarget,
linkedIssueMetaForTarget,
localAgentRuntimeTargetForTarget,
localTextGenerationTargetForTarget,
type RuntimeCommitMessageSettingsOverride
} from './runtime-git-generation-context'
export class RuntimeGitGenerationCommands {
constructor(private readonly host: RuntimeGitCommandHost) {}
async generateRuntimeCommitMessage(
worktreeSelector: string,
settingsOverride?: RuntimeCommitMessageSettingsOverride
): Promise<GenerateCommitMessageResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const discoveryHostKey =
settingsOverride?.commitMessageDiscoveryHostKey ??
getCommitMessageModelDiscoveryHostKey(target.connectionId ?? null)
const resolvedSettings = settingsOverride?.sourceControlAiResolvedParams
? { ok: true as const, params: settingsOverride.sourceControlAiResolvedParams }
: resolveCommitMessageSettings(
getRuntimeGitGenerationSettings(
this.host.getRuntimeSettings(),
settingsOverride,
'commitMessage'
),
discoveryHostKey,
'commitMessage',
target.repo ?? null
)
if (!resolvedSettings.ok) {
return { success: false, error: resolvedSettings.error }
}
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
return { success: false, error: SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE }
}
let context: CommitMessageDraftContext | null
try {
context = await provider.getStagedCommitContext(target.worktree.path)
} catch (error) {
console.error('[runtime-git] Failed to read remote staged commit context:', error)
return { success: false, error: 'Failed to read staged changes.' }
}
if (!context) {
return { success: false, error: 'No staged changes to summarize.' }
}
context = withLinkedIssueDraftContext(context, linkedIssueForTarget(this.host, target))
return generateCommitMessageFromContext(context, resolvedSettings.params, {
kind: 'remote',
cwd: target.worktree.path,
execute: (plan, cwd, timeoutMs, operation) =>
provider.executeCommitMessagePlan(plan, cwd, timeoutMs, operation),
missingBinaryLocation: 'remote PATH'
})
}
let context: CommitMessageDraftContext | null
try {
context = await getStagedCommitContext(target.worktree.path, localGitOptionsForTarget(target))
} catch (error) {
console.error('[runtime-git] Failed to read staged commit context:', error)
return { success: false, error: 'Failed to read staged changes.' }
}
if (!context) {
return { success: false, error: 'No staged changes to summarize.' }
}
context = withLinkedIssueDraftContext(context, linkedIssueForTarget(this.host, target))
const localEnv = await prepareLocalCommitMessageAgentEnv(
resolvedSettings.params.agentId,
this.host.getCommitMessageAgentEnvironment?.(),
localAgentRuntimeTargetForTarget(target)
)
if (!localEnv.ok) {
return { success: false, error: localEnv.error }
}
return generateCommitMessageFromContext(
context,
resolvedSettings.params,
localTextGenerationTargetForTarget(target, localEnv.env)
)
}
async cancelRuntimeGenerateCommitMessage(worktreeSelector: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
await provider?.cancelGenerateCommitMessage(target.worktree.path, 'commit-message')
return { ok: true }
}
cancelGenerateCommitMessageLocal(target.worktree.path)
return { ok: true }
}
async generateRuntimePullRequestFields(
worktreeSelector: string,
input: {
base: string
title: string
body: string
draft: boolean
provider?: HostedReviewProvider
useTemplate?: boolean
},
settingsOverride?: RuntimeCommitMessageSettingsOverride
): Promise<GeneratePullRequestFieldsResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const discoveryHostKey =
settingsOverride?.commitMessageDiscoveryHostKey ??
getCommitMessageModelDiscoveryHostKey(target.connectionId ?? null)
const resolvedSettings = settingsOverride?.sourceControlAiResolvedParams
? { ok: true as const, params: settingsOverride.sourceControlAiResolvedParams }
: resolveCommitMessageSettings(
getRuntimeGitGenerationSettings(
this.host.getRuntimeSettings(),
settingsOverride,
'pullRequest'
),
discoveryHostKey,
'pullRequest',
target.repo ?? null
)
if (!resolvedSettings.ok) {
return { success: false, error: resolvedSettings.error }
}
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId && !provider) {
return { success: false, error: SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE }
}
const issueMeta = linkedIssueMetaForTarget(this.host, target)
const linkedIssueDetailsPromise = loadPullRequestLinkedIssue({
meta: issueMeta,
provider: input.provider,
repoPath: target.worktree.path,
connectionId: target.connectionId,
localGitOptions: localGitOptionsForTarget(target)
})
let context: Awaited<ReturnType<typeof getPullRequestDraftContext>>
try {
const currentBody = await resolveHostedReviewBodyForGeneration({
body: input.body,
repoPath: target.worktree.path,
connectionId: target.connectionId,
provider: input.provider,
useTemplate: input.useTemplate
})
context = target.connectionId
? await getPullRequestDraftContext((argv) => provider!.exec(argv, target.worktree.path), {
base: input.base,
currentTitle: input.title,
currentBody,
currentDraft: input.draft
})
: await getPullRequestDraftContext(
(argv, options) =>
gitExecFileAsync(argv, {
cwd: target.worktree.path,
...localGitOptionsForTarget(target),
...options
}),
{
base: input.base,
currentTitle: input.title,
currentBody,
currentDraft: input.draft
}
)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to prepare branch for PR details.'
}
}
if (!context) {
return { success: false, error: 'No branch changes to summarize.' }
}
const linkedIssueDetails = await linkedIssueDetailsPromise
context = {
...withLinkedIssueDraftContext(context, issueMeta?.linkedIssue),
...(input.provider ? { provider: input.provider } : {}),
...(linkedIssueDetails ? { linkedIssueDetails } : {})
}
if (target.connectionId) {
return generatePullRequestFieldsFromContext(context, resolvedSettings.params, {
kind: 'remote',
cwd: target.worktree.path,
execute: (plan, cwd, timeoutMs, operation) =>
provider!.executeCommitMessagePlan(plan, cwd, timeoutMs, operation),
missingBinaryLocation: 'remote PATH'
})
}
const localEnv = await prepareLocalCommitMessageAgentEnv(
resolvedSettings.params.agentId,
this.host.getCommitMessageAgentEnvironment?.(),
localAgentRuntimeTargetForTarget(target)
)
if (!localEnv.ok) {
return { success: false, error: localEnv.error }
}
return generatePullRequestFieldsFromContext(
context,
resolvedSettings.params,
localTextGenerationTargetForTarget(target, localEnv.env)
)
}
async cancelRuntimeGeneratePullRequestFields(worktreeSelector: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
await provider?.cancelGenerateCommitMessage(target.worktree.path, 'pull-request-fields')
return { ok: true }
}
cancelGeneratePullRequestFieldsLocal(target.worktree.path)
return { ok: true }
}
async discoverRuntimeCommitMessageModels(
worktreeSelector: string,
agentId: string,
settingsOverride?: Pick<RuntimeCommitMessageSettingsOverride, 'agentCmdOverrides'>
): Promise<DiscoverCommitMessageModelsResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const typedAgentId = agentId as TuiAgent
const agentCommandOverride =
settingsOverride?.agentCmdOverrides?.[typedAgentId] ??
this.host.getRuntimeSettings().agentCmdOverrides?.[typedAgentId]
if (target.connectionId) {
const provider = getSshGitProvider(target.connectionId)
if (!provider) {
return { success: false, error: `No git provider for connection "${target.connectionId}"` }
}
return discoverCommitMessageModelsRemote(
typedAgentId,
target.worktree.path,
(plan, cwd, timeoutMs) => provider.executeCommitMessagePlan(plan, cwd, timeoutMs),
agentCommandOverride
)
}
const localEnv = await prepareLocalCommitMessageAgentEnv(
typedAgentId,
this.host.getCommitMessageAgentEnvironment?.(),
localAgentRuntimeTargetForTarget(target)
)
if (!localEnv.ok) {
return { success: false, error: localEnv.error }
}
const localOptions = localGitOptionsForTarget(target)
return localOptions.wslDistro
? discoverCommitMessageModelsLocal(typedAgentId, localEnv.env, agentCommandOverride, {
cwd: target.worktree.path,
wslDistro: localOptions.wslDistro
})
: discoverCommitMessageModelsLocal(typedAgentId, localEnv.env, agentCommandOverride)
}
}