authorgravatar for 56691628+Mnigos@users.noreply.github.comIgor Makowski <56691628+Mnigos@users.noreply.github.com> 2026-09-03 22:23:53+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 15:23:53-05:00
log36c4e9cf5c0123e33d65f2af9497ee090404b532
tree977b2d78e005b70b097f8414c350c21cfb89ea35
parentde025aa69ffb0ce1a45d30aed25c60454660b62d
signature Signed by PGP key B5690EEEBB952194

fix(server): keep a/ and b/ prefixes in rendered git patches (#9438)


4 files changed, 72 insertions(+), 1 deletions(-)

apps/server/src/checkpointing/CheckpointStore.test.ts+31
......@@ -147,6 +147,37 @@ it.layer(TestLayer)("CheckpointStore.layer", (it) => {
147147 }),
148148 );
149149
150 it.effect("keeps a/ and b/ patch prefixes when the repository disables them", () =>
151 Effect.gen(function* () {
152 const tmp = yield* makeTmpDir();
153 yield* initRepoWithCommit(tmp);
154 yield* git(tmp, ["config", "diff.noprefix", "true"]);
155 const checkpointStore = yield* CheckpointStore.CheckpointStore;
156 const threadId = ThreadId.make("thread-checkpoint-store-noprefix");
157 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
158 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
159
160 yield* checkpointStore.captureCheckpoint({
161 cwd: tmp,
162 checkpointRef: fromCheckpointRef,
163 });
164 yield* writeTextFile(NodePath.join(tmp, "README.md"), "# changed\n");
165 yield* checkpointStore.captureCheckpoint({
166 cwd: tmp,
167 checkpointRef: toCheckpointRef,
168 });
169
170 const diff = yield* checkpointStore.diffCheckpoints({
171 cwd: tmp,
172 fromCheckpointRef,
173 toCheckpointRef,
174 ignoreWhitespace: false,
175 });
176
177 expect(diff).toContain("diff --git a/README.md b/README.md");
178 }),
179 );
180
150181 it.effect("can hide indentation churn when changes wrap existing lines", () =>
151182 Effect.gen(function* () {
152183 const tmp = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriver.ts+6-1
......@@ -30,7 +30,11 @@ import {
3030 type VcsStatusInput,
3131 type VcsStatusResult,
3232} from "@t3tools/contracts";
33import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";
33import {
34 makeGitVcsDriverCore,
35 PATCH_RENDER_PREFIX_ARGS,
36 splitNullSeparatedGitStdoutPaths,
37} from "./GitVcsDriverCore.ts";
3438import * as VcsDriver from "./VcsDriver.ts";
3539import * as VcsProcess from "./VcsProcess.ts";
3640
......@@ -869,6 +873,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
869873 "--no-color",
870874 "--no-ext-diff",
871875 "--no-textconv",
876 ...PATCH_RENDER_PREFIX_ARGS,
872877 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
873878 `${fromRevision}^{commit}`,
874879 `${input.toCheckpointRef}^{commit}`,
apps/server/src/vcs/GitVcsDriverCore.test.ts+28
......@@ -819,6 +819,34 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
819819 }),
820820 );
821821
822 it.effect("keeps a/ and b/ patch prefixes when the repository disables them", () =>
823 Effect.gen(function* () {
824 const cwd = yield* makeTmpDir();
825 const { initialBranch } = yield* initRepoWithCommit(cwd);
826 const driver = yield* GitVcsDriver.GitVcsDriver;
827 yield* git(cwd, ["config", "diff.noprefix", "true"]);
828 yield* git(cwd, ["config", "diff.mnemonicPrefix", "true"]);
829 yield* git(cwd, ["checkout", "-b", "feature/noprefix"]);
830 yield* writeTextFile(cwd, "README.md", "# committed change\n");
831 yield* git(cwd, ["add", "README.md"]);
832 yield* git(cwd, ["commit", "-m", "committed change"]);
833 yield* writeTextFile(cwd, "README.md", "# dirty change\n");
834 yield* writeTextFile(cwd, "untracked.txt", "untracked\n");
835
836 const preview = yield* driver.getReviewDiffPreview({
837 cwd,
838 baseRef: initialBranch,
839 ignoreWhitespace: false,
840 });
841
842 const workingTree = preview.sources.find((source) => source.kind === "working-tree")?.diff;
843 const branchRange = preview.sources.find((source) => source.kind === "branch-range")?.diff;
844 assert.include(workingTree, "diff --git a/README.md b/README.md");
845 assert.include(workingTree, "+++ b/untracked.txt");
846 assert.include(branchRange, "diff --git a/README.md b/README.md");
847 }),
848 );
849
822850 it.effect("loads full file contents for working-tree diff expansion", () =>
823851 Effect.gen(function* () {
824852 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+7
......@@ -52,6 +52,10 @@ const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000;
5252const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;
5353const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000;
5454const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024;
55// Patches the clients render are parsed against git's default a/ and b/ path
56// prefixes. A repository or global diff.noprefix or diff.mnemonicPrefix would
57// otherwise leak into the patch and leave every parsed file unnamed.
58export const PATCH_RENDER_PREFIX_ARGS = ["--src-prefix=a/", "--dst-prefix=b/"] as const;
5559const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000;
5660const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);
5761const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);
......@@ -2205,6 +2209,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
22052209 "--no-ext-diff",
22062210 "--no-textconv",
22072211 "--minimal",
2212 ...PATCH_RENDER_PREFIX_ARGS,
22082213 "--",
22092214 "/dev/null",
22102215 relativePath,
......@@ -2257,6 +2262,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
22572262 "--no-ext-diff",
22582263 "--no-textconv",
22592264 "--minimal",
2265 ...PATCH_RENDER_PREFIX_ARGS,
22602266 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
22612267 "HEAD",
22622268 "--",
......@@ -2293,6 +2299,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
22932299 "--no-ext-diff",
22942300 "--no-textconv",
22952301 "--minimal",
2302 ...PATCH_RENDER_PREFIX_ARGS,
22962303 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
22972304 `${baseRef}...HEAD`,
22982305 ],