From 3e3a707f5a40c2e09536e0c167ea4417fca1c1d9 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:58:56 +0000 Subject: [PATCH] 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 * refactor(git): clarify compact rename handling and test cases Co-authored-by: hadan8977 --------- Co-authored-by: hadan8977 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/utils/checkout-git.commits.test.ts | 46 ++++++++++++++++++- packages/server/src/utils/checkout-git.ts | 5 ++ 2 files changed, 50 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..7baac91c23 100644 --- a/packages/server/src/utils/checkout-git.commits.test.ts +++ b/packages/server/src/utils/checkout-git.commits.test.ts @@ -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); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 358bd6eb69..bd25f46770 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -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}`); }