Skip to content

Conversation

@cte
Copy link
Collaborator

@cte cte commented Jan 5, 2026

Closes #10646

image image

Thanks to Cline / Saoud for the inspiration.

I had Opus take a peek at their WIP implementation and port it over.

I also added a --worktree option to the Roo Code cli which will automatically run your task in a new worktree.

Screenshot 2026-01-04 at 10 50 13 PM Screenshot 2026-01-04 at 10 50 37 PM Screenshot 2026-01-04 at 10 52 33 PM

Important

This pull request adds comprehensive Git worktree management to the Roo Code extension, including UI components, backend handlers, and internationalization support for creating, deleting, and managing worktrees.

  • Behavior:
    • Adds Git worktree management to the Roo Code extension, allowing users to create, delete, and switch worktrees.
    • Supports merging worktrees and resolving conflicts, with options to delete worktrees post-merge.
    • Handles .worktreeinclude files for copying untracked files to new worktrees.
  • UI Components:
    • Introduces CreateWorktreeModal, DeleteWorktreeModal, and WorktreesView for managing worktrees in the UI.
    • Updates App.tsx to include a new "worktrees" tab and handle related actions.
    • Adds internationalization support for worktree-related strings in multiple languages.
  • Backend:
    • Implements handlers in handlers.ts for worktree operations, including creation, deletion, and merging.
    • Updates extension.ts to check for worktree auto-open paths and initialize worktree-related commands.
    • Adds new commands to package.json for worktree management.

This description was created by Ellipsis for f538743. You can customize this summary. It will automatically update as commits are pushed.

@cte cte requested review from jr and mrubens as code owners January 5, 2026 06:57
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. Enhancement New feature or request labels Jan 5, 2026
@roomote
Copy link
Contributor

roomote bot commented Jan 5, 2026

Rooviewer Clock   See task on Roo Cloud

All previously flagged issues have been resolved. No new issues found in the latest changes.

  • Security: Remove hardcoded API key in apps/cli/src/index.ts:132
  • Bug: Fix branch deletion logic in deleteWorktree() - worktree info must be retrieved before deletion, not after
  • Bug: Fix worktreeIncludeStatus message handling in WorktreesView.tsx - line 73 should extract message.worktreeIncludeStatus instead of storing entire message
  • Bug: Fix worktreeIncludeStatus message handling in CreateWorktreeModal.tsx - line 76 should extract message.worktreeIncludeStatus instead of storing entire message
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

@hannesrudolph hannesrudolph added the Issue/PR - Triage New issue. Needs quick review to confirm validity and assign labels. label Jan 5, 2026
Base automatically changed from cte/cli-evals to main January 5, 2026 19:33
* Normalize a path for comparison (handle trailing slashes, etc.)
*/
private normalizePath(p: string): string {
return path.normalize(p).replace(/\/+$/, "")

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
library input
may run slow on strings with many repetitions of '/'.
}
} else if (branch) {
// Checkout existing branch: git worktree add <path> <branch>
command += ` "${worktreePath}" "${branch}"`

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.
This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 4 days ago

In general, the fix is to avoid constructing a single shell command string from dynamic input and running it with exec, and instead invoke the underlying executable (git) with an argument array using execFile (or spawn). This prevents the shell from interpreting special characters in branch, baseBranch, or worktreePath, eliminating injection risks and making handling of spaces and quoting robust.

Concretely for createWorktree in packages/core/src/worktree/worktree-service.ts, we should:

  • Keep using exec for fixed, constant commands like "git --version" and "git rev-parse ...", which are not built from tainted input.
  • Change only the createWorktree method to build an array args: string[] equivalent to the current command and run git via execFile.
  • Reuse the existing execAsync wrapper, but extend its type to allow passing an execFile-compatible function, or more simply, add a small local promisified wrapper around execFile just for this method.

Minimal, focused approach:

  • Import execFile from child_process alongside exec.
  • Create const execFileAsync = promisify(execFile) near execAsync.
  • In createWorktree, replace the let command = ... string building with construction of const args: string[] = [...] based on createNewBranch, branch, and baseBranch:
    • Always start with ["worktree", "add"].
    • If createNewBranch && branch: push "-b", branch, worktreePath, and optionally baseBranch.
    • Else if branch: push worktreePath, branch.
    • Else: push "--detach", worktreePath.
  • Replace await execAsync(command, { cwd }) with await execFileAsync("git", args, { cwd }).

This preserves existing functionality and messages, but removes shell interpretation of user-controlled inputs.

Suggested changeset 1
packages/core/src/worktree/worktree-service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/core/src/worktree/worktree-service.ts b/packages/core/src/worktree/worktree-service.ts
--- a/packages/core/src/worktree/worktree-service.ts
+++ b/packages/core/src/worktree/worktree-service.ts
@@ -5,7 +5,7 @@
  * Uses simple-git and native CLI commands - no VSCode dependencies.
  */
 
-import { exec } from "child_process"
+import { exec, execFile } from "child_process"
 import * as path from "path"
 import { promisify } from "util"
 
@@ -19,6 +19,7 @@
 } from "./types.js"
 
 const execAsync = promisify(exec)
