| ... | ... | @@ -15,8 +15,46 @@ import type { |
| 15 | 15 | TerminalCommandResult, |
| 16 | 16 | } from "@t3tools/contracts"; |
| 17 | 17 | |
| 18 | | function escapeSingleQuotes(value: string): string { |
| 19 | | return value.replace(/'/g, "'\\''"); |
| 18 | /** Spawn git directly with an argv array — no shell, no quoting needed. */ |
| 19 | function runGit( |
| 20 | args: string[], |
| 21 | cwd: string, |
| 22 | timeoutMs = 30_000, |
| 23 | ): Promise<TerminalCommandResult> { |
| 24 | return new Promise((resolve, reject) => { |
| 25 | const child = spawn("git", args, { |
| 26 | cwd, |
| 27 | env: process.env, |
| 28 | stdio: ["ignore", "pipe", "pipe"], |
| 29 | }); |
| 30 | |
| 31 | let stdout = ""; |
| 32 | let stderr = ""; |
| 33 | let timedOut = false; |
| 34 | |
| 35 | const timeout = setTimeout(() => { |
| 36 | timedOut = true; |
| 37 | child.kill("SIGTERM"); |
| 38 | setTimeout(() => { |
| 39 | if (!child.killed) child.kill("SIGKILL"); |
| 40 | }, 1_000).unref(); |
| 41 | }, timeoutMs); |
| 42 | |
| 43 | child.stdout?.on("data", (chunk: Buffer) => { |
| 44 | stdout += chunk.toString(); |
| 45 | }); |
| 46 | child.stderr?.on("data", (chunk: Buffer) => { |
| 47 | stderr += chunk.toString(); |
| 48 | }); |
| 49 | child.on("error", (error) => { |
| 50 | clearTimeout(timeout); |
| 51 | reject(error); |
| 52 | }); |
| 53 | child.on("close", (code, signal) => { |
| 54 | clearTimeout(timeout); |
| 55 | resolve({ stdout, stderr, code: code ?? null, signal: signal ?? null, timedOut }); |
| 56 | }); |
| 57 | }); |
| 20 | 58 | } |
| 21 | 59 | |
| 22 | 60 | export async function runTerminalCommand( |
| ... | ... | @@ -78,11 +116,7 @@ export async function runTerminalCommand( |
| 78 | 116 | } |
| 79 | 117 | |
| 80 | 118 | export async function listGitBranches(input: GitListBranchesInput): Promise<GitListBranchesResult> { |
| 81 | | const result = await runTerminalCommand({ |
| 82 | | command: "git branch --no-color", |
| 83 | | cwd: input.cwd, |
| 84 | | timeoutMs: 10_000, |
| 85 | | }); |
| 119 | const result = await runGit(["branch", "--no-color"], input.cwd, 10_000); |
| 86 | 120 | |
| 87 | 121 | if (result.code !== 0) { |
| 88 | 122 | const stderr = result.stderr.trim(); |
| ... | ... | @@ -93,11 +127,11 @@ export async function listGitBranches(input: GitListBranchesInput): Promise<GitL |
| 93 | 127 | } |
| 94 | 128 | |
| 95 | 129 | // Resolve the real default branch from the remote |
| 96 | | const defaultRef = await runTerminalCommand({ |
| 97 | | command: "git symbolic-ref refs/remotes/origin/HEAD", |
| 98 | | cwd: input.cwd, |
| 99 | | timeoutMs: 5_000, |
| 100 | | }); |
| 130 | const defaultRef = await runGit( |
| 131 | ["symbolic-ref", "refs/remotes/origin/HEAD"], |
| 132 | input.cwd, |
| 133 | 5_000, |
| 134 | ); |
| 101 | 135 | const defaultBranch = |
| 102 | 136 | defaultRef.code === 0 ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") : null; |
| 103 | 137 | |
| ... | ... | @@ -128,11 +162,10 @@ export async function createGitWorktree( |
| 128 | 162 | input.path ?? path.join(os.homedir(), ".t3", "worktrees", repoName, sanitizedBranch); |
| 129 | 163 | |
| 130 | 164 | // Create a new branch from the base branch in a new worktree |
| 131 | | const result = await runTerminalCommand({ |
| 132 | | command: `git worktree add -b '${escapeSingleQuotes(input.newBranch)}' '${escapeSingleQuotes(worktreePath)}' '${escapeSingleQuotes(input.branch)}'`, |
| 133 | | cwd: input.cwd, |
| 134 | | timeoutMs: 30_000, |
| 135 | | }); |
| 165 | const result = await runGit( |
| 166 | ["worktree", "add", "-b", input.newBranch, worktreePath, input.branch], |
| 167 | input.cwd, |
| 168 | ); |
| 136 | 169 | |
| 137 | 170 | if (result.code !== 0) { |
| 138 | 171 | throw new Error(result.stderr.trim() || "git worktree add failed"); |
| ... | ... | @@ -147,11 +180,7 @@ export async function createGitWorktree( |
| 147 | 180 | } |
| 148 | 181 | |
| 149 | 182 | export async function removeGitWorktree(input: GitRemoveWorktreeInput): Promise<void> { |
| 150 | | const result = await runTerminalCommand({ |
| 151 | | command: `git worktree remove '${escapeSingleQuotes(input.path)}'`, |
| 152 | | cwd: input.cwd, |
| 153 | | timeoutMs: 15_000, |
| 154 | | }); |
| 183 | const result = await runGit(["worktree", "remove", input.path], input.cwd, 15_000); |
| 155 | 184 | |
| 156 | 185 | if (result.code !== 0) { |
| 157 | 186 | throw new Error(result.stderr.trim() || "git worktree remove failed"); |
| ... | ... | @@ -159,11 +188,7 @@ export async function removeGitWorktree(input: GitRemoveWorktreeInput): Promise< |
| 159 | 188 | } |
| 160 | 189 | |
| 161 | 190 | export async function createGitBranch(input: GitCreateBranchInput): Promise<void> { |
| 162 | | const result = await runTerminalCommand({ |
| 163 | | command: `git branch '${escapeSingleQuotes(input.branch)}'`, |
| 164 | | cwd: input.cwd, |
| 165 | | timeoutMs: 10_000, |
| 166 | | }); |
| 191 | const result = await runGit(["branch", input.branch], input.cwd, 10_000); |
| 167 | 192 | |
| 168 | 193 | if (result.code !== 0) { |
| 169 | 194 | throw new Error(result.stderr.trim() || "git branch create failed"); |
| ... | ... | @@ -171,11 +196,7 @@ export async function createGitBranch(input: GitCreateBranchInput): Promise<void |
| 171 | 196 | } |
| 172 | 197 | |
| 173 | 198 | export async function checkoutGitBranch(input: GitCheckoutInput): Promise<void> { |
| 174 | | const result = await runTerminalCommand({ |
| 175 | | command: `git checkout '${escapeSingleQuotes(input.branch)}'`, |
| 176 | | cwd: input.cwd, |
| 177 | | timeoutMs: 10_000, |
| 178 | | }); |
| 199 | const result = await runGit(["checkout", input.branch], input.cwd, 10_000); |
| 179 | 200 | |
| 180 | 201 | if (result.code !== 0) { |
| 181 | 202 | throw new Error(result.stderr.trim() || "git checkout failed"); |
| ... | ... | @@ -183,11 +204,7 @@ export async function checkoutGitBranch(input: GitCheckoutInput): Promise<void> |
| 183 | 204 | } |
| 184 | 205 | |
| 185 | 206 | export async function initGitRepo(input: GitInitInput): Promise<void> { |
| 186 | | const result = await runTerminalCommand({ |
| 187 | | command: "git init", |
| 188 | | cwd: input.cwd, |
| 189 | | timeoutMs: 10_000, |
| 190 | | }); |
| 207 | const result = await runGit(["init"], input.cwd, 10_000); |
| 191 | 208 | |
| 192 | 209 | if (result.code !== 0) { |
| 193 | 210 | throw new Error(result.stderr.trim() || "git init failed"); |