authorgravatar for 111503233+Defmon3@users.noreply.github.comDefmon3 <111503233+Defmon3@users.noreply.github.com> 2026-10-14 02:39:23+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 19:39:23-05:00
logaef0e9ca401acb40057a286a19de8768809db960
treed02037dd08b1054c0e4ec481599196cb39100d78
parent25629a76a0458bdeb707483722894158af090b41
signature Signed by PGP key B5690EEEBB952194

fix(git): preserve whitespace in file paths (#12464) (#12465)

Co-authored-by: Yash Singh <saiansh2525@gmail.com>

2 files changed, 69 insertions(+), 4 deletions(-)

packages/contracts/src/git.test.ts+66
......@@ -8,6 +8,7 @@ import {
88 GitRunStackedActionResult,
99 GitRunStackedActionInput,
1010 GitResolvePullRequestResult,
11 VcsStatusLocalResult,
1112} from "./git.ts";
1213
1314const decodeCreateWorktreeInput = Schema.decodeUnknownSync(VcsCreateWorktreeInput);
......@@ -20,6 +21,7 @@ const decodePreparePullRequestThreadResult = Schema.decodeUnknownSync(
2021const decodeRunStackedActionInput = Schema.decodeUnknownSync(GitRunStackedActionInput);
2122const decodeRunStackedActionResult = Schema.decodeUnknownSync(GitRunStackedActionResult);
2223const decodeResolvePullRequestResult = Schema.decodeUnknownSync(GitResolvePullRequestResult);
24const decodeVcsStatusLocalResult = Schema.decodeUnknownSync(VcsStatusLocalResult);
2325
2426describe("VcsCreateWorktreeInput", () => {
2527 it("accepts omitted newRefName for existing-refName worktrees", () => {
......@@ -125,6 +127,70 @@ describe("GitRunStackedActionInput", () => {
125127 expect(parsed.actionId).toBe("action-1");
126128 expect(parsed.action).toBe("create_pr");
127129 });
130
131 it("preserves leading and trailing whitespace in file paths", () => {
132 const parsed = decodeRunStackedActionInput({
133 actionId: "action-1",
134 cwd: "/repo",
135 action: "commit",
136 filePaths: [" leading.txt", "trailing.txt "],
137 });
138
139 expect(parsed.filePaths).toEqual([" leading.txt", "trailing.txt "]);
140 });
141
142 it("rejects an empty file path", () => {
143 expect(() =>
144 decodeRunStackedActionInput({
145 actionId: "action-1",
146 cwd: "/repo",
147 action: "commit",
148 filePaths: [""],
149 }),
150 ).toThrow();
151 });
152});
153
154describe("VcsStatusLocalResult", () => {
155 it("preserves leading and trailing whitespace in working-tree paths", () => {
156 const parsed = decodeVcsStatusLocalResult({
157 isRepo: true,
158 hasPrimaryRemote: true,
159 isDefaultRef: false,
160 refName: "main",
161 hasWorkingTreeChanges: true,
162 workingTree: {
163 files: [
164 { path: " leading.txt", insertions: 1, deletions: 0 },
165 { path: "trailing.txt ", insertions: 0, deletions: 1 },
166 ],
167 insertions: 1,
168 deletions: 1,
169 },
170 });
171
172 expect(parsed.workingTree.files.map((file) => file.path)).toEqual([
173 " leading.txt",
174 "trailing.txt ",
175 ]);
176 });
177
178 it("rejects an empty working-tree path", () => {
179 expect(() =>
180 decodeVcsStatusLocalResult({
181 isRepo: true,
182 hasPrimaryRemote: true,
183 isDefaultRef: false,
184 refName: "main",
185 hasWorkingTreeChanges: true,
186 workingTree: {
187 files: [{ path: "", insertions: 0, deletions: 0 }],
188 insertions: 0,
189 deletions: 0,
190 },
191 }),
192 ).toThrow();
193 });
128194});
129195
130196describe("GitRunStackedActionResult", () => {
packages/contracts/src/git.ts+3-4
......@@ -11,6 +11,7 @@ import { SourceControlProviderError, SourceControlProviderInfo } from "./sourceC
1111import { VcsDriverKind } from "./vcs.ts";
1212
1313const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
14const GitFilePath = Schema.String.check(Schema.isNonEmpty());
1415const GIT_LIST_BRANCHES_MAX_LIMIT = 200;
1516
1617// Domain Types
......@@ -129,9 +130,7 @@ export const GitRunStackedActionInput = Schema.Struct({
129130 action: GitStackedAction,
130131 commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))),
131132 featureBranch: Schema.optional(Schema.Boolean),
132 filePaths: Schema.optional(
133 Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)),
134 ),
133 filePaths: Schema.optional(Schema.Array(GitFilePath).check(Schema.isMinLength(1))),
135134 /** The thread the action runs beside; a pull request it creates is linked to it. */
136135 threadId: Schema.optional(ThreadId),
137136 projectId: Schema.optional(ProjectId),
......@@ -234,7 +233,7 @@ const VcsStatusLocalShape = {
234233 workingTree: Schema.Struct({
235234 files: Schema.Array(
236235 Schema.Struct({
237 path: TrimmedNonEmptyStringSchema,
236 path: GitFilePath,
238237 insertions: NonNegativeInt,
239238 deletions: NonNegativeInt,
240239 }),