mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
fix(wsl): bound the line-ending sample's index scan, not just its matches (#29552)
* fix(wsl): bound the line-ending sample's index scan, not just its matches The WSL-on-Windows-drive line-ending sample piped ls-files --eol through grep then head, so when no entry was eligible (* text=auto) head never closed the pipe and every file was read over drvfs until the 15 s timeout, which stage, discard and commit waited on after each backoff. Cap the raw ls-files output before the grep. Follow-up to #27778. * docs(wsl): describe the line-ending sample as the first index entries
This commit is contained in:
@@ -37,8 +37,11 @@ export function writesAnotherCheckout(args: readonly string[]): boolean {
|
||||
return subcommandIndex !== -1 && OTHER_CHECKOUT_SUBCOMMANDS.has(args[subcommandIndex])
|
||||
}
|
||||
|
||||
/** Deliberate cap: ~200 eligible files decide the checkout without reading the whole tree over drvfs. */
|
||||
/** Deliberate cap: ~200 eligible files decide the checkout. */
|
||||
export const LINE_ENDING_SAMPLE_LIMIT = 200
|
||||
// Why: `ls-files --eol` reads every file it lists over drvfs, so capping matches alone still reads
|
||||
// the whole tree when few files qualify (`* text=auto`); the scan stops after this many entries.
|
||||
export const LINE_ENDING_SCAN_LIMIT = 10 * LINE_ENDING_SAMPLE_LIMIT
|
||||
// Fewer eligible files than this is no evidence either way.
|
||||
export const LINE_ENDING_MIN_EVIDENCE = 5
|
||||
// Why a TTL: a re-clone at the same path can change the answer.
|
||||
@@ -129,8 +132,8 @@ export function parseHostAutocrlf(showOriginOutput: string): 'true' | 'other' {
|
||||
/**
|
||||
* Classifies a bounded `ls-files --eol` sample of LF blobs with no eol attribute.
|
||||
*
|
||||
* Whole-tree rather than top-level: root files are the ones formatters re-save as LF, the ones
|
||||
* attributes pin, and absent in some repos. Majority of CRLF working files means Windows Git
|
||||
* The first index entries, not just top-level files: root files are the ones formatters re-save
|
||||
* as LF, the ones attributes pin, and absent in some repos. Majority of CRLF working files means Windows Git
|
||||
* (autocrlf=true) wrote this checkout; one CRLF README in a Linux checkout does not.
|
||||
*/
|
||||
export function classifyLineEndingSample(
|
||||
|
||||
@@ -250,13 +250,13 @@ describe('pendingWslWindowsCheckoutGitConfig', () => {
|
||||
await expect(waiting).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('samples the whole tree through argv, bounded by a deliberate cap', async () => {
|
||||
it('passes the sample cap and the index-entry scan cap through argv', async () => {
|
||||
const { deps, run } = harness()
|
||||
await run(WRITE)
|
||||
expect(deps.runWsl).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
distro: 'Ubuntu',
|
||||
args: ['/mnt/c/Users/dev/repo', '200'],
|
||||
args: ['/mnt/c/Users/dev/repo', '200', '2000'],
|
||||
loginPath: 'preferred'
|
||||
})
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
isFresh,
|
||||
isWslWindowsCheckoutCandidate,
|
||||
LINE_ENDING_SAMPLE_LIMIT,
|
||||
LINE_ENDING_SCAN_LIMIT,
|
||||
parseHostAutocrlf,
|
||||
recordHostAutocrlf,
|
||||
recordLineEndingSample,
|
||||
@@ -29,11 +30,12 @@ const SAMPLE_PROBE_TIMEOUT_MS = 15_000
|
||||
const RETRY_BASE_MS = 30_000
|
||||
const RETRY_MAX_MS = 10 * 60_000
|
||||
const HOST_CONFIG_ARGS = ['config', '--show-origin', '--get-regexp', '^core\\.(autocrlf|eol)$']
|
||||
// Exit 3 carries rev-parse's own diagnostic; the pipeline is bounded by head, not by git.
|
||||
const SAMPLE_SCRIPT = [
|
||||
// Exit 3 carries rev-parse's own diagnostic. The first head bounds what git reads even when grep
|
||||
// matches nothing; the second bounds the sample.
|
||||
export const SAMPLE_SCRIPT = [
|
||||
'git -C "$1" rev-parse --is-inside-work-tree >/dev/null || exit 3',
|
||||
"tab=$(printf '\\t')",
|
||||
'git -C "$1" ls-files --eol | grep "^i/lf *w/[a-z]* *attr/ *$tab" | head -n "$2"'
|
||||
'git -C "$1" ls-files --eol | head -n "$3" | grep "^i/lf *w/[a-z]* *attr/ *$tab" | head -n "$2"'
|
||||
].join('\n')
|
||||
|
||||
export type HostGitResult = {
|
||||
@@ -159,7 +161,11 @@ async function probeSample(
|
||||
(deps.runWsl ?? runWslProcess)({
|
||||
distro: options.wslDistro,
|
||||
script: SAMPLE_SCRIPT,
|
||||
args: [translateArgForWsl(options.cwd), String(LINE_ENDING_SAMPLE_LIMIT)],
|
||||
args: [
|
||||
translateArgForWsl(options.cwd),
|
||||
String(LINE_ENDING_SAMPLE_LIMIT),
|
||||
String(LINE_ENDING_SCAN_LIMIT)
|
||||
],
|
||||
loginPath: 'preferred',
|
||||
timeoutMs: SAMPLE_PROBE_TIMEOUT_MS,
|
||||
maxOutputBytes: 1024 * 1024
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { SAMPLE_SCRIPT } from './wsl-windows-checkout-git-probe'
|
||||
import { LINE_ENDING_SAMPLE_LIMIT, LINE_ENDING_SCAN_LIMIT } from './wsl-windows-checkout-git-config'
|
||||
|
||||
const TAB = '\t'
|
||||
const TEXT_AUTO_LINE = `i/lf w/lf attr/text=auto ${TAB}file.txt`
|
||||
const ELIGIBLE_LINE = `i/lf w/crlf attr/ ${TAB}file.txt`
|
||||
|
||||
// Runs the probe's real shell pipeline against a stand-in git whose `ls-files --eol` never ends,
|
||||
// like a huge drvfs tree: only a bounded scan lets the pipeline finish.
|
||||
describe.skipIf(process.platform === 'win32')('line-ending sample pipeline', () => {
|
||||
let binDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
binDir = mkdtempSync(join(tmpdir(), 'orca-eol-sample-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(binDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function installEndlessGit(line: string): void {
|
||||
const fakeGit = join(binDir, 'git')
|
||||
writeFileSync(
|
||||
fakeGit,
|
||||
[
|
||||
'#!/bin/sh',
|
||||
'case "$3" in',
|
||||
' rev-parse) exit 0 ;;',
|
||||
` ls-files) exec yes '${line}' ;;`,
|
||||
'esac',
|
||||
'exit 1'
|
||||
].join('\n')
|
||||
)
|
||||
chmodSync(fakeGit, 0o755)
|
||||
}
|
||||
|
||||
function runSample() {
|
||||
return runProcess({
|
||||
program: '/bin/sh',
|
||||
args: [
|
||||
'-c',
|
||||
SAMPLE_SCRIPT,
|
||||
'sh',
|
||||
'/repo',
|
||||
String(LINE_ENDING_SAMPLE_LIMIT),
|
||||
String(LINE_ENDING_SCAN_LIMIT)
|
||||
],
|
||||
env: { ...process.env, PATH: `${binDir}${delimiter}${process.env.PATH ?? ''}` },
|
||||
timeoutMs: 10_000,
|
||||
maxOutputBytes: 1024 * 1024
|
||||
})
|
||||
}
|
||||
|
||||
it('stops reading the index when no entry is eligible (`* text=auto`)', async () => {
|
||||
installEndlessGit(TEXT_AUTO_LINE)
|
||||
const result = await runSample()
|
||||
expect(result.timedOut).toBe(false)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stdout).toBe('')
|
||||
})
|
||||
|
||||
it('still returns a full sample when entries are eligible', async () => {
|
||||
installEndlessGit(ELIGIBLE_LINE)
|
||||
const result = await runSample()
|
||||
expect(result.timedOut).toBe(false)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stdout.split('\n').filter(Boolean)).toHaveLength(LINE_ENDING_SAMPLE_LIMIT)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user