Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/server/src/routes/worktree/routes/list-branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,37 @@ interface BranchInfo {
name: string;
isCurrent: boolean;
isRemote: boolean;
/** Path to worktree if this branch is checked out in a worktree (null if not) */
checkedOutInWorktree: string | null;
}

/**
* Get a map of branch names to their worktree paths.
* Git doesn't allow checking out a branch that's already checked out in a worktree.
*/
async function getWorktreeBranches(cwd: string): Promise<Map<string, string>> {
const branchToWorktree = new Map<string, string>();
try {
const { stdout } = await execAsync('git worktree list --porcelain', { cwd });
const lines = stdout.split('\n');
let currentWorktreePath: string | null = null;

for (const line of lines) {
if (line.startsWith('worktree ')) {
currentWorktreePath = line.slice(9);
} else if (line.startsWith('branch ')) {
const branchName = line.slice(7).replace('refs/heads/', '');
if (currentWorktreePath) {
branchToWorktree.set(branchName, currentWorktreePath);
}
} else if (line === '') {
currentWorktreePath = null;
}
}
} catch {
// If we can't get worktree info, return empty map
}
Comment on lines +46 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While returning an empty map on failure is a reasonable fallback, swallowing the error completely can make debugging difficult if git worktree list fails for an unexpected reason. It's better to log the error using the existing logWorktreeError utility to aid in troubleshooting.

Suggested change
} catch {
// If we can't get worktree info, return empty map
}
} catch (error) {
// If we can't get worktree info, log it and return an empty map so the main functionality isn't blocked.
logWorktreeError(error, 'Failed to get worktree branches', cwd);
}

return branchToWorktree;
}

export function createListBranchesHandler() {
Expand All @@ -40,6 +71,10 @@ export function createListBranchesHandler() {
});
const currentBranch = currentBranchOutput.trim();

// Get branches that are already checked out in worktrees
// These cannot be switched to from other locations
const worktreeBranches = await getWorktreeBranches(worktreePath);

// List all local branches
// Use double quotes around the format string for cross-platform compatibility
// Single quotes are preserved literally on Windows; double quotes work on both
Expand All @@ -54,10 +89,13 @@ export function createListBranchesHandler() {
.map((name) => {
// Remove any surrounding quotes (Windows git may preserve them)
const cleanName = name.trim().replace(/^['"]|['"]$/g, '');
const worktreePath = worktreeBranches.get(cleanName);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Variable shadowing: worktreePath shadows outer scope.

The variable worktreePath declared here shadows the worktreePath from the request body (line 56). This is confusing and could lead to subtle bugs if the outer variable is accidentally used.

Suggested rename
-          const worktreePath = worktreeBranches.get(cleanName);
+          const branchWorktreePath = worktreeBranches.get(cleanName);
           return {
             name: cleanName,
             isCurrent: cleanName === currentBranch,
             isRemote: false,
             // Mark if this branch is checked out in a worktree (and it's not the current worktree)
-            checkedOutInWorktree: worktreePath && cleanName !== currentBranch ? worktreePath : null,
+            checkedOutInWorktree: branchWorktreePath && cleanName !== currentBranch ? branchWorktreePath : null,
           };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const worktreePath = worktreeBranches.get(cleanName);
const branchWorktreePath = worktreeBranches.get(cleanName);
return {
name: cleanName,
isCurrent: cleanName === currentBranch,
isRemote: false,
// Mark if this branch is checked out in a worktree (and it's not the current worktree)
checkedOutInWorktree: branchWorktreePath && cleanName !== currentBranch ? branchWorktreePath : null,
};
🤖 Prompt for AI Agents
In `@apps/server/src/routes/worktree/routes/list-branches.ts` at line 92, The
local const worktreePath returned from worktreeBranches.get(cleanName) shadows
the worktreePath from the request body; rename the local variable (e.g.,
branchWorktreePath or branchPath) at the declaration where
worktreeBranches.get(cleanName) is used and update all subsequent references in
this function (including any null/undefined checks and uses when constructing
the branch object) so the outer request body worktreePath remains distinct and
unshadowed.

return {
name: cleanName,
isCurrent: cleanName === currentBranch,
isRemote: false,
// Mark if this branch is checked out in a worktree (and it's not the current worktree)
checkedOutInWorktree: worktreePath && cleanName !== currentBranch ? worktreePath : null,
};
});

Expand Down Expand Up @@ -102,6 +140,7 @@ export function createListBranchesHandler() {
name: cleanName, // Keep full name like "origin/main"
isCurrent: false,
isRemote: true,
checkedOutInWorktree: null, // Remote branches are never checked out in worktrees
});
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ interface UseProjectCreationProps {
upsertAndSetCurrentProject: (path: string, name: string) => Project;
}

export function useProjectCreation({
upsertAndSetCurrentProject,
}: UseProjectCreationProps) {
export function useProjectCreation({ upsertAndSetCurrentProject }: UseProjectCreationProps) {
// Modal state
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
const [isCreatingProject, setIsCreatingProject] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,23 @@ export function BranchSwitchDropdown({
{branchFilter ? 'No matching branches' : 'No branches found'}
</DropdownMenuItem>
) : (
filteredBranches.map((branch) => (
<DropdownMenuItem
key={branch.name}
onClick={() => onSwitchBranch(worktree, branch.name)}
disabled={isSwitching || branch.name === worktree.branch}
className="text-xs font-mono"
>
{branch.name === worktree.branch ? (
<Check className="w-3.5 h-3.5 mr-2 flex-shrink-0" />
) : (
<span className="w-3.5 mr-2 flex-shrink-0" />
)}
<span className="truncate">{branch.name}</span>
</DropdownMenuItem>
))
filteredBranches
.filter((branch) => !branch.checkedOutInWorktree) // Hide branches already in worktrees
.map((branch) => (
<DropdownMenuItem
key={branch.name}
onClick={() => onSwitchBranch(worktree, branch.name)}
disabled={isSwitching || branch.name === worktree.branch}
className="text-xs font-mono"
>
{branch.name === worktree.branch ? (
<Check className="w-3.5 h-3.5 mr-2 flex-shrink-0" />
) : (
<span className="w-3.5 mr-2 flex-shrink-0" />
)}
<span className="truncate">{branch.name}</span>
</DropdownMenuItem>
))
)}
</div>
<DropdownMenuSeparator />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface BranchInfo {
name: string;
isCurrent: boolean;
isRemote: boolean;
/** Path to worktree if this branch is checked out in a worktree (null if not) */
checkedOutInWorktree: string | null;
}

export interface GitRepoStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,8 @@ export function WorktreePanel({
)}
</div>

{/* Worktrees section - only show if enabled */}
{useWorktreesEnabled && (
{/* Worktrees section - show if enabled OR if worktrees exist (so users can navigate to them) */}
{(useWorktreesEnabled || nonMainWorktrees.length > 0) && (
<>
<div className="w-px h-5 bg-border mx-2" />
<GitBranch className="w-4 h-4 text-muted-foreground" />
Expand Down