diff --git a/cli/src/utils/git.ts b/cli/src/utils/git.ts index 77f187c312..427d0f6ad6 100644 --- a/cli/src/utils/git.ts +++ b/cli/src/utils/git.ts @@ -170,20 +170,12 @@ export function gitRecordedDatatableMigrationPaths(): RecordedMigrationPaths { }; } - // `core.quotePath` (on by default) C-quotes any path with a non-ASCII byte, which - // matches nothing the caller holds and reads as "never tracked". + // `-z`, not `core.quotePath=false`: that setting only stops git C-quoting non-ASCII + // bytes, leaving a path holding `"`, `\` or a control character quoted and matching + // nothing the caller holds. `-z` emits every path raw, NUL-separated. const r = spawnSync( "git", - [ - "-c", - "core.quotePath=false", - "log", - "HEAD", - "--format=", - "--name-only", - "--", - "migrations/datatable", - ], + ["log", "HEAD", "--format=", "--name-only", "-z", "--", "migrations/datatable"], { encoding: "utf8", stdio: "pipe", maxBuffer: 64 * 1024 * 1024 }, ); if ((r.status ?? 1) !== 0) { @@ -195,7 +187,7 @@ export function gitRecordedDatatableMigrationPaths(): RecordedMigrationPaths { }; } const paths = new Set(); - for (const line of (r.stdout ?? "").split("\n")) { + for (const line of (r.stdout ?? "").split("\0")) { const p = line.trim(); if (p.length === 0) continue; if (prefix.length > 0) { diff --git a/cli/test/git_recorded_migrations_unit.test.ts b/cli/test/git_recorded_migrations_unit.test.ts index d087c14bde..dddfa99b44 100644 --- a/cli/test/git_recorded_migrations_unit.test.ts +++ b/cli/test/git_recorded_migrations_unit.test.ts @@ -55,15 +55,23 @@ describe("gitRecordedDatatableMigrationPaths", () => { } }); - test("records a path with a non-ASCII byte unquoted", () => { - // core.quotePath would return "migrations/datatable/dt/1_caf\303\251.up.sql", - // which matches no path the caller holds and reads as never recorded. - const rel = commitMigration("20260101000000_café.up.sql"); + test("records paths git would otherwise C-quote", () => { + // Non-ASCII is what `core.quotePath=false` covers; `"` and `\` stay quoted under + // it and need `-z`. All three read as never recorded when quoted. + const rels = [ + commitMigration("20260101000000_café.up.sql"), + commitMigration('20260101000001_a"b.up.sql'), + commitMigration("20260101000002_a\\b.up.sql"), + ]; const r = gitRecordedDatatableMigrationPaths(); expect(r.kind).toBe("known"); - if (r.kind === "known") expect(r.paths.has(rel)).toBe(true); + if (r.kind === "known") { + for (const rel of rels) expect(r.paths.has(rel)).toBe(true); + } }); + // Guards the `-z` parser, not the pre-existing behaviour: the split moved from + // newline to NUL, and a deletion still has to leave the path listed. test("a committed migration stays recorded after its deletion is committed", () => { const rel = commitMigration("20260101000000_a.up.sql"); fs.rmSync(path.join(dir, rel));