+const execFileAsync = promisify(execFile)
 
 /**
  * Service for managing git worktrees.
@@ -105,24 +106,24 @@
 		try {
 			const { path: worktreePath, branch, baseBranch, createNewBranch } = options
 
-			// Build the git worktree add command
-			let command = `git worktree add`
+			// Build the git worktree add command arguments
+			const args: string[] = ["worktree", "add"]
 
 			if (createNewBranch && branch) {
 				// Create new branch: git worktree add -b <branch> <path> [<base>]
-				command += ` -b "${branch}" "${worktreePath}"`
+				args.push("-b", branch, worktreePath)
 				if (baseBranch) {
-					command += ` "${baseBranch}"`
+					args.push(baseBranch)
 				}
 			} else if (branch) {
 				// Checkout existing branch: git worktree add <path> <branch>
-				command += ` "${worktreePath}" "${branch}"`
+				args.push(worktreePath, branch)
 			} else {
 				// Detached HEAD at current commit
-				command += ` --detach "${worktreePath}"`
+				args.push("--detach", worktreePath)
 			}
 
-			await execAsync(command, { cwd })
+			await execFileAsync("git", args, { cwd })
 
 			// Get the created worktree info
 			const worktrees = await this.listWorktrees(cwd)
EOF
@@ -5,7 +5,7 @@
* Uses simple-git and native CLI commands - no VSCode dependencies.
*/

import { exec } from "child_process"
import { exec, execFile } from "child_process"
import * as path from "path"
import { promisify } from "util"

@@ -19,6 +19,7 @@
} from "./types.js"

const execAsync = promisify(exec)
const execFileAsync = promisify(execFile)

