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 {...@@ -8,6 +8,7 @@ import {
8 GitRunStackedActionResult,8 GitRunStackedActionResult,
9 GitRunStackedActionInput,9 GitRunStackedActionInput,
10 GitResolvePullRequestResult,10 GitResolvePullRequestResult,
11 VcsStatusLocalResult,
11} from "./git.ts";12} from "./git.ts";
1213
13const decodeCreateWorktreeInput = Schema.decodeUnknownSync(VcsCreateWorktreeInput);14const decodeCreateWorktreeInput = Schema.decodeUnknownSync(VcsCreateWorktreeInput);
...@@ -20,6 +21,7 @@ const decodePreparePullRequestThreadResult = Schema.decodeUnknownSync(...@@ -20,6 +21,7 @@ const decodePreparePullRequestThreadResult = Schema.decodeUnknownSync(
20const decodeRunStackedActionInput = Schema.decodeUnknownSync(GitRunStackedActionInput);21const decodeRunStackedActionInput = Schema.decodeUnknownSync(GitRunStackedActionInput);
21const decodeRunStackedActionResult = Schema.decodeUnknownSync(GitRunStackedActionResult);22const decodeRunStackedActionResult = Schema.decodeUnknownSync(GitRunStackedActionResult);
22const decodeResolvePullRequestResult = Schema.decodeUnknownSync(GitResolvePullRequestResult);23const decodeResolvePullRequestResult = Schema.decodeUnknownSync(GitResolvePullRequestResult);
24const decodeVcsStatusLocalResult = Schema.decodeUnknownSync(VcsStatusLocalResult);
2325
24describe("VcsCreateWorktreeInput", () => {26describe("VcsCreateWorktreeInput", () => {
25 it("accepts omitted newRefName for existing-refName worktrees", () => {27 it("accepts omitted newRefName for existing-refName worktrees", () => {
...@@ -125,6 +127,70 @@ describe("GitRunStackedActionInput", () => {...@@ -125,6 +127,70 @@ describe("GitRunStackedActionInput", () => {
125 expect(parsed.actionId).toBe("action-1");127 expect(parsed.actionId).toBe("action-1");
126 expect(parsed.action).toBe("create_pr");128 expect(parsed.action).toBe("create_pr");
127 });129 });
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 });
128});194});
129195
130describe("GitRunStackedActionResult", () => {196describe("GitRunStackedActionResult", () => {
packages/contracts/src/git.ts+3-4
...@@ -11,6 +11,7 @@ import { SourceControlProviderError, SourceControlProviderInfo } from "./sourceC...@@ -11,6 +11,7 @@ import { SourceControlProviderError, SourceControlProviderInfo } from "./sourceC
11import { VcsDriverKind } from "./vcs.ts";11import { VcsDriverKind } from "./vcs.ts";
1212
13const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;13const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
14const GitFilePath = Schema.String.check(Schema.isNonEmpty());
14const GIT_LIST_BRANCHES_MAX_LIMIT = 200;15const GIT_LIST_BRANCHES_MAX_LIMIT = 200;
1516
16// Domain Types17// Domain Types
...@@ -129,9 +130,7 @@ export const GitRunStackedActionInput = Schema.Struct({...@@ -129,9 +130,7 @@ export const GitRunStackedActionInput = Schema.Struct({
129 action: GitStackedAction,130 action: GitStackedAction,
130 commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))),131 commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))),
131 featureBranch: Schema.optional(Schema.Boolean),132 featureBranch: Schema.optional(Schema.Boolean),
132 filePaths: Schema.optional(133 filePaths: Schema.optional(Schema.Array(GitFilePath).check(Schema.isMinLength(1))),
133 Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)),
134 ),
135 /** The thread the action runs beside; a pull request it creates is linked to it. */134 /** The thread the action runs beside; a pull request it creates is linked to it. */
136 threadId: Schema.optional(ThreadId),135 threadId: Schema.optional(ThreadId),
137 projectId: Schema.optional(ProjectId),136 projectId: Schema.optional(ProjectId),
...@@ -234,7 +233,7 @@ const VcsStatusLocalShape = {...@@ -234,7 +233,7 @@ const VcsStatusLocalShape = {
234 workingTree: Schema.Struct({233 workingTree: Schema.Struct({
235 files: Schema.Array(234 files: Schema.Array(
236 Schema.Struct({235 Schema.Struct({
237 path: TrimmedNonEmptyStringSchema,236 path: GitFilePath,
238 insertions: NonNegativeInt,237 insertions: NonNegativeInt,
239 deletions: NonNegativeInt,238 deletions: NonNegativeInt,
240 }),239 }),