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) => {...@@ -147,6 +147,37 @@ it.layer(TestLayer)("CheckpointStore.layer", (it) => {
147 }),147 }),
148 );148 );
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
150 it.effect("can hide indentation churn when changes wrap existing lines", () =>181 it.effect("can hide indentation churn when changes wrap existing lines", () =>
151 Effect.gen(function* () {182 Effect.gen(function* () {
152 const tmp = yield* makeTmpDir();183 const tmp = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriver.ts+6-1
...@@ -30,7 +30,11 @@ import {...@@ -30,7 +30,11 @@ import {
30 type VcsStatusInput,30 type VcsStatusInput,
31 type VcsStatusResult,31 type VcsStatusResult,
32} from "@t3tools/contracts";32} from "@t3tools/contracts";
33import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";33import {
34 makeGitVcsDriverCore,
35 PATCH_RENDER_PREFIX_ARGS,
36 splitNullSeparatedGitStdoutPaths,
37} from "./GitVcsDriverCore.ts";
34import * as VcsDriver from "./VcsDriver.ts";38import * as VcsDriver from "./VcsDriver.ts";
35import * as VcsProcess from "./VcsProcess.ts";39import * as VcsProcess from "./VcsProcess.ts";
3640
...@@ -869,6 +873,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (...@@ -869,6 +873,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
869 "--no-color",873 "--no-color",
870 "--no-ext-diff",874 "--no-ext-diff",
871 "--no-textconv",875 "--no-textconv",
876 ...PATCH_RENDER_PREFIX_ARGS,
872 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),877 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
873 `${fromRevision}^{commit}`,878 `${fromRevision}^{commit}`,
874 `${input.toCheckpointRef}^{commit}`,879 `${input.toCheckpointRef}^{commit}`,
apps/server/src/vcs/GitVcsDriverCore.test.ts+28
...@@ -819,6 +819,34 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {...@@ -819,6 +819,34 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
819 }),819 }),
820 );820 );
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
822 it.effect("loads full file contents for working-tree diff expansion", () =>850 it.effect("loads full file contents for working-tree diff expansion", () =>
823 Effect.gen(function* () {851 Effect.gen(function* () {
824 const cwd = yield* makeTmpDir();852 const cwd = yield* makeTmpDir();
apps/server/src/vcs/GitVcsDriverCore.ts+7
...@@ -52,6 +52,10 @@ const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000;...@@ -52,6 +52,10 @@ const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000;
52const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;52const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000;
53const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000;53const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000;
54const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024;54const 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;
55const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000;59const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000;
56const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);60const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);
57const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);61const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);
...@@ -2205,6 +2209,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2205,6 +2209,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2205 "--no-ext-diff",2209 "--no-ext-diff",
2206 "--no-textconv",2210 "--no-textconv",
2207 "--minimal",2211 "--minimal",
2212 ...PATCH_RENDER_PREFIX_ARGS,
2208 "--",2213 "--",
2209 "/dev/null",2214 "/dev/null",
2210 relativePath,2215 relativePath,
...@@ -2257,6 +2262,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2257,6 +2262,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2257 "--no-ext-diff",2262 "--no-ext-diff",
2258 "--no-textconv",2263 "--no-textconv",
2259 "--minimal",2264 "--minimal",
2265 ...PATCH_RENDER_PREFIX_ARGS,
2260 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),2266 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
2261 "HEAD",2267 "HEAD",
2262 "--",2268 "--",
...@@ -2293,6 +2299,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2293,6 +2299,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2293 "--no-ext-diff",2299 "--no-ext-diff",
2294 "--no-textconv",2300 "--no-textconv",
2295 "--minimal",2301 "--minimal",
2302 ...PATCH_RENDER_PREFIX_ARGS,
2296 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),2303 ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
2297 `${baseRef}...HEAD`,2304 `${baseRef}...HEAD`,
2298 ],2305 ],