/**
* Service for managing git worktrees.
@@ -105,24 +106,24 @@
try {
const { path: worktreePath, branch, baseBranch, createNewBranch } = options

// Build the git worktree add command
let command = `git worktree add`
// Build the git worktree add command arguments
const args: string[] = ["worktree", "add"]

if (createNewBranch && branch) {
// Create new branch: git worktree add -b <branch> <path> [<base>]
command += ` -b "${branch}" "${worktreePath}"`
args.push("-b", branch, worktreePath)
if (baseBranch) {
command += ` "${baseBranch}"`
args.push(baseBranch)
}
} else if (branch) {
// Checkout existing branch: git worktree add <path> <branch>
command += ` "${worktreePath}" "${branch}"`
args.push(worktreePath, branch)
} else {
// Detached HEAD at current commit
command += ` --detach "${worktreePath}"`
args.push("--detach", worktreePath)
}

await execAsync(command, { cwd })
await execFileAsync("git", args, { cwd })

// Get the created worktree info
const worktrees = await this.listWorktrees(cwd)
Copilot is powered by AI and may make mistakes. Always verify output.
command += ` "${worktreePath}" "${branch}"`
} else {
// Detached HEAD at current commit
command += ` --detach "${worktreePath}"`

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 4 days ago

In general, the fix is to stop constructing a single shell command string with unescaped input and instead invoke Git using child_process.execFile (or spawn) with an argument array. This prevents the shell from interpreting special characters in worktreePath, branch, or baseBranch. Since listWorktrees already uses a static command string and does not include tainted input, only createWorktree needs changes for this specific issue.

Concretely, in packages/core/src/worktree/worktree-service.ts, within createWorktree, replace the string-based command building logic and the execAsync(command, { cwd }) call with construction of an array of arguments, then call execFile (promisified) with "git" as the executable and the arguments array. Keep the operational semantics the same: when createNewBranch && branch, run git worktree add -b <branch> <path> [<baseBranch>]; when branch only, run git worktree add <path> <branch>; otherwise run git worktree add --detach <path>. To implement this, we will:

  • Import execFile from child_process in addition to exec.
  • Create a new execFileAsync constant via promisify(execFile).
  • In createWorktree, replace the command string with a const args: string[] = ["worktree", "add"] and push appropriate arguments based on the conditions.
  • Replace await execAsync(command, { cwd }) with await execFileAsync("git", args, { cwd }).

This keeps functionality unchanged while eliminating shell interpretation of tainted inputs.

Suggested changeset 1
packages/core/src/worktree/worktree-service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/core/src/worktree/worktree-service.ts b/packages/core/src/worktree/worktree-service.ts
--- a/packages/core/src/worktree/worktree-service.ts
+++ b/packages/core/src/worktree/worktree-service.ts
@@ -5,7 +5,7 @@
  * Uses simple-git and native CLI commands - no VSCode dependencies.
  */
 
-import { exec } from "child_process"
+import { exec, execFile } from "child_process"
 import * as path from "path"
 import { promisify } from "util"
 
@@ -19,6 +19,7 @@
 } from "./types.js"
 
 const execAsync = promisify(exec)
+const execFileAsync = promisify(execFile)
 
 /**
  * Service for managing git worktrees.
@@ -105,24 +106,24 @@
 		try {
 			const { path: worktreePath, branch, baseBranch, createNewBranch } = options
 
-			// Build the git worktree add command
-			let command = `git worktree add`
+			// Build the git worktree add command arguments
+			const args: string[] = ["worktree", "add"]
 
 			if (createNewBranch && branch) {
 				// Create new branch: git worktree add -b <branch> <path> [<base>]
-				command += ` -b "${branch}" "${worktreePath}"`
+				args.push("-b", branch, worktreePath)
 				if (baseBranch) {
-					command += ` "${baseBranch}"`
+					args.push(baseBranch)
 				}
 			} else if (branch) {
 				// Checkout existing branch: git worktree add <path> <branch>
-				command += ` "${worktreePath}" "${branch}"`
+				args.push(worktreePath, branch)
 			} else {
 				// Detached HEAD at current commit
-				command += ` --detach "${worktreePath}"`
+				args.push("--detach", worktreePath)
 			}
 
-			await execAsync(command, { cwd })
+			await execFileAsync("git", args, { cwd })
 
 			// Get the created worktree info
 			const worktrees = await this.listWorktrees(cwd)
EOF
@@ -5,7 +5,7 @@
* Uses simple-git and native CLI commands - no VSCode dependencies.
*/

import { exec } from "child_process"
import { exec, execFile } from "child_process"
import * as path from "path"
import { promisify } from "util"

@@ -19,6 +19,7 @@
} from "./types.js"

const execAsync = promisify(exec)
const execFileAsync = promisify(execFile)

