Keep moved file paths and diffs intact in commit history (#8509)

* fix(server): keep commit file paths when a file moves up a directory

* test(git): open moved commit files through their listed path

Verify commit history and diff lookup agree for files moved up a directory, with and without content edits.

Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn>

* refactor(git): clarify compact rename handling and test cases

Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn>

---------

Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-11-02 07:58:56 +00:00
committed by GitHub
co-authored by hadan8977
parent d4ac64f335
commit 3e3a707f5a
2 changed files with 50 additions and 1 deletions
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "fs"
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, describe, expect, it } from "vitest";
import { listCheckoutCommits } from "./checkout-git.js";
import { getCommitFileDiff, listCheckoutCommits } from "./checkout-git.js";
import { writePaseoWorktreeMetadata } from "./worktree-metadata.js";
const tempDirs: string[] = [];
@@ -332,6 +332,50 @@ describe("listCheckoutCommits", () => {
]);
});
it.each([
{
label: "without edits",
content: "one\ntwo\nthree\nfour\nfive\n",
additions: 0,
deletions: 0,
},
{ label: "with edits", content: "one\nTWO\nthree\nfour\nfive\n", additions: 1, deletions: 1 },
])(
"opens a file moved up out of a directory $label",
async ({ content, additions, deletions }) => {
const { repoDir } = initRepoOnMain();
git(["checkout", "-b", "feature"], repoDir);
mkdirSync(join(repoDir, "src", "nested"), { recursive: true });
commitFile(repoDir, "src/nested/a.txt", "one\ntwo\nthree\nfour\nfive\n", "Add nested file");
git(["mv", "src/nested/a.txt", "src/a.txt"], repoDir);
writeFileSync(join(repoDir, "src/a.txt"), content);
git(["add", "src/a.txt"], repoDir);
commit(repoDir, "Move file up");
const { commits } = await listCheckoutCommits({ cwd: repoDir });
const movedCommit = commits[0]!;
const movedFile = movedCommit.files[0]!;
expect(movedCommit.files).toEqual([
{
path: "src/a.txt",
additions,
deletions,
status: "renamed",
},
]);
expect(
await getCommitFileDiff({ cwd: repoDir, sha: movedCommit.sha, path: movedFile.path }),
).toMatchObject({
path: "src/a.txt",
isNew: false,
isDeleted: false,
additions,
deletions,
});
},
);
it("derives status for modified and deleted files", async () => {
const { repoDir } = initRepoOnMain();
git(["checkout", "-b", "feature"], repoDir);
@@ -636,6 +636,11 @@ function normalizeNumstatPath(pathField: string): string {
const braceRenameMatch = pathField.match(/^(.*)\{(.*) => (.*)\}(.*)$/);
if (braceRenameMatch) {
const [, prefix, , renamed, suffix] = braceRenameMatch;
// Moving out of a directory prints `src/{nested => }/a.ts`, so an empty
// destination leaves a separator from each side behind.
if (!renamed) {
return unquoteGitPath(`${prefix}${suffix.slice(1)}`);
}
return unquoteGitPath(`${prefix}${renamed}${suffix}`);
}