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 { 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 { 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 { 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 { 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 { 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 { 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 { 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) } }