/**
* Service for managing git worktrees.
@@ -105,24 +106,24 @@
try {
const { path: worktreePath, branch, baseBranch, createNewBranch } = options

// Build the git worktree add command
let command = `git worktree add`
// Build the git worktree add command arguments
const args: string[] = ["worktree", "add"]

if (createNewBranch && branch) {
// Create new branch: git worktree add -b <branch> <path> [<base>]
command += ` -b "${branch}" "${worktreePath}"`
args.push("-b", branch, worktreePath)
if (baseBranch) {
command += ` "${baseBranch}"`
args.push(baseBranch)
}
} else if (branch) {
// Checkout existing branch: git worktree add <path> <branch>
command += ` "${worktreePath}" "${branch}"`
args.push(worktreePath, branch)
} else {
// Detached HEAD at current commit
command += ` --detach "${worktreePath}"`
args.push("--detach", worktreePath)
}

await execAsync(command, { cwd })
await execFileAsync("git", args, { cwd })

// Get the created worktree info
const worktrees = await this.listWorktrees(cwd)
Copilot is powered by AI and may make mistakes. Always verify output.
)

const forceFlag = force ? " --force" : ""
await execAsync(`git worktree remove${forceFlag} "${worktreePath}"`, { cwd })

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.
This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 4 days ago

In general, the fix is to avoid passing a single interpolated command string to child_process.exec when it contains dynamic values. Instead, use an API that accepts the command and its arguments as an array and does not invoke a shell, such as child_process.execFile or child_process.spawn/spawnSync. This way, dynamic values are treated purely as arguments and are not interpreted by the shell, eliminating command-injection risks and making paths with spaces work correctly.

For this file, the best targeted change is:

  • Import execFile from child_process.
  • Create a promisified execFileAsync alongside execAsync.
  • For the vulnerable calls that interpolate worktreePath and branch names, switch from execAsync with a templated string to execFileAsync with a program and argument list:
    • Replace execAsync(git worktree remove${forceFlag} "${worktreePath}", { cwd }) with execFileAsync("git", ["worktree", "remove", ...(force ? ["--force"] : []), worktreePath], { cwd }).
    • Replace execAsync(git branch -d "${worktreeToDelete.branch}", { cwd }) with execFileAsync("git", ["branch", "-d", worktreeToDelete.branch], { cwd }).

These changes preserve behaviour (same commands and options, still run in cwd) but avoid shell interpretation of worktreePath and branch names. Other uses of execAsync in this file that do not interpolate tainted values (like fixed git branch --format=...) can be left as-is, since they are not constructing commands from untrusted input.

Concretely:

  • In packages/core/src/worktree/worktree-service.ts, adjust the import on line 8 to include execFile, and add a new execFileAsync constant after execAsync.
  • In the deleteWorktree method, refactor the two execAsync calls on lines 159 and 164 to use execFileAsync with argument arrays as described above.
Suggested changeset 1
packages/core/src/worktree/worktree-service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/core/src/worktree/worktree-service.ts b/packages/core/src/worktree/worktree-service.ts
--- a/packages/core/src/worktree/worktree-service.ts
+++ b/packages/core/src/worktree/worktree-service.ts
@@ -5,7 +5,7 @@
  * Uses simple-git and native CLI commands - no VSCode dependencies.
  */
 
-import { exec } from "child_process"
+import { exec, execFile } from "child_process"
 import * as path from "path"
 import { promisify } from "util"
 
@@ -19,6 +19,7 @@
 } from "./types.js"
 
 const execAsync = promisify(exec)
