mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
* 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
177 lines
6.9 KiB
TypeScript
177 lines
6.9 KiB
TypeScript
import * as path from 'node:path'
|
|
import type { RequestContext } from './dispatcher'
|
|
import { GitHandlerOperationContext } from './git-handler-operation-context'
|
|
import { getStatusOp } from './git-handler-status-ops'
|
|
import { streamRelayGitStdout } from './git-stdout-stream'
|
|
import { capGitStatusEntries, resolveGitStatusLimit } from '../shared/git-status-limit'
|
|
import {
|
|
buildSubmoduleInnerCommitRangeDiff,
|
|
computeSubmodulePointerDiff,
|
|
computeSubmoduleRangeEntries,
|
|
findContainingSubmodule,
|
|
listSubmodulePathsCached,
|
|
resolveSubmoduleWorktreePath,
|
|
resolveSubmoduleCommitRange
|
|
} from './git-handler-submodule-ops'
|
|
import { computeDiff, type GitExec } from './git-handler-ops'
|
|
import { checkIgnoredPathsOp } from './git-handler-check-ignore'
|
|
import { loadGitHistoryFromExecutor } from '../shared/git-history'
|
|
import { stableInFlightKey } from '../shared/in-flight-promise-dedupe'
|
|
|
|
function resolveSubmoduleStatusArea(
|
|
params: Record<string, unknown>
|
|
): 'staged' | 'unstaged' | 'untracked' {
|
|
if (params.area === 'staged' || params.area === 'unstaged' || params.area === 'untracked') {
|
|
return params.area
|
|
}
|
|
return 'unstaged'
|
|
}
|
|
|
|
export class GitHandlerReadOperations extends GitHandlerOperationContext {
|
|
async getStatus(params: Record<string, unknown>, context: RequestContext) {
|
|
this.gitDiffReadDedupe.clear()
|
|
return getStatusOp(this.git.bind(this), streamRelayGitStdout, params, {
|
|
signal: context.signal
|
|
})
|
|
}
|
|
|
|
// Why: fetch per-file submodule changes from the submodule worktree.
|
|
async getSubmoduleStatus(params: Record<string, unknown>, context: RequestContext) {
|
|
const worktreePath = params.worktreePath as string
|
|
const submodulePath = params.submodulePath as string
|
|
const area = resolveSubmoduleStatusArea(params)
|
|
const staged = area === 'staged'
|
|
const resolved = resolveSubmoduleWorktreePath(worktreePath, submodulePath)
|
|
const limit = resolveGitStatusLimit(params.limit)
|
|
// Why: staged expansion only represents HEAD→index; scanning the submodule worktree is wasted work.
|
|
const workingResult = staged
|
|
? { entries: [], conflictOperation: 'unknown' }
|
|
: await getStatusOp(
|
|
this.git.bind(this),
|
|
streamRelayGitStdout,
|
|
{
|
|
...params,
|
|
worktreePath: resolved
|
|
},
|
|
{ signal: context.signal }
|
|
)
|
|
// Why: pointer/range probes are part of the same SSH request and must not outlive its cancellation.
|
|
const requestGit: GitExec = (args, cwd, options) =>
|
|
this.git(args, cwd, { ...options, signal: context.signal })
|
|
// Why: moved clean gitlinks need committed changes surfaced.
|
|
const { fromOid, toOid } = await resolveSubmoduleCommitRange(
|
|
requestGit,
|
|
worktreePath,
|
|
submodulePath,
|
|
staged
|
|
)
|
|
if (fromOid && toOid && fromOid !== toOid) {
|
|
const rangeEntries = await computeSubmoduleRangeEntries(requestGit, resolved, fromOid, toOid)
|
|
if (staged) {
|
|
return { ...workingResult, ...capGitStatusEntries(rangeEntries, limit) }
|
|
}
|
|
const rangePaths = new Set(rangeEntries.map((entry) => entry.path))
|
|
const entries = [
|
|
...rangeEntries,
|
|
...workingResult.entries.filter((entry) => !rangePaths.has(entry.path))
|
|
]
|
|
return {
|
|
...workingResult,
|
|
...capGitStatusEntries(entries, limit, workingResult)
|
|
}
|
|
}
|
|
if (staged) {
|
|
return { ...workingResult, entries: [] }
|
|
}
|
|
return workingResult
|
|
}
|
|
|
|
async checkIgnored(params: Record<string, unknown>) {
|
|
return checkIgnoredPathsOp(this.git.bind(this), params)
|
|
}
|
|
|
|
async history(params: Record<string, unknown>) {
|
|
const worktreePath = params.worktreePath as string
|
|
return loadGitHistoryFromExecutor(this.git.bind(this), worktreePath, {
|
|
limit: typeof params.limit === 'number' ? params.limit : undefined,
|
|
baseRef: typeof params.baseRef === 'string' ? params.baseRef : null
|
|
})
|
|
}
|
|
|
|
async getDiff(params: Record<string, unknown>, context?: RequestContext) {
|
|
const worktreePath = params.worktreePath as string
|
|
const filePath = params.filePath as string
|
|
// Why: validate relative paths to prevent traversal outside the worktree.
|
|
const resolved = path.resolve(worktreePath, filePath)
|
|
const rel = path.relative(path.resolve(worktreePath), resolved)
|
|
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
throw new Error(`Path "${filePath}" resolves outside the worktree`)
|
|
}
|
|
const staged = params.staged as boolean
|
|
const compareAgainstHead = params.compareAgainstHead as boolean | undefined
|
|
// Why: register dedupe before awaiting so identical reads coalesce.
|
|
const result = await this.gitDiffReadDedupe.run(
|
|
stableInFlightKey(['diff', worktreePath, filePath, staged, compareAgainstHead]),
|
|
async () => {
|
|
// Why: route gitlink roots to pointer diffs and inner files to their submodule worktree.
|
|
const submodulePaths = await listSubmodulePathsCached(
|
|
this.git.bind(this),
|
|
worktreePath,
|
|
this.submodulePathsCache
|
|
)
|
|
if (submodulePaths.length > 0) {
|
|
const matchedSubmodule = findContainingSubmodule(submodulePaths, filePath)
|
|
if (matchedSubmodule) {
|
|
const normalizedFilePath = filePath.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
if (normalizedFilePath === matchedSubmodule) {
|
|
return computeSubmodulePointerDiff(
|
|
this.git.bind(this),
|
|
worktreePath,
|
|
matchedSubmodule,
|
|
staged,
|
|
compareAgainstHead
|
|
)
|
|
}
|
|
const submoduleWorktreePath = resolveSubmoduleWorktreePath(
|
|
worktreePath,
|
|
matchedSubmodule
|
|
)
|
|
const innerPath = normalizedFilePath.slice(matchedSubmodule.length + 1)
|
|
const { fromOid, toOid } = await resolveSubmoduleCommitRange(
|
|
this.git.bind(this),
|
|
worktreePath,
|
|
matchedSubmodule,
|
|
staged
|
|
)
|
|
// Why: a moved gitlink (clean worktree) keeps inner changes in committed history, so diff the two commits; otherwise read the working-tree blob.
|
|
if (fromOid && toOid && fromOid !== toOid) {
|
|
return buildSubmoduleInnerCommitRangeDiff(
|
|
this.gitBuffer.bind(this),
|
|
submoduleWorktreePath,
|
|
innerPath,
|
|
fromOid,
|
|
toOid
|
|
)
|
|
}
|
|
return computeDiff(
|
|
this.gitBuffer.bind(this),
|
|
submoduleWorktreePath,
|
|
innerPath,
|
|
staged,
|
|
compareAgainstHead
|
|
)
|
|
}
|
|
}
|
|
return computeDiff(
|
|
this.gitBuffer.bind(this),
|
|
worktreePath,
|
|
filePath,
|
|
staged,
|
|
compareAgainstHead
|
|
)
|
|
}
|
|
)
|
|
return this.maybeStreamResponse(result, params, context)
|
|
}
|
|
}
|