From 220aa0479ab233afba06b365d746f213c07ab7bc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 14 Sep 2026 23:55:21 -0700 Subject: [PATCH] fix: include both committed rename paths in diff selection --- CHANGELOG.md | 2 ++ docs/code-review.md | 7 ++++--- src/git.test.ts | 33 ++++++++++++++++++++++++++++++++- src/git.ts | 3 ++- src/workflow.test.ts | 16 ++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 530ea82..5d732f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.8.2 - Unreleased +- Fixed diff-scoped review, CI, and revalidation to include both paths of committed renames, preventing features mapped to the old path from being silently skipped. + ## 0.8.1 - 2026-09-13 **Highlights:** Nested-project repairs stop tripping over sibling changes, and failed patch attempts keep the edits a provider already wrote. diff --git a/docs/code-review.md b/docs/code-review.md index 3d33f73..4961674 100644 --- a/docs/code-review.md +++ b/docs/code-review.md @@ -53,9 +53,10 @@ clawpatch review --feature-list /tmp/features.txt ### --since Restrict review to features whose owned or context files have changed in -`git diff --name-only --relative ...HEAD`. Paths are compared relative to -the selected project root, so `--root` may point at a subdirectory inside a -larger Git repository. Useful for CI: +`git diff --no-renames --name-only --relative ...HEAD`. Committed renames +include both the old and new paths, so features mapped to either path remain +eligible. Paths are compared relative to the selected project root, so `--root` +may point at a subdirectory inside a larger Git repository. Useful for CI: ```bash clawpatch review --since origin/main # review what this branch changed diff --git a/src/git.test.ts b/src/git.test.ts index d397d27..062bd1c 100644 --- a/src/git.test.ts +++ b/src/git.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -12,6 +12,37 @@ afterEach(async () => { }); describe("Git machine output", () => { + it.each(["true", "copies", "false"])( + "includes both committed rename paths with diff.renames=%s", + async (renames) => { + const root = await gitFixture(); + await git(root, ["config", "diff.renames", renames]); + await git(root, ["mv", "base.txt", "renamed.txt"]); + await git(root, ["commit", "-qm", "rename"]); + + expect(await changedFilesSince(root, "HEAD~1")).toEqual(new Set(["base.txt", "renamed.txt"])); + }, + ); + + it("scopes both sides of committed renames to a nested project", async () => { + const root = await gitFixture(); + const project = join(root, "app"); + await mkdir(project); + await writeFile(join(project, "outgoing.txt"), "outgoing\n"); + await writeFile(join(project, "before.txt"), "internal\n"); + await git(root, ["add", "."]); + await git(root, ["commit", "-qm", "nested project"]); + await git(root, ["config", "diff.renames", "true"]); + await git(root, ["mv", "base.txt", "app/incoming.txt"]); + await git(root, ["mv", "app/outgoing.txt", "outside.txt"]); + await git(root, ["mv", "app/before.txt", "app/after.txt"]); + await git(root, ["commit", "-qm", "move across project boundaries"]); + + expect(await changedFilesSince(project, "HEAD~1")).toEqual( + new Set(["incoming.txt", "outgoing.txt", "before.txt", "after.txt"]), + ); + }); + it.skipIf(process.platform === "win32")( "preserves unusual filenames in committed and dirty sets", async () => { diff --git a/src/git.ts b/src/git.ts index 0ca96e0..a887e99 100644 --- a/src/git.ts +++ b/src/git.ts @@ -70,7 +70,8 @@ export async function changedFilesSince(root: string, ref: string): Promise { }); }); + it("selects existing review features when their owned file is renamed", async () => { + const root = await sinceFixture("clawpatch-since-rename-"); + const context = await makeContext(testOptions(root)); + await initCommand(context, {}); + await mapCommand(context); + const features = await readFeatures(statePaths(join(root, ".clawpatch"))); + await checkCommand(root, "git config diff.renames true"); + await checkCommand(root, "git mv src/two.ts src/renamed.ts"); + await commitAll(root, "rename two"); + + const reviewed = await reviewCommand(context, { since: "base", dryRun: true }); + const expected = expectedFeatureIds(features, new Set(["src/two.ts"]), true); + expect(expected.length).toBeGreaterThan(0); + expect(reviewed).toMatchObject({ dryRun: true, featureIds: expected }); + }); + it("selects changed features regardless of their previous review status", async () => { const root = await sinceFixture("clawpatch-since-status-"); const context = await makeContext(testOptions(root));