From 5b495fe868b5c3d40552a748305be8febaca635a Mon Sep 17 00:00:00 2001 From: hadan8977 Date: Mon, 19 Oct 2026 17:51:11 +0800 Subject: [PATCH] fix(server): keep commit file paths when a file moves up a directory --- .../server/src/utils/checkout-git.commits.test.ts | 15 +++++++++++++++ packages/server/src/utils/checkout-git.ts | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/checkout-git.commits.test.ts b/packages/server/src/utils/checkout-git.commits.test.ts index ee29579f55..f6f4543763 100644 --- a/packages/server/src/utils/checkout-git.commits.test.ts +++ b/packages/server/src/utils/checkout-git.commits.test.ts @@ -332,6 +332,21 @@ describe("listCheckoutCommits", () => { ]); }); + it("keeps the destination path when a file moves up out of a directory", async () => { + const { repoDir } = initRepoOnMain(); + git(["checkout", "-b", "feature"], repoDir); + mkdirSync(join(repoDir, "src", "nested"), { recursive: true }); + commitFile(repoDir, "src/nested/a.txt", "content\n", "Add nested file"); + git(["mv", "src/nested/a.txt", "src/a.txt"], repoDir); + commit(repoDir, "Move file up"); + + const { commits } = await listCheckoutCommits({ cwd: repoDir }); + + expect(commits[0]?.files).toEqual([ + { path: "src/a.txt", additions: 0, deletions: 0, status: "renamed" }, + ]); + }); + it("derives status for modified and deleted files", async () => { const { repoDir } = initRepoOnMain(); git(["checkout", "-b", "feature"], repoDir); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 358bd6eb69..31be4cdfc3 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -636,7 +636,10 @@ function normalizeNumstatPath(pathField: string): string { const braceRenameMatch = pathField.match(/^(.*)\{(.*) => (.*)\}(.*)$/); if (braceRenameMatch) { const [, prefix, , renamed, suffix] = braceRenameMatch; - return unquoteGitPath(`${prefix}${renamed}${suffix}`); + // Moving out of a directory prints `src/{nested => }/a.ts`, so an empty + // destination leaves a separator from each side behind. + const destination = renamed ? `${prefix}${renamed}${suffix}` : `${prefix}${suffix.slice(1)}`; + return unquoteGitPath(destination); } const inlineRenameMatch = pathField.match(/^(.*) => (.*)$/);