Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 86 additions & 45 deletions config/patches/@xterm__xterm@6.1.0-beta.303.patch

Large diffs are not rendered by default.

97 changes: 69 additions & 28 deletions config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ index 497afcf535f3eaca00889525a77e15eb633ccd96..96d499b34605f860608382114c3fbdc0

export interface IBrowser {
diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee3acfddfa 100644
index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..b2d99074a17a2197fc50091a1a2f760c27ece99f 100644
--- a/src/browser/input/CompositionHelper.ts
+++ b/src/browser/input/CompositionHelper.ts
@@ -1,10 +1,15 @@
Expand Down Expand Up @@ -413,7 +413,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
}

/**
@@ -113,7 +313,19 @@ export class CompositionHelper {
@@ -113,14 +313,32 @@ export class CompositionHelper {
* @returns Whether the Terminal should continue processing the keydown event.
*/
public keydown(ev: KeyboardEvent): boolean {
Expand All @@ -434,21 +434,42 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
if (ev.keyCode === 20 || ev.keyCode === 229) {
// 20 is CapsLock, 229 is Enter
// Continue composing if the keyCode is the "composition character"
@@ -128,6 +340,10 @@ export class CompositionHelper {
+ if (this._isImeModeToggleShift(ev)) {
+ this._imeKeydownAwaitingCommit = true;
+ }
return false;
}
if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
// Continue composing if the keyCode is a modifier key
+ if (this._isImeModeToggleShift(ev)) {
+ this._imeKeydownAwaitingCommit = true;
+ }
return false;
}
// Finish composition immediately. This is mainly here for the case where enter is
@@ -128,6 +346,10 @@ export class CompositionHelper {
this._finalizeComposition(false);
}

+ // Nothing is forwarded for a keydown the IME consumed, so it is left owing its commit; any
+ // other keydown either forwards its own text or produces none, and clears the debt.
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229;
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '420,535p' config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
sed -n '950,990p' config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
sed -n '1010,1070p' config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch

Repository: stablyai/orca

Length of output: 8574


🏁 Script executed:

#!/bin/bash
set -eu
file='config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch'
printf '%s\n' '--- CompositionHelper keydown/input/reconciliation ---'
sed -n '400,570p' "$file"
sed -n '760,880p' "$file"
printf '%s\n' '--- CoreBrowserTerminal event wiring ---'
rg -n -C 12 'compositionHelper\.(keydown|input|keypress|textarea|blur)|_inputEvent|_keyDown|insertText' 'config/patches/@xterm__xterm@6.1.0-beta.303.patch' "$file"

Repository: stablyai/orca

Length of output: 45546


🏁 Script executed:

#!/bin/bash
set -eu
file='config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch'
core='config/patches/@xterm__xterm@6.1.0-beta.303.patch'
printf '%s\n' '--- CompositionHelper state and reconciliation ---'
rg -n --no-heading --fixed-strings '_imeKeydownAwaitingCommit' "$file"
sed -n '780,875p' "$file"
printf '%s\n' '--- CoreBrowserTerminal keydown and input wiring ---'
sed -n '95,140p' "$core"
sed -n '1010,1065p' "$core"

Repository: stablyai/orca

Length of output: 8234


Prevent idle Shift from creating IME commit debt.

CoreBrowserTerminal._keyDown calls CompositionHelper.keydown for every textarea keydown. An idle IME-toggle Shift can set _imeKeydownAwaitingCommit. Textarea reconciliation clears this flag only when the textarea value changes, so a later unrelated insertText can be claimed as the pending IME commit.

Gate the Shift case on active composition or pending finalization.

Suggested fix
-    this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);
+    this._imeKeydownAwaitingCommit =
+      ev.keyCode === 229 ||
+      ((this._isComposing || this.hasPendingCompositionFinalization) &&
+        this._isImeModeToggleShift(ev));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);
this._imeKeydownAwaitingCommit =
ev.keyCode === 229 ||
((this._isComposing || this.hasPendingCompositionFinalization) &&
this._isImeModeToggleShift(ev));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arming the claim for an idle (non-composing) Shift keydown leaves it armed when the next keydown is bypassed by shouldBypassXtermKeyboardEvent (Shift + a single non-ASCII printable). That keydown never reaches CompositionHelper.keydown to clear the flag, so the input event's _claimImeKeydownCommit re-sends the glyph _keyPress already emitted — every Shift+<non-ASCII> keystroke (uppercase Cyrillic/Greek, etc.) types twice on Windows/Linux.

