P0 — parsePorcelainZ rename/copy handling Git's -z porcelain emits `R <new>\0<old>\0` for renames (and C for copies). The naive split-and-slice treated <old> as an independent dirty path, which made runObservedDestructiveSyncOp warn about phantom "cleared paths" whenever a rename was in flight. Now we detect R/C status and skip the trailing entry. P1 — race-rescue loop unstages between attempts `git stash create` snapshots the index without clearing it, so iteration 2's `git add -A` would re-stage atop iteration 1's leftovers. Tree differences inside the loop then reflected stale staging rather than genuine new writes. Added a `git reset` at the top of each iteration so every attempt starts from a clean index baseline. P1 — writeActiveMergerStatus is now atomic Switched from in-place writeFileSync to temp-file + renameSync. POSIX guarantees rename atomicity on the same filesystem, so a reader can no longer catch the file mid-flush and return a false-negative "no merger active" advisory. P2 — Step regex em-dash clarity `[—\-:]` is functionally fine but obscures intent; switched to `(?:—|-|:)` so the em-dash branch is obvious. Added a test case for the em-dash separator. New tests: - parse-porcelain-z.test.ts (8 cases including renames + copies) - em-dash case added to derive-subject-summary.test.ts 247/247 merger-suite tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { parsePorcelainZ } from "../merger.js";
|
|
|
|
describe("parsePorcelainZ", () => {
|
|
it("returns empty set for empty input", () => {
|
|
expect(parsePorcelainZ("")).toEqual(new Set());
|
|
});
|
|
|
|
it("parses single modified file", () => {
|
|
expect(parsePorcelainZ(" M src/foo.ts\0")).toEqual(new Set(["src/foo.ts"]));
|
|
});
|
|
|
|
it("parses staged + unstaged modifications", () => {
|
|
expect(parsePorcelainZ("M src/a.ts\0 M src/b.ts\0")).toEqual(
|
|
new Set(["src/a.ts", "src/b.ts"]),
|
|
);
|
|
});
|
|
|
|
it("parses untracked files", () => {
|
|
expect(parsePorcelainZ("?? new.ts\0")).toEqual(new Set(["new.ts"]));
|
|
});
|
|
|
|
it("treats a rename as a single path (the new name), not two", () => {
|
|
// Format: `R <new>\0<old>\0`
|
|
const raw = "R src/new.ts\0src/old.ts\0";
|
|
const result = parsePorcelainZ(raw);
|
|
expect(result).toEqual(new Set(["src/new.ts"]));
|
|
expect(result.has("src/old.ts")).toBe(false);
|
|
});
|
|
|
|
it("treats a copy the same way (single new path, skip old)", () => {
|
|
const raw = "C src/copy.ts\0src/original.ts\0";
|
|
expect(parsePorcelainZ(raw)).toEqual(new Set(["src/copy.ts"]));
|
|
});
|
|
|
|
it("handles a rename interleaved with regular modifications", () => {
|
|
// Three logical changes: M src/a.ts, R src/old → src/new, M src/b.ts
|
|
const raw = " M src/a.ts\0R src/new.ts\0src/old.ts\0 M src/b.ts\0";
|
|
expect(parsePorcelainZ(raw)).toEqual(
|
|
new Set(["src/a.ts", "src/new.ts", "src/b.ts"]),
|
|
);
|
|
});
|
|
|
|
it("handles paths with spaces", () => {
|
|
expect(parsePorcelainZ(" M src/file with spaces.ts\0")).toEqual(
|
|
new Set(["src/file with spaces.ts"]),
|
|
);
|
|
});
|
|
});
|