+const execFileAsync = promisify(execFile)
 
 /**
  * Service for managing git worktrees.
@@ -155,13 +156,13 @@
 				(wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath),
 			)
 
-			const forceFlag = force ? " --force" : ""
-			await execAsync(`git worktree remove${forceFlag} "${worktreePath}"`, { cwd })
+			const forceArgs = force ? ["--force"] : []
+			await execFileAsync("git", ["worktree", "remove", ...forceArgs, worktreePath], { cwd })
 
 			// Also try to delete the branch if it exists
 			if (worktreeToDelete?.branch) {
 				try {
-					await execAsync(`git branch -d "${worktreeToDelete.branch}"`, { cwd })
+					await execFileAsync("git", ["branch", "-d", worktreeToDelete.branch], { cwd })
 				} catch {
 					// Branch deletion is best-effort
 				}
EOF
@@ -5,7 +5,7 @@
* Uses simple-git and native CLI commands - no VSCode dependencies.
*/

import { exec } from "child_process"
import { exec, execFile } from "child_process"
import * as path from "path"
import { promisify } from "util"

@@ -19,6 +19,7 @@
} from "./types.js"

const execAsync = promisify(exec)
const execFileAsync = promisify(execFile)

/**
* Service for managing git worktrees.
@@ -155,13 +156,13 @@
(wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath),
)

const forceFlag = force ? " --force" : ""
await execAsync(`git worktree remove${forceFlag} "${worktreePath}"`, { cwd })
const forceArgs = force ? ["--force"] : []
await execFileAsync("git", ["worktree", "remove", ...forceArgs, worktreePath], { cwd })

// Also try to delete the branch if it exists
if (worktreeToDelete?.branch) {
try {
await execAsync(`git branch -d "${worktreeToDelete.branch}"`, { cwd })
await execFileAsync("git", ["branch", "-d", worktreeToDelete.branch], { cwd })
} catch {
// Branch deletion is best-effort
}
Copilot is powered by AI and may make mistakes. Always verify output.
}

// Ensure we're on the target branch
await execAsync(`git checkout "${targetBranch}"`, { cwd: mergeCwd })

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 4 days ago

In general, the safest fix is to avoid interpolating untrusted data into a shell command string. Instead, use child_process.execFile or spawn with an argument array so the shell does not interpret the arguments. When using execFile, you pass the program name (git) and an array of arguments (e.g. ["checkout", targetBranch]), and the OS will invoke the program directly without passing through a shell. This preserves functionality but removes shell-injection risk.

In this file, execAsync is a promisified exec, which always goes through the shell and currently runs commands like git checkout "${targetBranch}" and git merge "${sourceBranch}" --no-edit. To fix the specific vulnerable use at line 283 without changing external behavior, introduce a separate promisified execFile helper (for example, execFileAsync) and use it for these git operations. Replace execAsync calls that interpolate targetBranch/sourceBranch into a shell string with execFileAsync("git", [...args], { cwd }). For the status check, we can also safely switch to execFileAsync("git", ["status", "--porcelain"], ...), which avoids using the shell at all. Concretely:

  • Add an import of execFile from child_process.
  • Add const execFileAsync = promisify(execFile) next to execAsync.
  • Change:
    • execAsync("git status --porcelain", { cwd: worktreePath }) to execFileAsync("git", ["status", "--porcelain"], { cwd: worktreePath }).
    • execAsync(\git checkout "${targetBranch}"`, { cwd: mergeCwd })toexecFileAsync("git", ["checkout", targetBranch], { cwd: mergeCwd })`.
    • execAsync(\git merge "${sourceBranch}" --no-edit`, { cwd: mergeCwd })toexecFileAsync("git", ["merge", sourceBranch, "--no-edit"], { cwd: mergeCwd })`.

This keeps behavior (same git commands, same cwd) while ensuring targetBranch and sourceBranch are passed as arguments instead of being parsed by a shell.

Suggested changeset 1
packages/core/src/worktree/worktree-service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/core/src/worktree/worktree-service.ts b/packages/core/src/worktree/worktree-service.ts
--- a/packages/core/src/worktree/worktree-service.ts
+++ b/packages/core/src/worktree/worktree-service.ts
@@ -5,7 +5,7 @@
  * Uses simple-git and native CLI commands - no VSCode dependencies.
  */
 
