From 71cf0eef3b52077713c011a56505cb5933a47a8d Mon Sep 17 00:00:00 2001 From: Jean-Dominique Stepek Date: Sun, 15 Mar 2026 21:35:50 -0400 Subject: [PATCH] fix: git commands fail when repo path contains spaces (#561) * fix: use execFileSync for git commands to handle paths with spaces execSync builds a shell command string via string interpolation, so any path containing spaces (e.g. 'Current Projects/my-repo') gets word-split by the shell into multiple arguments. This caused 'git worktree add' to fail with a usage error whenever the repo was in a directory with spaces. Switch all three git runner functions to execFileSync, which takes args as an array and bypasses the shell entirely. Paths are passed as discrete arguments and never subject to word-splitting or other shell expansions. Affected files: - worktree-manager.ts: runGit() - git-service.ts: runGit() - native-git-bridge.ts: gitExec() * fix: restore pre-merge check command execution --- src/resources/extensions/gsd/git-service.ts | 6 +++--- src/resources/extensions/gsd/native-git-bridge.ts | 6 +++--- src/resources/extensions/gsd/worktree-manager.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/resources/extensions/gsd/git-service.ts b/src/resources/extensions/gsd/git-service.ts index f96066d0d..36b018aa3 100644 --- a/src/resources/extensions/gsd/git-service.ts +++ b/src/resources/extensions/gsd/git-service.ts @@ -8,7 +8,7 @@ * paths, commit type inference, and the runGit shell helper. */ -import { execSync } from "node:child_process"; +import { execFileSync, execSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join, sep } from "node:path"; @@ -212,7 +212,7 @@ function filterGitSvnNoise(message: string): string { */ export function runGit(basePath: string, args: string[], options: { allowFailure?: boolean; input?: string } = {}): string { try { - return execSync(`git ${args.join(" ")}`, { + return execFileSync("git", args, { cwd: basePath, stdio: [options.input != null ? "pipe" : "ignore", "pipe", "pipe"], encoding: "utf-8", @@ -452,7 +452,7 @@ export class GitServiceImpl { } else { // Auto-detect: look for package.json with a test script try { - const pkg = execSync("cat package.json", { cwd: this.basePath, encoding: "utf-8" }); + const pkg = execFileSync("cat", ["package.json"], { cwd: this.basePath, encoding: "utf-8" }); const parsed = JSON.parse(pkg); if (parsed.scripts?.test) { command = "npm test"; diff --git a/src/resources/extensions/gsd/native-git-bridge.ts b/src/resources/extensions/gsd/native-git-bridge.ts index e613409a5..2e0e62d9b 100644 --- a/src/resources/extensions/gsd/native-git-bridge.ts +++ b/src/resources/extensions/gsd/native-git-bridge.ts @@ -5,7 +5,7 @@ // Only READ operations are native — WRITE operations (commit, merge, checkout, push) // remain as execSync calls in git-service.ts. -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; /** Env overlay that suppresses interactive git credential prompts and git-svn noise. */ const GIT_NO_PROMPT_ENV = { @@ -44,10 +44,10 @@ function loadNative(): typeof nativeModule { return nativeModule; } -/** Run a git command via execSync. Returns trimmed stdout. */ +/** Run a git command via execFileSync. Returns trimmed stdout. */ function gitExec(basePath: string, args: string[], allowFailure = false): string { try { - return execSync(`git ${args.join(" ")}`, { + return execFileSync("git", args, { cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", diff --git a/src/resources/extensions/gsd/worktree-manager.ts b/src/resources/extensions/gsd/worktree-manager.ts index 6696b7cf8..847dc4061 100644 --- a/src/resources/extensions/gsd/worktree-manager.ts +++ b/src/resources/extensions/gsd/worktree-manager.ts @@ -16,7 +16,7 @@ */ import { existsSync, mkdirSync, realpathSync } from "node:fs"; -import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; import { join, resolve, sep } from "node:path"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -69,7 +69,7 @@ function filterGitSvnNoise(message: string): string { function runGit(cwd: string, args: string[], opts: { allowFailure?: boolean } = {}): string { try { - return execSync(`git ${args.join(" ")}`, { + return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8",