Show actual edits when a file is renamed in Changes (#6559)

* fix(server): show actual edits when a file is renamed

Path-limited patch requests discarded the detected rename source, so Git reported the destination as a new file. Include both literal paths and select the destination patch to preserve the rename and avoid duplicating changed copy sources.

* fix(server): preserve small copy patches beside oversized sources

Filter rename and copy output before enforcing the bounded request budget, so a modified source cannot hide its small copy patch.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-11 07:55:09 +02:00
committed by GitHub
parent cfb18f16d4
commit dd0350bc2b
2 changed files with 150 additions and 8 deletions
+120 -1
View File
@@ -539,10 +539,129 @@ describe("checkout git utilities", () => {
});
expect(diff.structured).toContainEqual(
expect.objectContaining({ path: "renamed.txt", oldPath: "file.txt" }),
expect.objectContaining({
path: "renamed.txt",
oldPath: "file.txt",
additions: 0,
deletions: 0,
hunks: [],
}),
);
});
it.each([
{ mode: "uncommitted", oldPath: "to-rename.txt", path: "renamed.txt" },
{ mode: "base", oldPath: "to-rename.txt", path: "renamed.txt" },
{ mode: "uncommitted", oldPath: "old [file].txt", path: "new [file].txt" },
] as const)(
"shows only the edited line of a rename ($mode, $path)",
async ({ mode, oldPath, path }) => {
const original = Array.from({ length: 12 }, (_, index) => `line ${index + 1}`);
commitFile(repoDir, oldPath, original.join("\n") + "\n", "file to rename");
if (mode === "base") {
execFileSync("git", ["checkout", "-b", "feature/rename"], { cwd: repoDir });
}
execFileSync("git", ["mv", oldPath, path], { cwd: repoDir });
const edited = [...original];
edited[5] = "edited line 6";
writeFileSync(join(repoDir, path), edited.join("\n") + "\n");
execFileSync("git", ["add", path], { cwd: repoDir });
expect(
execFileSync("git", ["diff", "--name-status", "HEAD"], { cwd: repoDir }).toString(),
).toMatch(/R\d+\t/);
if (mode === "base") {
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "rename and edit"], {
cwd: repoDir,
});
}
const diff = await getCheckoutDiff(repoDir, {
mode,
...(mode === "base" ? { baseRef: "main" } : {}),
includeStructured: true,
});
expect(diff.structured).toEqual([
expect.objectContaining({
path,
oldPath,
isNew: false,
isDeleted: false,
additions: 1,
deletions: 1,
status: "ok",
}),
]);
expect(diff.diff).toContain(`rename from ${oldPath}`);
expect(diff.diff).toContain(`rename to ${path}`);
expect(diff.diff).toContain("-line 6\n+edited line 6");
expect(diff.diff).not.toContain("new file mode");
},
);
it("includes a changed copy source only once in the diff", async () => {
const original =
Array.from({ length: 12 }, (_, index) => `line ${index + 1}`).join("\n") + "\n";
commitFile(repoDir, "source.txt", original, "copy source");
execFileSync("git", ["config", "diff.renames", "copies"], { cwd: repoDir });
writeFileSync(join(repoDir, "copy.txt"), original);
writeFileSync(join(repoDir, "source.txt"), original.replace("line 6", "edited line 6"));
execFileSync("git", ["add", "copy.txt", "source.txt"], { cwd: repoDir });
expect(
execFileSync("git", ["diff", "--name-status", "HEAD"], { cwd: repoDir }).toString(),
).toContain("C100\tsource.txt\tcopy.txt");
const diff = await getCheckoutDiff(repoDir, {
mode: "uncommitted",
includeStructured: true,
});
expect(diff.structured).toEqual([
expect.objectContaining({
path: "copy.txt",
oldPath: "source.txt",
additions: 0,
deletions: 0,
}),
expect.objectContaining({ path: "source.txt", additions: 1, deletions: 1 }),
]);
expect(diff.diff.match(/^diff --git a\/source.txt b\/source.txt$/gm)).toHaveLength(1);
expect(diff.diff).toContain("copy from source.txt");
expect(diff.diff).toContain("copy to copy.txt");
});
it("keeps a small copy patch when its edited source exceeds the patch limit", async () => {
const original =
Array.from({ length: 12 }, (_, index) => `line ${index + 1}`).join("\n") + "\n";
commitFile(repoDir, "source.txt", original, "copy source");
execFileSync("git", ["config", "diff.renames", "copies"], { cwd: repoDir });
writeFileSync(join(repoDir, "copy.txt"), original);
writeFileSync(join(repoDir, "source.txt"), original + "added source line\n".repeat(80_000));
execFileSync("git", ["add", "copy.txt", "source.txt"], { cwd: repoDir });
expect(
execFileSync("git", ["diff", "--name-status", "HEAD"], { cwd: repoDir }).toString(),
).toContain("C100\tsource.txt\tcopy.txt");
const diff = await getCheckoutDiff(repoDir, {
mode: "uncommitted",
includeStructured: true,
});
expect(diff.structured).toEqual([
expect.objectContaining({
path: "copy.txt",
oldPath: "source.txt",
additions: 0,
deletions: 0,
hunks: [],
status: "ok",
}),
expect.objectContaining({ path: "source.txt", status: "too_large", hunks: [] }),
]);
expect(diff.diff).toContain("copy from source.txt");
expect(diff.diff).toContain("copy to copy.txt");
expect(diff.diff).not.toContain("+added source line");
});
it("reads the origin URL once when collecting facts for an origin-tracking branch", async () => {
setupRemoteTrackingMain(repoDir, tempDir);