Technical details
# Idle-Shift claim survives a bypassed keydown and double-emits

## Affected sites
- `config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:456` — `_imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev)` arms the claim for an idle, non-composing Shift keydown.
- `src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts:315` — every `key:'Shift'` keydown now passes the modifier policy.
- `src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts:346-357` — a Shift + single non-ASCII printable keydown is bypassed before `CompositionHelper.keydown`, so nothing clears the claim.
- `node_modules/@xterm/xterm/src/browser/CoreBrowserTerminal.ts:869-871` — the bypass makes `_keyDown` return at the custom-handler check, never calling `CompositionHelper.keydown`.
- `CoreBrowserTerminal.ts:1031-1035` (`_keyPress` emits and sets `_keyPressHandled`) and the patched `_inputEvent` first branch (calls `_compositionHelper.input(ev.data)` before the `_keyPressHandled` dedup).

## Sequence
1. Idle `Shift` keydown → not suppressed by the IME policy (`isImeModeToggleShiftKey`) or the modifier policy → `CompositionHelper.keydown` sets `_imeKeydownAwaitingCommit = true`.
2. `Shift+Ф` keydown → `shouldBypassXtermKeyboardEvent` returns true → `_keyDown` returns before `CompositionHelper.keydown`; claim stays true.
3. `keypress` → `_keyPress` emits `Ф`, sets `_keyPressHandled = true`.
4. `input` `insertText:'Ф'` → `_inputEvent` first branch → `_claimImeKeydownCommit` sees the stale claim and emits `Ф` again.

## Required outcome
- A Shift keydown that does not begin an IME commit must not leave a claim a later `input` event consumes, so `Shift+<non-ASCII>` emits once. macOS is already shielded by the native-text forwarder; Windows/Linux are exposed.

## Suggested approach
- Arm `_imeKeydownAwaitingCommit` only for a Shift keydown while composing/finalizing (as the previous revision did), or clear the claim whenever a keydown is bypassed. Add an integration test that types `Shift` + a non-ASCII letter and asserts one emission (`terminal-ime-xterm-consumed-key-commit.test.ts:233` only covers the non-bypassed next keydown).

+
if (ev.keyCode === 229) {
// If the "composition character" is used but gets to this point it means a non-composition
// character (eg. numbers and punctuation) was pressed when the IME was active.
@@ -138,6 +354,74 @@ export class CompositionHelper {
return true;
}
@@ -135,6 +357,79 @@ export class CompositionHelper {
return false;
}

+ if (this._isImeModeToggleShift(ev)) {
+ // Consume idle/trailing Shift so kitty cannot encode it; the custom handler
+ // must still deliver the keydown or Sogou's delayed insertText is dropped.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Defers keypress text while a composition finalizer is pending so all input is emitted once
+ * after reconciliation with the final textarea candidate.
Expand Down Expand Up @@ -514,13 +535,10 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ this._textareaChangeTimer = undefined;
+ }
+ this._coreService.triggerDataEvent(text, true);
+ return true;
+ }
+
/**
* Finalizes the composition, resuming regular input actions. This is called when a composition
* is ending.
@@ -146,23 +430,52 @@ export class CompositionHelper {
return true;
}

@@ -146,23 +441,52 @@ export class CompositionHelper {
* compositionend event is triggered, such as enter, so that the composition is sent before
* the command is executed.
*/
Expand Down Expand Up @@ -584,7 +602,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee

