Files
orca/src/main/git/source-control/git-pathspec.ts
T

92 lines
3.8 KiB
TypeScript

import {
commandLineLength,
MAX_COMMAND_LINE_CHARS
} from '../../../shared/windows-command-line-budget'
import { resolveGitCommandWithoutProbe } from '../command-runner/git-command-resolution'
import type { GitRuntimeOptions } from '../git-runtime-options'
/** Ceiling on argv entries per invocation; under WSL the byte budget bites first. */
const BULK_CHUNK_SIZE = 100
/**
* POSIX hosts have no CreateProcess cap: ARG_MAX is 256KB on macOS and 2MB on
* Linux, shared with the environment block. Half the macOS floor keeps a native
* or SSH-host invocation clear of E2BIG without charging it the WSL wrapper's
* quoting overhead, which is a different transport's problem.
*/
const POSIX_COMMAND_LINE_BUDGET = 128_000
export function literalPathspec(filePath: string, options: GitRuntimeOptions): string {
// Why: Git inside WSL needs POSIX paths, but host paths must stay literal, so convert backslashes only for WSL.
const runtimePath = options.wslDistro ? filePath.replace(/\\/g, '/') : filePath
return `:(literal)${runtimePath}`
}
/**
* Length of the line the OS will actually be handed, wrapper included.
*
* Why resolve rather than estimate: a WSL-routed write goes through the login
* shell, which shell-quotes every pathspec, quotes the resulting command line
* again, and embeds that three times (one branch per guest shell). The finished
* line runs ~3.4x the raw pathspec bytes, and nothing about the path list says
* so. Writes never take the direct-git lane, so this is the exact shape they
* get; a read that does take it resolves shorter, so the estimate stays safe.
*/
function finishedCommandLineLength(
args: readonly string[],
worktreePath: string,
options: GitRuntimeOptions
): number {
const resolved = resolveGitCommandWithoutProbe([...args], {
cwd: worktreePath,
...(options.wslDistro ? { wslDistro: options.wslDistro } : {})
})
return commandLineLength([resolved.binary, ...resolved.args])
}
/**
* Split a bulk pathspec operation into invocations the host can actually spawn.
*
* Why a byte budget and not a path count: 100 was chosen against a raw argv, but
* a WSL-routed `git add` is folded into one login-shell command line, so 100
* ordinary paths reached ~43,000 characters -- past the 32,767 CreateProcess cap
* -- and the bulk stage failed with nothing staged. Cost is measured per
* pathspec through the real resolver so the wrapper's quoting rules live in one
* place.
*
* Chunks split only between whole pathspecs, and a pathspec that alone exceeds
* the budget still ships alone rather than being dropped or truncated. If an
* invocation fails partway through, the earlier chunks stay applied: every
* operation here is idempotent and per-path, so `git status` shows the true
* state and re-running converges.
*/
export function bulkPathspecCommands(
leadingArgs: readonly string[],
filePaths: readonly string[],
worktreePath: string,
options: GitRuntimeOptions
): string[][] {
// Budget belongs to the host that spawns; the overhead measured above belongs to the transport.
const budget = process.platform === 'win32' ? MAX_COMMAND_LINE_CHARS : POSIX_COMMAND_LINE_BUDGET
const baseLength = finishedCommandLineLength(leadingArgs, worktreePath, options)
const commands: string[][] = []
let pathspecs: string[] = []
let length = baseLength
for (const filePath of filePaths) {
const pathspec = literalPathspec(filePath, options)
const cost =
finishedCommandLineLength([...leadingArgs, pathspec], worktreePath, options) - baseLength
if (pathspecs.length > 0 && (pathspecs.length >= BULK_CHUNK_SIZE || length + cost > budget)) {
commands.push([...leadingArgs, ...pathspecs])
pathspecs = []
length = baseLength
}
pathspecs.push(pathspec)
length += cost
}
if (pathspecs.length > 0) {
commands.push([...leadingArgs, ...pathspecs])
}
return commands
}