-import { exec } from "child_process"
+import { exec, execFile } from "child_process"
 import * as path from "path"
 import { promisify } from "util"
 
@@ -19,6 +19,7 @@
 } from "./types.js"
 
 const execAsync = promisify(exec)
+const execFileAsync = promisify(execFile)
 
 /**
  * Service for managing git worktrees.
@@ -264,7 +265,7 @@
 
 			// Check for uncommitted changes in source worktree
 			try {
-				const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath })
+				const { stdout: statusOutput } = await execFileAsync("git", ["status", "--porcelain"], { cwd: worktreePath })
 				if (statusOutput.trim()) {
 					return {
 						success: false,
@@ -280,11 +281,11 @@
 			}
 
 			// Ensure we're on the target branch
-			await execAsync(`git checkout "${targetBranch}"`, { cwd: mergeCwd })
+			await execFileAsync("git", ["checkout", targetBranch], { cwd: mergeCwd })
 
 			// Attempt the merge
 			try {
-				await execAsync(`git merge "${sourceBranch}" --no-edit`, { cwd: mergeCwd })
+				await execFileAsync("git", ["merge", sourceBranch, "--no-edit"], { cwd: mergeCwd })
 
 				// Merge succeeded
 				if (deleteAfterMerge) {
EOF
@@ -5,7 +5,7 @@
* Uses simple-git and native CLI commands - no VSCode dependencies.
*/

import { exec } from "child_process"
import { exec, execFile } from "child_process"
import * as path from "path"
import { promisify } from "util"

@@ -19,6 +19,7 @@
} from "./types.js"

const execAsync = promisify(exec)
const execFileAsync = promisify(execFile)

