Files
orca/src/main/runtime/orca-runtime-git.ts
T
2026-05-14 23:13:37 -07:00

309 lines
11 KiB
TypeScript

import type {
GitBranchCompareResult,
GitConflictOperation,
GitDiffResult,
GitPushTarget,
GitStatusResult,
GitUpstreamStatus,
GitWorktreeInfo,
Worktree
} from '../../shared/types'
import { getRemoteFileUrl } from '../git/repo'
import {
bulkStageFiles,
bulkUnstageFiles,
commitChanges,
detectConflictOperation,
discardChanges,
getBranchCompare,
getBranchDiff,
getDiff,
getStatus as getGitStatus,
stageFile,
unstageFile
} from '../git/status'
import { getUpstreamStatus } from '../git/upstream'
import { gitFetch, gitPull, gitPush } from '../git/remote'
import { getSshGitProvider } from '../providers/ssh-git-dispatch'
import { normalizeRuntimeRelativePath } from './runtime-relative-paths'
export type ResolvedRuntimeGitWorktree = Worktree & { git: GitWorktreeInfo }
export type RuntimeGitCommandHost = {
resolveRuntimeGitTarget(
selector: string
): Promise<{ worktree: ResolvedRuntimeGitWorktree; connectionId?: string }>
}
export class RuntimeGitCommands {
constructor(private readonly host: RuntimeGitCommandHost) {}
async getRuntimeGitStatus(worktreeSelector: string): Promise<GitStatusResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.getStatus(target.worktree.path)
}
return getGitStatus(target.worktree.path)
}
async getRuntimeGitConflictOperation(worktreeSelector: string): Promise<GitConflictOperation> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.detectConflictOperation(target.worktree.path)
}
return detectConflictOperation(target.worktree.path)
}
async getRuntimeGitDiff(
worktreeSelector: string,
filePath: string,
staged: boolean,
compareAgainstHead?: boolean
): Promise<GitDiffResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePath = normalizeRuntimeRelativePath(filePath)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.getDiff(target.worktree.path, relativePath, staged, compareAgainstHead)
}
return getDiff(target.worktree.path, relativePath, staged, compareAgainstHead)
}
async getRuntimeGitBranchCompare(
worktreeSelector: string,
baseRef: string
): Promise<GitBranchCompareResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.getBranchCompare(target.worktree.path, baseRef)
}
return getBranchCompare(target.worktree.path, baseRef)
}
async getRuntimeGitUpstreamStatus(worktreeSelector: string): Promise<GitUpstreamStatus> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.getUpstreamStatus(target.worktree.path)
}
return getUpstreamStatus(target.worktree.path)
}
async fetchRuntimeGit(worktreeSelector: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.fetchRemote(target.worktree.path)
return { ok: true }
}
await gitFetch(target.worktree.path)
return { ok: true }
}
async pullRuntimeGit(worktreeSelector: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.pullBranch(target.worktree.path)
return { ok: true }
}
await gitPull(target.worktree.path)
return { ok: true }
}
async pushRuntimeGit(
worktreeSelector: string,
publish?: boolean,
pushTarget?: GitPushTarget
): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.pushBranch(target.worktree.path, publish === true, pushTarget)
return { ok: true }
}
await gitPush(target.worktree.path, publish === true, pushTarget)
return { ok: true }
}
async getRuntimeGitBranchDiff(
worktreeSelector: string,
compare: { mergeBase: string; headOid: string },
filePath: string,
oldPath?: string
): Promise<GitDiffResult> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePath = normalizeRuntimeRelativePath(filePath)
const oldRelativePath = oldPath ? normalizeRuntimeRelativePath(oldPath) : undefined
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
const results = await provider.getBranchDiff(target.worktree.path, compare.mergeBase, {
includePatch: true,
filePath: relativePath,
oldPath: oldRelativePath
})
return (
results[0] ?? {
kind: 'text',
originalContent: '',
modifiedContent: '',
originalIsBinary: false,
modifiedIsBinary: false
}
)
}
return getBranchDiff(target.worktree.path, {
mergeBase: compare.mergeBase,
headOid: compare.headOid,
filePath: relativePath,
oldPath: oldRelativePath
})
}
async commitRuntimeGit(
worktreeSelector: string,
message: string
): Promise<{ success: boolean; error?: string }> {
if (message.trim().length === 0) {
throw new Error('Commit message is required')
}
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.commit(target.worktree.path, message)
}
return commitChanges(target.worktree.path, message)
}
async stageRuntimeGitPath(worktreeSelector: string, filePath: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePath = normalizeRuntimeRelativePath(filePath)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.stageFile(target.worktree.path, relativePath)
return { ok: true }
}
await stageFile(target.worktree.path, relativePath)
return { ok: true }
}
async unstageRuntimeGitPath(worktreeSelector: string, filePath: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePath = normalizeRuntimeRelativePath(filePath)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.unstageFile(target.worktree.path, relativePath)
return { ok: true }
}
await unstageFile(target.worktree.path, relativePath)
return { ok: true }
}
async bulkStageRuntimeGitPaths(
worktreeSelector: string,
filePaths: string[]
): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePaths = filePaths.map((path) => normalizeRuntimeRelativePath(path))
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.bulkStageFiles(target.worktree.path, relativePaths)
return { ok: true }
}
await bulkStageFiles(target.worktree.path, relativePaths)
return { ok: true }
}
async bulkUnstageRuntimeGitPaths(
worktreeSelector: string,
filePaths: string[]
): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePaths = filePaths.map((path) => normalizeRuntimeRelativePath(path))
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.bulkUnstageFiles(target.worktree.path, relativePaths)
return { ok: true }
}
await bulkUnstageFiles(target.worktree.path, relativePaths)
return { ok: true }
}
async discardRuntimeGitPath(worktreeSelector: string, filePath: string): Promise<{ ok: true }> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const relativePath = normalizeRuntimeRelativePath(filePath)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
await provider.discardChanges(target.worktree.path, relativePath)
return { ok: true }
}
await discardChanges(target.worktree.path, relativePath)
return { ok: true }
}
async getRuntimeGitRemoteFileUrl(
worktreeSelector: string,
relativePath: string,
line: number
): Promise<string | null> {
const target = await this.host.resolveRuntimeGitTarget(worktreeSelector)
const normalizedRelativePath = normalizeRuntimeRelativePath(relativePath)
const provider = target.connectionId ? getSshGitProvider(target.connectionId) : null
if (target.connectionId) {
if (!provider) {
throw new Error('remote_git_unavailable')
}
return provider.getRemoteFileUrl(target.worktree.path, normalizedRelativePath, line)
}
return getRemoteFileUrl(target.worktree.path, normalizedRelativePath, line)
}
}