// Since composition* events happen before the changes take place in the textarea on most
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
@@ -172,37 +485,315 @@ export class CompositionHelper {
@@ -172,37 +496,339 @@ export class CompositionHelper {
// - The last compositionupdate event's data property does not always accurately describe
// the character, a counter example being Korean where an ending consonsant can move to
// the following character if the following input is a vowel.
Expand Down Expand Up @@ -613,17 +631,39 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
- }
- if (input.length > 0) {
- this._coreService.triggerDataEvent(input, true);
- }
+ pending.finalizerTimer = this._defer(() => {
+ pending.finalizerTimer = undefined;
+ if (this._compositionTransactionId === pending.transactionId) {
+ this._isAwaitingCompositionEnd = false;
+ }
+ if (this._pendingComposition === pending) {
+ if (this._shouldWaitForHeldImeCommit(pending)) {
+ return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_shouldWaitForHeldImeCommit's early return leaves _imeKeydownAwaitingCommit set, and nothing on the flush path clears it. When the textarea is non-empty at compositionend, _updatePostCompositionInputExpectation sets expectsPostCompositionInput false, the finalizer flushes the preedit, and a later insertText is then claimed by _claimImeKeydownCommit and sent again.

Technical details
# Sticky `_imeKeydownAwaitingCommit` can duplicate a commit or swallow a cancel

## Affected sites
- `config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:635-636` — early return leaves `_pendingComposition` open with no timer.
- `...:920-921` — `_deferPreeditResync` refuses to cancel while the flag is set, so a key that empties the preedit later in the same composition no longer clears it.
- `_sendPendingComposition` / `_settlePendingComposition` (pre-existing) never reset the flag; `_claimImeKeydownCommit` (`:520-531`) then claims a later `insertText`.

## Sequence (double commit)
1. `compositionstart`; `compositionupdate('s')`; textarea `'s'`.
2. Shift `Process`/`ShiftLeft`/229 keydown → `_imeKeydownAwaitingCommit = true`.
3. Empty `compositionend` while the textarea still holds `'s'` → `expectsPostCompositionInput = false`, finalizer flushes `'s'`, `_pendingComposition` cleared, flag still true.
4. IME's delayed `insertText 's'` → `input()` finds no pending → `_claimImeKeydownCommit` sees the flag and sends `'s'` a second time.

## Required outcome
- Once a pending composition has been sent or settled, the held-key debt is discharged and a later `insertText` must not be re-sent.

## Suggested approach
- Clear `_imeKeydownAwaitingCommit` in `_sendPendingComposition` / `_settlePendingComposition` (one source of truth), or set it only on the empty-`compositionend` path that actually waits.

## Open questions for the human
- Does the captured Sogou trace ever leave the textarea non-empty at `compositionend`? If not, this is latent rather than live.

}
+ this._sendPendingComposition(pending, true);
+ }
}
- }, 0);
+ });
+ }
}
}

