Files
orca/src/relay/git-handler-comparison-operations.ts
T
Neil 91a35b9257 Split relay Git handler responsibilities (#17217)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Split mobile host screen

* Retarget mobile view settings source test

* Split runtime file client layers

* Split ports panel layers

* Split runtime environments pane layers

* Split local PTY provider responsibilities

* Split CDP bridge responsibilities

* Split relay Git handler responsibilities

* Track moved relay Git fetch audit

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:19:09 -07:00

88 lines
3.8 KiB
TypeScript

import { GitHandlerOperationContext } from './git-handler-operation-context'
import { branchCompare as branchCompareOp } from './git-handler-ops'
import { commitCompare as commitCompareOp } from './git-handler-commit-diff-ops'
import { parseBranchDiff } from './git-handler-utils'
import { parseNumstat } from '../shared/git-uncommitted-line-stats'
import { isNoUpstreamError, normalizeGitErrorMessage } from '../shared/git-remote-error'
import { upstreamOnlyCommitsArePatchEquivalent } from '../shared/git-upstream-status'
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
import { getPublishTargetStatus, type GitCommandRunner } from '../shared/git-publish-target-status'
import type { GitPushTarget } from '../shared/worktree/types'
import { getEffectiveGitUpstreamStatus } from '../shared/git-effective-upstream'
export class GitHandlerComparisonOperations extends GitHandlerOperationContext {
async branchCompare(params: Record<string, unknown>) {
const worktreePath = params.worktreePath as string
const baseRef = params.baseRef as string
// Why: reject flag-like base refs to prevent rev-parse option injection.
if (baseRef.startsWith('-')) {
throw new Error('Base ref must not start with "-"')
}
const gitBound = this.git.bind(this)
return branchCompareOp(gitBound, worktreePath, baseRef, async (mergeBase, headOid) => {
// Why: preserve non-ASCII filenames as UTF-8 for parseBranchDiff.
const [{ stdout }, { stdout: numstat }] = await Promise.all([
gitBound(
['-c', 'core.quotePath=false', 'diff', '--name-status', '-M', '-C', mergeBase, headOid],
worktreePath
),
gitBound(
['-c', 'core.quotePath=false', 'diff', '--numstat', '-M', '-C', mergeBase, headOid],
worktreePath
)
])
return parseBranchDiff(stdout, parseNumstat(numstat))
})
}
async commitCompare(params: Record<string, unknown>) {
const worktreePath = params.worktreePath as string
const commitId = params.commitId as string
return commitCompareOp(this.git.bind(this), worktreePath, commitId)
}
async upstreamStatus(params: Record<string, unknown>) {
const worktreePath = params.worktreePath as string
try {
if (params.pushTarget !== undefined) {
assertGitPushTargetShape(params.pushTarget)
const pushTarget = params.pushTarget as GitPushTarget
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
return await getPublishTargetStatus(
((args) => this.git(args, worktreePath)) as GitCommandRunner,
pushTarget,
(upstreamName) => this.getBehindCommitsArePatchEquivalent(worktreePath, upstreamName)
)
}
return await getEffectiveGitUpstreamStatus(
(args) => this.git(args, worktreePath),
(upstreamName) => this.getBehindCommitsArePatchEquivalent(worktreePath, upstreamName)
)
} catch (error) {
// Why: suppress only the expected no-upstream error; surface all others.
if (isNoUpstreamError(error)) {
return { hasUpstream: false, ahead: 0, behind: 0 }
}
// Why: match fetch/push/pull normalization so execFile preamble and local paths don't leak to the renderer.
throw new Error(normalizeGitErrorMessage(error, 'upstream'))
}
}
private async getBehindCommitsArePatchEquivalent(
worktreePath: string,
upstreamName: string
): Promise<boolean> {
try {
const { stdout } = await this.git(
['log', '--oneline', '--cherry-mark', '--right-only', `HEAD...${upstreamName}`, '--'],
worktreePath
)
return upstreamOnlyCommitsArePatchEquivalent(stdout)
} catch {
// Why: this only identifies stale post-rebase upstreams; if the probe fails over SSH, keep the conservative pull-first sync path.
return false
}
}
}