fix(i18n): preserve merge conflicts without runtime
This commit is contained in:
@@ -31,9 +31,15 @@ const PAIRING_MERGE_DRIVER_CONFIG = [
|
||||
['merge.dsh-translation-pairing.name', 'DeepSeek Harness bilingual pairing records'],
|
||||
[
|
||||
'merge.dsh-translation-pairing.driver',
|
||||
'node --import tsx/esm scripts/merge-translation-pairing.ts %O %A %B %P',
|
||||
'scripts/merge-translation-pairing-driver.sh %O %A %B %P',
|
||||
],
|
||||
]
|
||||
const PAIRING_MERGE_DRIVER_PROBE = [
|
||||
'--import',
|
||||
'tsx/esm',
|
||||
'scripts/merge-translation-pairing.ts',
|
||||
'--probe',
|
||||
]
|
||||
|
||||
function errorCode(error) {
|
||||
return typeof error === 'object' && error !== null && 'code' in error
|
||||
@@ -678,6 +684,10 @@ function installPairingMergeDriver(root, worktreeConfigPath) {
|
||||
}
|
||||
}
|
||||
|
||||
function probePairingMergeDriver(root) {
|
||||
capture(process.execPath, PAIRING_MERGE_DRIVER_PROBE, { cwd: root })
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') return
|
||||
if (typeof lefthookPackage.bin?.lefthook !== 'string') return
|
||||
@@ -767,6 +777,7 @@ async function main() {
|
||||
let pathChanged = false
|
||||
let rollbackPairingMergeDriver = () => {}
|
||||
try {
|
||||
probePairingMergeDriver(root)
|
||||
rollbackPairingMergeDriver = installPairingMergeDriver(root, worktreeConfigPath)
|
||||
git(['config', '--worktree', 'core.hooksPath', hooksPath], root)
|
||||
pathChanged = worktreePath !== hooksPath
|
||||
|
||||
@@ -18,7 +18,9 @@ import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url))
|
||||
const pairingMergeDriver = 'node --import tsx/esm scripts/merge-translation-pairing.ts %O %A %B %P'
|
||||
const pairingMergeDriver = 'scripts/merge-translation-pairing-driver.sh %O %A %B %P'
|
||||
const scriptsDirectory = fileURLToPath(new URL('.', import.meta.url))
|
||||
const tsxPackageDirectory = dirname(fileURLToPath(import.meta.resolve('tsx/package.json')))
|
||||
const fixtures: string[] = []
|
||||
// Multi-worktree cases spawn several Git and Node subprocesses; coverage concurrency can
|
||||
// legitimately exceed Vitest's default deadline without changing the installer behavior.
|
||||
@@ -123,6 +125,12 @@ function installFakeLefthook(root: string): void {
|
||||
chmodSync(shim, 0o755)
|
||||
}
|
||||
|
||||
function installPairingProbeFixture(root: string): void {
|
||||
const linkType = process.platform === 'win32' ? 'junction' : 'dir'
|
||||
symlinkSync(scriptsDirectory, join(root, 'scripts'), linkType)
|
||||
symlinkSync(tsxPackageDirectory, join(root, 'node_modules/tsx'), linkType)
|
||||
}
|
||||
|
||||
function createFixture(names: { main?: string; linked?: string } = {}): Fixture {
|
||||
const container = mkdtempSync(join(tmpdir(), 'dsh-lefthook-'))
|
||||
fixtures.push(container)
|
||||
@@ -152,6 +160,8 @@ function createFixture(names: { main?: string; linked?: string } = {}): Fixture
|
||||
write(join(linked, 'lefthook.yml'), 'linked-worktree-config\n')
|
||||
installFakeLefthook(main)
|
||||
installFakeLefthook(linked)
|
||||
installPairingProbeFixture(main)
|
||||
installPairingProbeFixture(linked)
|
||||
return fixture
|
||||
}
|
||||
|
||||
@@ -287,6 +297,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
git(fixture, fixture.main, ['worktree', 'add', '-b', 'late-linked', lateLinked])
|
||||
write(join(lateLinked, 'lefthook.yml'), 'late-linked-worktree-config\n')
|
||||
installFakeLefthook(lateLinked)
|
||||
installPairingProbeFixture(lateLinked)
|
||||
expect(git(fixture, lateLinked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(mainHooks)
|
||||
|
||||
const linkedInstall = await runInstaller(fixture, lateLinked)
|
||||
@@ -791,6 +802,20 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy pre-push\n')
|
||||
})
|
||||
|
||||
it('does not publish worktree integration when the pairing driver probe fails', async () => {
|
||||
const fixture = createFixture()
|
||||
rmSync(join(fixture.main, 'node_modules/tsx'), { recursive: true, force: true })
|
||||
|
||||
const result = await runInstaller(fixture, fixture.main)
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('merge-translation-pairing.ts --probe failed')
|
||||
expect(gitResult(fixture, fixture.main, ['config', '--get', 'core.hooksPath']).status).toBe(1)
|
||||
expect(gitResult(fixture, fixture.main, [
|
||||
'config', '--get', 'merge.dsh-translation-pairing.driver',
|
||||
]).status).toBe(1)
|
||||
})
|
||||
|
||||
it('reports installation and hook-path rollback failures together', async () => {
|
||||
const fixture = createFixture()
|
||||
|
||||
|
||||
35
scripts/merge-translation-pairing-driver.sh
Executable file
35
scripts/merge-translation-pairing-driver.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$#" -ne 4 ]; then
|
||||
echo 'merge-translation-pairing: expected <ancestor> <current> <other> <repository-path>' >&2
|
||||
exit 129
|
||||
fi
|
||||
|
||||
ancestor_path=$1
|
||||
current_path=$2
|
||||
other_path=$3
|
||||
meta_path=$4
|
||||
driver_directory=$(CDPATH= cd -P "$(dirname "$0")" && pwd) || exit 129
|
||||
driver_path=$driver_directory/merge-translation-pairing.ts
|
||||
|
||||
if command -v node >/dev/null 2>&1 \
|
||||
&& node --import tsx/esm "$driver_path" --probe >/dev/null 2>&1; then
|
||||
exec node --import tsx/esm "$driver_path" \
|
||||
"$ancestor_path" "$current_path" "$other_path" "$meta_path"
|
||||
fi
|
||||
|
||||
echo "merge-translation-pairing: runtime is unavailable; leaving an ordinary text conflict in $meta_path" >&2
|
||||
git merge-file \
|
||||
-L "$meta_path:current" \
|
||||
-L "$meta_path:ancestor" \
|
||||
-L "$meta_path:other" \
|
||||
-- "$current_path" "$ancestor_path" "$other_path"
|
||||
fallback_status=$?
|
||||
echo 'merge-translation-pairing: restore Node dependencies, then rerun the merge or `pnpm run resolve-translation-pairing-conflicts`; use `git merge --abort` to cancel' >&2
|
||||
|
||||
# A clean text merge is still unverified pairing metadata, so the driver must
|
||||
# leave Git's index stages unresolved until the repository-aware resolver runs.
|
||||
if [ "$fallback_status" -gt 127 ]; then
|
||||
exit "$fallback_status"
|
||||
fi
|
||||
exit 1
|
||||
@@ -10,31 +10,35 @@ import {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
try {
|
||||
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim()
|
||||
if (args[0] === '--resolve') {
|
||||
if (args.length !== 1) throw new Error('--resolve takes no paths; it inspects the unmerged index')
|
||||
const resolved = resolveTranslationPairingConflicts(root)
|
||||
if (resolved.length === 0) {
|
||||
console.log('merge-translation-pairing: no unresolved pairing records')
|
||||
} else {
|
||||
for (const path of resolved) console.log(`merge-translation-pairing: resolved ${path}`)
|
||||
}
|
||||
if (args[0] === '--probe') {
|
||||
if (args.length !== 1) throw new Error('--probe takes no other arguments')
|
||||
} else {
|
||||
if (args.length !== 4) {
|
||||
throw new Error('merge-driver mode requires <ancestor> <current> <other> <repository-path>')
|
||||
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim()
|
||||
if (args[0] === '--resolve') {
|
||||
if (args.length !== 1) throw new Error('--resolve takes no paths; it inspects the unmerged index')
|
||||
const resolved = resolveTranslationPairingConflicts(root)
|
||||
if (resolved.length === 0) {
|
||||
console.log('merge-translation-pairing: no unresolved pairing records')
|
||||
} else {
|
||||
for (const path of resolved) console.log(`merge-translation-pairing: resolved ${path}`)
|
||||
}
|
||||
} else {
|
||||
if (args.length !== 4) {
|
||||
throw new Error('merge-driver mode requires <ancestor> <current> <other> <repository-path>')
|
||||
}
|
||||
const [ancestorPath, currentPath, otherPath, metaPath] = args
|
||||
if (ancestorPath === undefined || currentPath === undefined || otherPath === undefined || metaPath === undefined) {
|
||||
throw new Error('merge-driver arguments are incomplete')
|
||||
}
|
||||
const result = mergeTranslationPairingRecords(
|
||||
root,
|
||||
metaPath,
|
||||
readFileSync(ancestorPath, 'utf8'),
|
||||
readFileSync(currentPath, 'utf8'),
|
||||
readFileSync(otherPath, 'utf8'),
|
||||
)
|
||||
writeFileSync(currentPath, result.record)
|
||||
}
|
||||
const [ancestorPath, currentPath, otherPath, metaPath] = args
|
||||
if (ancestorPath === undefined || currentPath === undefined || otherPath === undefined || metaPath === undefined) {
|
||||
throw new Error('merge-driver arguments are incomplete')
|
||||
}
|
||||
const result = mergeTranslationPairingRecords(
|
||||
root,
|
||||
metaPath,
|
||||
readFileSync(ancestorPath, 'utf8'),
|
||||
readFileSync(currentPath, 'utf8'),
|
||||
readFileSync(otherPath, 'utf8'),
|
||||
)
|
||||
writeFileSync(currentPath, result.record)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`merge-translation-pairing: ${error instanceof Error ? error.message : String(error)}`)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** Integration coverage for automatic and explicit pairing-record conflict resolution. */
|
||||
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
} from './translation-pairing-record.ts'
|
||||
|
||||
const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url))
|
||||
const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url))
|
||||
const workspaceRoot = fileURLToPath(new URL('../', import.meta.url))
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx/esm'))
|
||||
const fixtures: string[] = []
|
||||
|
||||
@@ -46,6 +48,16 @@ function shellQuote(value: string): string {
|
||||
return `"${value.replace(/["\\$`]/g, '\\$&')}"`
|
||||
}
|
||||
|
||||
function installFixtureRuntime(root: string): void {
|
||||
const linkType = process.platform === 'win32' ? 'junction' : 'dir'
|
||||
symlinkSync(
|
||||
join(workspaceRoot, 'node_modules'),
|
||||
join(root, 'node_modules'),
|
||||
linkType,
|
||||
)
|
||||
symlinkSync(join(workspaceRoot, 'scripts'), join(root, 'scripts'), linkType)
|
||||
}
|
||||
|
||||
function createFixture(attributes = true): Fixture {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-merge-'))
|
||||
fixtures.push(root)
|
||||
@@ -99,6 +111,17 @@ function commitPair(fixture: Fixture, source: string, zh: string, message: strin
|
||||
return sidecar
|
||||
}
|
||||
|
||||
function commitTextCleanPair(fixture: Fixture, source: string, zh: string, message: string): void {
|
||||
const sidecar = record(fixture.root, 'docs/guide.md', source, zh)
|
||||
write(
|
||||
fixture.root,
|
||||
'docs/guide.i18n.yaml',
|
||||
sidecar.replace('\nguide.zh.md:', '\n# Stable separator for independent line merges.\nguide.zh.md:'),
|
||||
)
|
||||
git(fixture, ['add', '.'])
|
||||
git(fixture, ['commit', '-m', message])
|
||||
}
|
||||
|
||||
function createDivergedPair(fixture: Fixture): { ancestor: string; current: string; other: string } {
|
||||
const ancestor = commitPair(fixture, baseSource, baseZh, 'base')
|
||||
git(fixture, ['switch', '-c', 'current'])
|
||||
@@ -109,6 +132,15 @@ function createDivergedPair(fixture: Fixture): { ancestor: string; current: stri
|
||||
return { ancestor, current, other }
|
||||
}
|
||||
|
||||
function createTextCleanDivergedPair(fixture: Fixture): void {
|
||||
commitTextCleanPair(fixture, baseSource, baseZh, 'base')
|
||||
git(fixture, ['switch', '-c', 'current'])
|
||||
commitTextCleanPair(fixture, currentSource, baseZh, 'current source')
|
||||
git(fixture, ['switch', 'master'])
|
||||
commitTextCleanPair(fixture, baseSource, otherZh, 'other translation')
|
||||
git(fixture, ['switch', 'current'])
|
||||
}
|
||||
|
||||
function startStoppedPairingMerge(fixture: Fixture): void {
|
||||
createDivergedPair(fixture)
|
||||
const merge = spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], {
|
||||
@@ -279,13 +311,12 @@ describe('translation pairing merge composition', () => {
|
||||
it('runs as Git\'s custom driver and commits a clean composed record', () => {
|
||||
const fixture = createFixture()
|
||||
createDivergedPair(fixture)
|
||||
const command = [
|
||||
shellQuote(process.execPath),
|
||||
'--import', shellQuote(tsxLoader),
|
||||
shellQuote(driver),
|
||||
'%O', '%A', '%B', '%P',
|
||||
].join(' ')
|
||||
git(fixture, ['config', 'merge.dsh-translation-pairing.driver', command])
|
||||
installFixtureRuntime(fixture.root)
|
||||
git(fixture, [
|
||||
'config',
|
||||
'merge.dsh-translation-pairing.driver',
|
||||
'scripts/merge-translation-pairing-driver.sh %O %A %B %P',
|
||||
])
|
||||
|
||||
git(fixture, ['merge', '--no-edit', 'master'])
|
||||
|
||||
@@ -293,6 +324,152 @@ describe('translation pairing merge composition', () => {
|
||||
expectMergedPair(fixture)
|
||||
})
|
||||
|
||||
it('leaves an ordinary recoverable conflict when the configured runtime is unavailable', () => {
|
||||
const fixture = createFixture()
|
||||
const records = createDivergedPair(fixture)
|
||||
const fakeBin = join(fixture.root, 'fake-bin')
|
||||
const fakeNode = join(fakeBin, 'node')
|
||||
write(fixture.root, 'fake-bin/node', '#!/bin/sh\nexit 72\n')
|
||||
chmodSync(fakeNode, 0o755)
|
||||
const command = [
|
||||
shellQuote(driverLauncher),
|
||||
'%O', '%A', '%B', '%P',
|
||||
].join(' ')
|
||||
git(fixture, ['config', 'merge.dsh-translation-pairing.driver', command])
|
||||
const headBefore = git(fixture, ['rev-parse', 'HEAD'])
|
||||
|
||||
const result = spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...fixture.env,
|
||||
PATH: `${fakeBin}${delimiter}${fixture.env.PATH ?? ''}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('runtime is unavailable; leaving an ordinary text conflict')
|
||||
expect(git(fixture, ['rev-parse', 'HEAD'])).toBe(headBefore)
|
||||
expect(git(fixture, ['rev-parse', '--verify', 'MERGE_HEAD'])).not.toBe('')
|
||||
expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml')
|
||||
expect(git(fixture, ['ls-files', '--unmerged', '--', 'docs/guide.i18n.yaml']).split('\n')).toHaveLength(3)
|
||||
const conflicted = readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')
|
||||
expect(conflicted).toContain('<<<<<<< docs/guide.i18n.yaml:current')
|
||||
for (const record of [records.current, records.other]) {
|
||||
const dataLines = record.split('\n').filter(line => line !== '' && !line.startsWith('#')).join('\n')
|
||||
expect(conflicted).toContain(dataLines)
|
||||
}
|
||||
|
||||
expect(resolveTranslationPairingConflicts(fixture.root)).toEqual(['docs/guide.i18n.yaml'])
|
||||
expectMergedPair(fixture)
|
||||
})
|
||||
|
||||
it('falls back before a broken driver entrypoint can replace the launcher', () => {
|
||||
const fixture = createFixture()
|
||||
createDivergedPair(fixture)
|
||||
const fakeBin = join(fixture.root, 'fake-bin')
|
||||
const fakeNode = join(fakeBin, 'node')
|
||||
write(
|
||||
fixture.root,
|
||||
'fake-bin/node',
|
||||
'#!/bin/sh\nif [ "$3" = "--eval" ]; then exit 0; fi\nexit 72\n',
|
||||
)
|
||||
chmodSync(fakeNode, 0o755)
|
||||
git(fixture, [
|
||||
'config',
|
||||
'merge.dsh-translation-pairing.driver',
|
||||
`${shellQuote(driverLauncher)} %O %A %B %P`,
|
||||
])
|
||||
|
||||
const result = spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...fixture.env,
|
||||
PATH: `${fakeBin}${delimiter}${fixture.env.PATH ?? ''}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('runtime is unavailable; leaving an ordinary text conflict')
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toContain(
|
||||
'<<<<<<< docs/guide.i18n.yaml:current',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps a clean text fallback unresolved until the explicit resolver confirms it', () => {
|
||||
const fixture = createFixture()
|
||||
createTextCleanDivergedPair(fixture)
|
||||
const fakeBin = join(fixture.root, 'fake-bin')
|
||||
const fakeNode = join(fakeBin, 'node')
|
||||
write(fixture.root, 'fake-bin/node', '#!/bin/sh\nexit 72\n')
|
||||
chmodSync(fakeNode, 0o755)
|
||||
git(fixture, [
|
||||
'config',
|
||||
'merge.dsh-translation-pairing.driver',
|
||||
`${shellQuote(driverLauncher)} %O %A %B %P`,
|
||||
])
|
||||
|
||||
const result = spawnSync('git', ['-C', fixture.root, 'merge', '--no-commit', 'master'], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...fixture.env,
|
||||
PATH: `${fakeBin}${delimiter}${fixture.env.PATH ?? ''}`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('docs/guide.i18n.yaml')
|
||||
const canonicalRecord = renderTranslationPairingRecord(translationPairPaths('docs/guide.md'), {
|
||||
sourceHash: gitBlobHash(Buffer.from(currentSource)),
|
||||
zhHash: gitBlobHash(Buffer.from(otherZh)),
|
||||
})
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toBe(
|
||||
canonicalRecord.replace(
|
||||
'\nguide.zh.md:',
|
||||
'\n# Stable separator for independent line merges.\nguide.zh.md:',
|
||||
),
|
||||
)
|
||||
|
||||
expect(resolveTranslationPairingConflicts(fixture.root)).toEqual(['docs/guide.i18n.yaml'])
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.md'), 'utf8')).toBe(currentSource)
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.zh.md'), 'utf8')).toBe(otherZh)
|
||||
expect(readFileSync(join(fixture.root, 'docs/guide.i18n.yaml'), 'utf8')).toBe(canonicalRecord)
|
||||
})
|
||||
|
||||
it('leaves a staged merge when the pre-merge-commit hook rejects it', () => {
|
||||
const fixture = createFixture()
|
||||
createDivergedPair(fixture)
|
||||
installFixtureRuntime(fixture.root)
|
||||
git(fixture, [
|
||||
'config',
|
||||
'merge.dsh-translation-pairing.driver',
|
||||
'scripts/merge-translation-pairing-driver.sh %O %A %B %P',
|
||||
])
|
||||
const hooks = join(fixture.root, 'hooks')
|
||||
write(
|
||||
fixture.root,
|
||||
'hooks/pre-merge-commit',
|
||||
'#!/bin/sh\necho "fixture pre-merge-commit rejection" >&2\nexit 77\n',
|
||||
)
|
||||
chmodSync(join(hooks, 'pre-merge-commit'), 0o755)
|
||||
git(fixture, ['config', 'core.hooksPath', hooks])
|
||||
const headBefore = git(fixture, ['rev-parse', 'HEAD'])
|
||||
|
||||
const result = spawnSync('git', ['-C', fixture.root, 'merge', '--no-edit', 'master'], {
|
||||
encoding: 'utf8',
|
||||
env: fixture.env,
|
||||
})
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('fixture pre-merge-commit rejection')
|
||||
expect(git(fixture, ['rev-parse', 'HEAD'])).toBe(headBefore)
|
||||
expect(git(fixture, ['rev-parse', '--verify', 'MERGE_HEAD'])).not.toBe('')
|
||||
expect(git(fixture, ['diff', '--name-only', '--diff-filter=U'])).toBe('')
|
||||
expect(git(fixture, ['diff', '--cached', '--name-only']).split('\n')).toContain(
|
||||
'docs/guide.i18n.yaml',
|
||||
)
|
||||
expectMergedPair(fixture)
|
||||
})
|
||||
|
||||
it('prints the recovery path when driver input is not composable', () => {
|
||||
const fixture = createFixture(false)
|
||||
const result = spawnSync(process.execPath, ['--import', tsxLoader, driver], {
|
||||
|
||||
@@ -97,13 +97,13 @@ function assertDefaultTextMerge(root: string, paths: TranslationPairPaths): void
|
||||
}
|
||||
}
|
||||
|
||||
function mergeBlobTriplet(
|
||||
function runTextMerge(
|
||||
root: string,
|
||||
owner: string,
|
||||
ancestor: Buffer,
|
||||
current: Buffer,
|
||||
other: Buffer,
|
||||
): Buffer {
|
||||
label: string,
|
||||
ancestor: Buffer | string,
|
||||
current: Buffer | string,
|
||||
other: Buffer | string,
|
||||
): { output: Buffer; status: number | null } {
|
||||
const temporary = mkdtempSync(join(tmpdir(), 'dsh-translation-pairing-merge-'))
|
||||
try {
|
||||
const ancestorPath = join(temporary, 'ancestor')
|
||||
@@ -115,26 +115,37 @@ function mergeBlobTriplet(
|
||||
const result = spawnSync('git', [
|
||||
'-C', root,
|
||||
'merge-file', '-p',
|
||||
'-L', `${owner}:current`,
|
||||
'-L', `${owner}:ancestor`,
|
||||
'-L', `${owner}:other`,
|
||||
'-L', `${label}:current`,
|
||||
'-L', `${label}:ancestor`,
|
||||
'-L', `${label}:other`,
|
||||
currentPath, ancestorPath, otherPath,
|
||||
], { maxBuffer: GIT_COMMAND_MAX_BUFFER })
|
||||
if (result.error) {
|
||||
throw new Error(`merging ${owner} failed: ${result.error.message}`, { cause: result.error })
|
||||
throw new Error(`merging ${label} failed: ${result.error.message}`, { cause: result.error })
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const kind = result.status !== null && result.status > 0 && result.status <= 127
|
||||
? 'has content conflicts'
|
||||
: `failed with status ${String(result.status)}`
|
||||
throw new Error(`${owner} ${kind}`)
|
||||
}
|
||||
return result.stdout
|
||||
return { output: result.stdout, status: result.status }
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function mergeBlobTriplet(
|
||||
root: string,
|
||||
owner: string,
|
||||
ancestor: Buffer,
|
||||
current: Buffer,
|
||||
other: Buffer,
|
||||
): Buffer {
|
||||
const result = runTextMerge(root, owner, ancestor, current, other)
|
||||
if (result.status !== 0) {
|
||||
const kind = result.status !== null && result.status > 0 && result.status <= 127
|
||||
? 'has content conflicts'
|
||||
: `failed with status ${String(result.status)}`
|
||||
throw new Error(`${owner} ${kind}`)
|
||||
}
|
||||
return result.output
|
||||
}
|
||||
|
||||
function loadRecordOwners(
|
||||
root: string,
|
||||
label: string,
|
||||
@@ -243,11 +254,14 @@ function unmergedSidecars(root: string): Map<string, UnmergedStages> {
|
||||
function assertUneditedSidecar(
|
||||
root: string,
|
||||
metaPath: string,
|
||||
ancestorRecord: string,
|
||||
currentRecord: string,
|
||||
otherRecord: string,
|
||||
): void {
|
||||
const worktreeRecord = readFileSync(join(root, metaPath), 'utf8')
|
||||
if (worktreeRecord === currentRecord || worktreeRecord === otherRecord) return
|
||||
const textMerge = runTextMerge(root, metaPath, ancestorRecord, currentRecord, otherRecord)
|
||||
if (textMerge.status === 0 && textMerge.output.toString('utf8') === worktreeRecord) return
|
||||
const stageDataLines = [currentRecord, otherRecord]
|
||||
.flatMap(record => record.split(/\r?\n/))
|
||||
.filter(line => line !== '' && !line.startsWith('#'))
|
||||
@@ -282,7 +296,7 @@ export function resolveTranslationPairingConflicts(root: string): string[] {
|
||||
const ancestorRecord = readGitBlob(root, stages.ancestor, `ancestor ${metaPath}`).toString('utf8')
|
||||
const currentRecord = readGitBlob(root, stages.current, `current ${metaPath}`).toString('utf8')
|
||||
const otherRecord = readGitBlob(root, stages.other, `other ${metaPath}`).toString('utf8')
|
||||
assertUneditedSidecar(root, metaPath, currentRecord, otherRecord)
|
||||
assertUneditedSidecar(root, metaPath, ancestorRecord, currentRecord, otherRecord)
|
||||
const result = mergeTranslationPairingRecords(
|
||||
root,
|
||||
metaPath,
|
||||
|
||||
Reference in New Issue
Block a user