/**
* Service for managing git worktrees.
@@ -264,7 +265,7 @@

// Check for uncommitted changes in source worktree
try {
const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath })
const { stdout: statusOutput } = await execFileAsync("git", ["status", "--porcelain"], { cwd: worktreePath })
if (statusOutput.trim()) {
return {
success: false,
@@ -280,11 +281,11 @@
}

// Ensure we're on the target branch
await execAsync(`git checkout "${targetBranch}"`, { cwd: mergeCwd })
await execFileAsync("git", ["checkout", targetBranch], { cwd: mergeCwd })

// Attempt the merge
try {
await execAsync(`git merge "${sourceBranch}" --no-edit`, { cwd: mergeCwd })
await execFileAsync("git", ["merge", sourceBranch, "--no-edit"], { cwd: mergeCwd })

// Merge succeeded
if (deleteAfterMerge) {
Copilot is powered by AI and may make mistakes. Always verify output.
*/
async checkoutBranch(cwd: string, branch: string): Promise<WorktreeResult> {
try {
await execAsync(`git checkout "${branch}"`, { cwd })

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 4 days ago

In general, the safe way to fix this issue is to avoid constructing shell command strings that embed untrusted input and then passing them to exec, which invokes a shell. Instead, use APIs such as child_process.execFile or spawn with an argument array so that the untrusted value is passed as a separate argument and not interpreted by a shell. This way, even if the value contains spaces or shell metacharacters, they are treated as literal argument content, not as shell syntax.

For this specific case, the best fix with minimal behavior change is to:

  • Replace the use of execAsync (which wraps exec) with a direct use of child_process.execFile (or a promisified variant) for the git checkout call.
  • Pass "git" as the command and ["checkout", branch] as the argument array, using the existing cwd option.
  • Avoid manual quoting entirely; let execFile handle argument passing to git.

To keep changes localized:

  • Import execFile from child_process alongside the existing exec import.
  • Create a promisify(execFile) helper (e.g., execFileAsync) near the existing execAsync declaration.
  • In checkoutBranch, replace await execAsync(\git checkout "${branch}"`, { cwd })withawait execFileAsync("git", ["checkout", branch], { cwd })`.
    No other functionality or method signatures need to change.
Suggested changeset 1
packages/core/src/worktree/worktree-service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/core/src/worktree/worktree-service.ts b/packages/core/src/worktree/worktree-service.ts
--- a/packages/core/src/worktree/worktree-service.ts
+++ b/packages/core/src/worktree/worktree-service.ts
@@ -5,7 +5,7 @@
  * Uses simple-git and native CLI commands - no VSCode dependencies.
  */
 
-import { exec } from "child_process"
+import { exec, execFile } from "child_process"
 import * as path from "path"
 import { promisify } from "util"
 
@@ -19,6 +19,7 @@
 } from "./types.js"
 
 const execAsync = promisify(exec)
+const execFileAsync = promisify(execFile)
 
 /**
  * Service for managing git worktrees.
@@ -355,7 +356,7 @@
 	 */
 	async checkoutBranch(cwd: string, branch: string): Promise<WorktreeResult> {
 		try {
-			await execAsync(`git checkout "${branch}"`, { cwd })
+			await execFileAsync("git", ["checkout", branch], { cwd })
 			return {
 				success: true,
 				message: `Checked out branch ${branch}`,
EOF
@@ -5,7 +5,7 @@
* Uses simple-git and native CLI commands - no VSCode dependencies.
*/

import { exec } from "child_process"
import { exec, execFile } from "child_process"
import * as path from "path"
import { promisify } from "util"

@@ -19,6 +19,7 @@
} from "./types.js"

const execAsync = promisify(exec)
const execFileAsync = promisify(execFile)

/**
* Service for managing git worktrees.
@@ -355,7 +356,7 @@
*/
async checkoutBranch(cwd: string, branch: string): Promise<WorktreeResult> {
try {
await execAsync(`git checkout "${branch}"`, { cwd })
await execFileAsync("git", ["checkout", branch], { cwd })
return {
success: true,
message: `Checked out branch ${branch}`,
Copilot is powered by AI and may make mistakes. Always verify output.
@hannesrudolph hannesrudolph moved this from Triage to PR [Needs Prelim Review] in Roo Code Roadmap Jan 6, 2026
@hannesrudolph hannesrudolph moved this from PR [Needs Prelim Review] to PR [Needs Review] in Roo Code Roadmap Jan 6, 2026
@hannesrudolph hannesrudolph added PR - Needs Review and removed Issue/PR - Triage New issue. Needs quick review to confirm validity and assign labels. labels Jan 6, 2026
cte and others added 11 commits January 9, 2026 13:05
- Add worktrees.json translation files for all 17 locales
- Add command.worktrees.title translation to all package.nls locale files
Performance:
- Parallelize git commands in getAvailableBranches() using Promise.all
- Add maxDisplayItems prop to SearchableSelect (default 50) to improve dropdown performance
- Disable cmdk's slow built-in filtering for large option lists

UX Improvements:
- Add loading spinner while branches load with i18n support
- Add onWheel handler to CommandList for VSCode webview scroll compatibility
- Round modal input fields with rounded-full class
- Apply consistent WorktreesView styling (rounded-xl, transparent borders, hover effects)

Branch .worktreeinclude Check:
- Add branchHasWorktreeInclude() to check if selected base branch has .worktreeinclude committed
- Show warning when base branch doesn't have .worktreeinclude (not local filesystem)
- Add checkBranchWorktreeInclude message type and handler

Translations:
- Add searchBranch, noBranchFound, loadingBranches keys to all 18 locales
The test performs 9 sequential git operations which can exceed the default
5000ms timeout on Windows CI. Increased to 30s for reliability.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request PR - Needs Preliminary Review size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

Status: PR [Needs Review]

Development

Successfully merging this pull request may close these issues.

[ENHANCEMENT] Git worktree management (enables parallel development workflows)

4 participants