+ /** Physical Shift used by Windows IMEs (Sogou) to commit pinyin as Latin. */
+ private _isImeModeToggleShift(ev: KeyboardEvent): boolean {
+ return ev.keyCode === 16 || (typeof ev.code === 'string' && ev.code.startsWith('Shift'));
+ }
+
+ /**
+ * Windows/Sogou Shift-to-English ends the composition with empty data while the Process key
+ * is still down, then delivers the latin commit as a later insertText. Flushing the empty
+ * textarea on this tick would drop that commit.
+ */
+ private _shouldWaitForHeldImeCommit(pending: IPendingComposition): boolean {
+ return (
+ pending.expectsPostCompositionInput &&
+ this._imeKeydownAwaitingCommit &&
+ pending.inputData.length === 0 &&
+ this._getPendingTextareaInput(pending, true).length === 0
+ );
+ }
+
+ private _sendPendingComposition(
Expand Down Expand Up @@ -765,6 +805,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ this._pendingComposition = undefined;
+ this._isAwaitingCompositionEnd = false;
+ this._isComposing = false;
+ this._imeKeydownAwaitingCommit = false;
+ this._compositionView.classList.remove('active');
+ this._resetCompositionView();
+ this._textarea.value =
Expand Down Expand Up @@ -813,18 +854,17 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ id: pending.transactionId,
+ data: input,
+ dataPendingReconciliation: true
}
- }, 0);
+ }
+ }
+ ));
+ }
+
+ private _dispatchCompositionSessionEvent(event: CustomEvent): void {
+ if (typeof this._textarea.dispatchEvent === 'function') {
+ this._textarea.dispatchEvent(event);
}
}
+ }
+ }
+
+ private _dispatchCompositionTransactionSettled(): void {
+ this._dispatchCompositionSessionEvent(new CustomEvent(
+ 'xterm-composition-transaction-settled',
Expand Down Expand Up @@ -883,7 +923,8 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
+ if (
+ this._isComposing &&
+ this._compositionTransactionId === transactionId &&
+ this._composedRegionLength() === 0
+ this._composedRegionLength() === 0 &&
+ !this._imeKeydownAwaitingCommit
+ ) {
+ this._cancelComposition();
+ }
Expand Down Expand Up @@ -927,7 +968,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
/**
* Apply any changes made to the textarea after the current event chain is allowed to complete.
* This should be called when not currently composing but a keydown event with the "composition
@@ -222,6 +813,9 @@ export class CompositionHelper {
@@ -222,6 +848,9 @@ export class CompositionHelper {

const diff = newValue.replace(oldValue, '');

Expand All @@ -937,7 +978,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
this._dataAlreadySent = diff;

if (newValue.length > oldValue.length) {
@@ -236,6 +830,105 @@ export class CompositionHelper {
@@ -236,6 +865,105 @@ export class CompositionHelper {
}, 0);
}

Expand Down Expand Up @@ -1043,7 +1084,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
/**
* Positions the composition view on top of the cursor and the textarea just below it (so the
* IME helper dialog is positioned correctly).
@@ -243,10 +936,23 @@ export class CompositionHelper {
@@ -243,10 +971,23 @@ export class CompositionHelper {
* necessary as the IME events across browsers are not consistently triggered.
*/
public updateCompositionElements(dontRecurse?: boolean): void {
Expand All @@ -1068,7 +1109,7 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..4d62369d0005f465318ec93e221fd3ee
if (this._bufferService.buffer.isCursorInViewport) {
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);

@@ -254,31 +960,156 @@ export class CompositionHelper {
@@ -254,31 +995,156 @@ export class CompositionHelper {
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.css.cell.height;
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;

Expand Down
29 changes: 24 additions & 5 deletions config/scripts/regenerate-xterm-patches.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import {
CHECKOUT_DIFF_FLAGS,
PNPM_DIFF_FLAGS,
assertSourceDerivationsAgree,
commonParent,
escapeRegExp,
formatCheckFailure,
posixRelative,
normalizePnpmDiff,
pnpmDiffEnvironment
pnpmDiffEnvironment,
withoutGitRepositoryLocation
} from './xterm-patch-text.mjs'

const DEFAULT_REPO_ROOT = path.resolve(import.meta.dirname, '..', '..')
Expand Down Expand Up @@ -166,12 +169,16 @@ const WINDOWS_SHIM_COMMANDS = new Set(['npm', 'npx', 'pnpm', 'yarn'])

function run(command, args, options = {}) {
const shim = process.platform === 'win32' && WINDOWS_SHIM_COMMANDS.has(command)
const { env: providedEnv, ...rest } = options
const env =
command === 'git' ? withoutGitRepositoryLocation(providedEnv ?? process.env) : providedEnv
return execFileSync(shim ? `${command}.cmd` : command, args, {
encoding: 'utf8',
maxBuffer: 256 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'inherit'],
shell: shim,
...options
...rest,
...(env ? { env } : {})
})
}

Expand Down Expand Up @@ -228,6 +235,14 @@ function ensureUpstreamCheckout(manifest, workDir) {
}
run('git', ['checkout', '--quiet', '--detach', commit], { cwd: root })
run('git', ['reset', '--quiet', '--hard', commit], { cwd: root })
// Why: upstream's `* text=auto` smudges CRLF on Windows. A later
// `checkout --force` leaves those files in place because Git still
// considers them clean, so drop the index and reset after disabling text.
const localAttributes = path.join(root, '.git', 'info', 'attributes')
mkdirSync(path.dirname(localAttributes), { recursive: true })
writeFileSync(localAttributes, '* -text\n')
run('git', ['rm', '-r', '--quiet', '--cached', '.'], { cwd: root })
run('git', ['reset', '--quiet', '--hard', commit], { cwd: root })
return root
}

Expand Down Expand Up @@ -277,7 +292,7 @@ function assertPristineSourceMatches(pristineDir, upstreamRoot, packageEntry) {
const sourceRoot = path.join(pristineDir, 'src')
const drifted = listFilesRelative(sourceRoot)
.map((relative) => path.join('src', relative))
.filter((relative) => relative !== stampFile)
.filter((relative) => !stampFile || toPosix(relative) !== toPosix(stampFile))
.filter(
(relative) =>
!sameBytes(
Expand Down Expand Up @@ -356,9 +371,13 @@ function overlayBuildOutput(pristineDir, upstreamRoot, packageEntry, destination
}

function diffFolders(folderA, folderB) {
const cwd = commonParent(folderA, folderB)
const relA = posixRelative(cwd, folderA)
const relB = posixRelative(cwd, folderB)
let stdout
try {
stdout = execFileSync('git', [...PNPM_DIFF_FLAGS, folderA, folderB], {
stdout = execFileSync('git', [...PNPM_DIFF_FLAGS, relA, relB], {
cwd,
encoding: 'utf8',
maxBuffer: 512 * 1024 * 1024,
env: pnpmDiffEnvironment(),
Expand All @@ -371,7 +390,7 @@ function diffFolders(folderA, folderB) {
}
stdout = error.stdout
}
return normalizePnpmDiff(stdout, folderA, folderB)
return normalizePnpmDiff(stdout, relA, relB)
}

/** The source of truth for the hand-written half: what the checkout itself holds. */
Expand Down
41 changes: 37 additions & 4 deletions config/scripts/regenerate-xterm-patches.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import {
CHECKOUT_DIFF_FLAGS,
PNPM_DIFF_FLAGS,
assertSourceDerivationsAgree,
commonParent,
firstDifferenceIndex,
formatCheckFailure,
normalizePnpmDiff,
pnpmDiffEnvironment,
posixRelative,
selectPatchEntries,
sourceHunks,
splitPatchEntries
Expand Down Expand Up @@ -66,9 +68,13 @@ async function writeTree(root, files) {

/** The three exported diff pieces, composed the way the generator composes them. */
function diffFolders(folderA, folderB) {
const cwd = commonParent(folderA, folderB)
const relA = posixRelative(cwd, folderA)
const relB = posixRelative(cwd, folderB)
let stdout
try {
stdout = execFileSync('git', [...PNPM_DIFF_FLAGS, folderA, folderB], {
stdout = execFileSync('git', [...PNPM_DIFF_FLAGS, relA, relB], {
cwd,
encoding: 'utf8',
env: pnpmDiffEnvironment(),
stdio: ['ignore', 'pipe', 'pipe']
Expand All @@ -79,7 +85,7 @@ function diffFolders(folderA, folderB) {
}
stdout = error.stdout
}
return normalizePnpmDiff(stdout, folderA, folderB)
return normalizePnpmDiff(stdout, relA, relB)
}

const PRISTINE = {
Expand Down Expand Up @@ -128,6 +134,25 @@ describe('pnpm diff format', () => {
})
})

it('strips Git repository-location variables so cwd-targeted git cannot rewrite the caller', () => {
const environment = pnpmDiffEnvironment({
PATH: '/usr/bin',
GIT_DIR: '/other/repo/.git',
GIT_WORK_TREE: '/other/repo',
GIT_INDEX_FILE: '/tmp/index'
})
expect(environment.GIT_DIR).toBeUndefined()
expect(environment.GIT_WORK_TREE).toBeUndefined()
expect(environment.GIT_INDEX_FILE).toBeUndefined()
expect(environment.GIT_CONFIG_NOSYSTEM).toBe('1')
})

it('returns the filesystem root when it is the only shared directory', () => {
expect(commonParent('/left', '/right', path.posix)).toBe('/')
expect(commonParent('C:\\left', 'C:\\right', path.win32)).toBe('C:\\')
expect(commonParent('/tmp/a', '/tmp/b', path.posix)).toBe('/tmp')
})

it('strips both scratch folder prefixes from headers and index lines', async () => {
const root = await createDirectory()
const folderA = path.join(root, 'pristine')
Expand Down Expand Up @@ -216,7 +241,11 @@ describe('round-trip stability', () => {
await writeTree(replay, PRISTINE)
const patchFile = path.join(root, 'round-trip.patch')
await writeFile(patchFile, patch)
execFileSync('git', ['apply', '-p1', '--whitespace=nowarn', patchFile], { cwd: replay })
execFileSync(
'git',
['-c', 'core.autocrlf=false', 'apply', '-p1', '--whitespace=nowarn', patchFile],
{ cwd: replay }
)

expect(await readFile(path.join(replay, 'lib/widget.js'), 'utf8')).toBe(
PATCHED['lib/widget.js']
Expand All @@ -235,7 +264,11 @@ describe('round-trip stability', () => {

const replay = path.join(root, 'replay')
await writeTree(replay, PRISTINE)
execFileSync('git', ['apply', '-p1', '--whitespace=nowarn', patchFile], { cwd: replay })
execFileSync(
'git',
['-c', 'core.autocrlf=false', 'apply', '-p1', '--whitespace=nowarn', patchFile],
{ cwd: replay }
)

expect(await readFile(path.join(replay, 'src/Widget.ts'), 'utf8')).toBe(
PATCHED['src/Widget.ts']
Expand Down
Loading