diff --git a/apps/server/src/routes/worktree/common.ts b/apps/server/src/routes/worktree/common.ts index bc6e59ba..6527ab77 100644 --- a/apps/server/src/routes/worktree/common.ts +++ b/apps/server/src/routes/worktree/common.ts @@ -111,6 +111,19 @@ export async function isGitRepo(repoPath: string): Promise { } } +/** + * Check if a git repository has at least one commit (i.e., HEAD exists) + * Returns false for freshly initialized repos with no commits + */ +export async function hasCommits(repoPath: string): Promise { + try { + await execAsync('git rev-parse --verify HEAD', { cwd: repoPath }); + return true; + } catch { + return false; + } +} + /** * Check if an error is ENOENT (file/path not found or spawn failed) * These are expected in test environments with mock paths diff --git a/apps/server/src/routes/worktree/index.ts b/apps/server/src/routes/worktree/index.ts index a3780b45..7fef5c6e 100644 --- a/apps/server/src/routes/worktree/index.ts +++ b/apps/server/src/routes/worktree/index.ts @@ -4,6 +4,7 @@ import { Router } from 'express'; import { validatePathParams } from '../../middleware/validate-paths.js'; +import { requireValidWorktree, requireValidProject, requireGitRepoOnly } from './middleware.js'; import { createInfoHandler } from './routes/info.js'; import { createStatusHandler } from './routes/status.js'; import { createListHandler } from './routes/list.js'; @@ -38,17 +39,42 @@ export function createWorktreeRoutes(): Router { router.post('/list', createListHandler()); router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler()); router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler()); - router.post('/merge', validatePathParams('projectPath'), createMergeHandler()); + router.post( + '/merge', + validatePathParams('projectPath'), + requireValidProject, + createMergeHandler() + ); router.post('/create', validatePathParams('projectPath'), createCreateHandler()); router.post('/delete', validatePathParams('projectPath', 'worktreePath'), createDeleteHandler()); router.post('/create-pr', createCreatePRHandler()); router.post('/pr-info', createPRInfoHandler()); - router.post('/commit', validatePathParams('worktreePath'), createCommitHandler()); - router.post('/push', validatePathParams('worktreePath'), createPushHandler()); - router.post('/pull', validatePathParams('worktreePath'), createPullHandler()); - router.post('/checkout-branch', createCheckoutBranchHandler()); - router.post('/list-branches', validatePathParams('worktreePath'), createListBranchesHandler()); - router.post('/switch-branch', createSwitchBranchHandler()); + router.post( + '/commit', + validatePathParams('worktreePath'), + requireGitRepoOnly, + createCommitHandler() + ); + router.post( + '/push', + validatePathParams('worktreePath'), + requireValidWorktree, + createPushHandler() + ); + router.post( + '/pull', + validatePathParams('worktreePath'), + requireValidWorktree, + createPullHandler() + ); + router.post('/checkout-branch', requireValidWorktree, createCheckoutBranchHandler()); + router.post( + '/list-branches', + validatePathParams('worktreePath'), + requireValidWorktree, + createListBranchesHandler() + ); + router.post('/switch-branch', requireValidWorktree, createSwitchBranchHandler()); router.post('/open-in-editor', validatePathParams('worktreePath'), createOpenInEditorHandler()); router.get('/default-editor', createGetDefaultEditorHandler()); router.post('/init-git', validatePathParams('projectPath'), createInitGitHandler()); diff --git a/apps/server/src/routes/worktree/middleware.ts b/apps/server/src/routes/worktree/middleware.ts new file mode 100644 index 00000000..d933fff4 --- /dev/null +++ b/apps/server/src/routes/worktree/middleware.ts @@ -0,0 +1,74 @@ +/** + * Middleware for worktree route validation + */ + +import type { Request, Response, NextFunction } from 'express'; +import { isGitRepo, hasCommits } from './common.js'; + +interface ValidationOptions { + /** Check if the path is a git repository (default: true) */ + requireGitRepo?: boolean; + /** Check if the repository has at least one commit (default: true) */ + requireCommits?: boolean; + /** The name of the request body field containing the path (default: 'worktreePath') */ + pathField?: 'worktreePath' | 'projectPath'; +} + +/** + * Middleware factory to validate that a path is a valid git repository with commits. + * This reduces code duplication across route handlers. + * + * @param options - Validation options + * @returns Express middleware function + */ +export function requireValidGitRepo(options: ValidationOptions = {}) { + const { requireGitRepo = true, requireCommits = true, pathField = 'worktreePath' } = options; + + return async (req: Request, res: Response, next: NextFunction): Promise => { + const repoPath = req.body[pathField] as string | undefined; + + if (!repoPath) { + // Let the route handler deal with missing path validation + next(); + return; + } + + if (requireGitRepo && !(await isGitRepo(repoPath))) { + res.status(400).json({ + success: false, + error: 'Not a git repository', + code: 'NOT_GIT_REPO', + }); + return; + } + + if (requireCommits && !(await hasCommits(repoPath))) { + res.status(400).json({ + success: false, + error: 'Repository has no commits yet', + code: 'NO_COMMITS', + }); + return; + } + + next(); + }; +} + +/** + * Middleware to validate git repo for worktreePath field + */ +export const requireValidWorktree = requireValidGitRepo({ pathField: 'worktreePath' }); + +/** + * Middleware to validate git repo for projectPath field + */ +export const requireValidProject = requireValidGitRepo({ pathField: 'projectPath' }); + +/** + * Middleware to validate git repo without requiring commits (for commit route) + */ +export const requireGitRepoOnly = requireValidGitRepo({ + pathField: 'worktreePath', + requireCommits: false, +}); diff --git a/apps/server/src/routes/worktree/routes/checkout-branch.ts b/apps/server/src/routes/worktree/routes/checkout-branch.ts index ef8ddc47..ffa6e5e3 100644 --- a/apps/server/src/routes/worktree/routes/checkout-branch.ts +++ b/apps/server/src/routes/worktree/routes/checkout-branch.ts @@ -1,5 +1,8 @@ /** * POST /checkout-branch endpoint - Create and checkout a new branch + * + * Note: Git repository validation (isGitRepo, hasCommits) is handled by + * the requireValidWorktree middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/worktree/routes/commit.ts b/apps/server/src/routes/worktree/routes/commit.ts index 6cdc39c1..f33cd94b 100644 --- a/apps/server/src/routes/worktree/routes/commit.ts +++ b/apps/server/src/routes/worktree/routes/commit.ts @@ -1,5 +1,8 @@ /** * POST /commit endpoint - Commit changes in a worktree + * + * Note: Git repository validation (isGitRepo) is handled by + * the requireGitRepoOnly middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/worktree/routes/list-branches.ts b/apps/server/src/routes/worktree/routes/list-branches.ts index 5fab4aff..dc7d7d6c 100644 --- a/apps/server/src/routes/worktree/routes/list-branches.ts +++ b/apps/server/src/routes/worktree/routes/list-branches.ts @@ -1,5 +1,8 @@ /** * POST /list-branches endpoint - List all local branches + * + * Note: Git repository validation (isGitRepo, hasCommits) is handled by + * the requireValidWorktree middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/worktree/routes/merge.ts b/apps/server/src/routes/worktree/routes/merge.ts index 40ac8dd4..ab4e0c17 100644 --- a/apps/server/src/routes/worktree/routes/merge.ts +++ b/apps/server/src/routes/worktree/routes/merge.ts @@ -1,5 +1,8 @@ /** * POST /merge endpoint - Merge feature (merge worktree branch into main) + * + * Note: Git repository validation (isGitRepo, hasCommits) is handled by + * the requireValidProject middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/worktree/routes/pull.ts b/apps/server/src/routes/worktree/routes/pull.ts index 4384e207..7b922994 100644 --- a/apps/server/src/routes/worktree/routes/pull.ts +++ b/apps/server/src/routes/worktree/routes/pull.ts @@ -1,5 +1,8 @@ /** * POST /pull endpoint - Pull latest changes for a worktree/branch + * + * Note: Git repository validation (isGitRepo, hasCommits) is handled by + * the requireValidWorktree middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/worktree/routes/push.ts b/apps/server/src/routes/worktree/routes/push.ts index c0337f43..b044ba00 100644 --- a/apps/server/src/routes/worktree/routes/push.ts +++ b/apps/server/src/routes/worktree/routes/push.ts @@ -1,5 +1,8 @@ /** * POST /push endpoint - Push a worktree branch to remote + * + * Note: Git repository validation (isGitRepo, hasCommits) is handled by + * the requireValidWorktree middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/server/src/routes/worktree/routes/switch-branch.ts b/apps/server/src/routes/worktree/routes/switch-branch.ts index 3df7a3f2..d087341b 100644 --- a/apps/server/src/routes/worktree/routes/switch-branch.ts +++ b/apps/server/src/routes/worktree/routes/switch-branch.ts @@ -4,6 +4,9 @@ * Simple branch switching. * If there are uncommitted changes, the switch will fail and * the user should commit first. + * + * Note: Git repository validation (isGitRepo, hasCommits) is handled by + * the requireValidWorktree middleware in index.ts */ import type { Request, Response } from 'express'; diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx new file mode 100644 index 00000000..b1d9d8e7 --- /dev/null +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx @@ -0,0 +1,44 @@ +import type { ReactElement, ReactNode } from 'react'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +interface TooltipWrapperProps { + /** The element to wrap with a tooltip */ + children: ReactElement; + /** The content to display in the tooltip */ + tooltipContent: ReactNode; + /** Whether to show the tooltip (if false, renders children without tooltip) */ + showTooltip: boolean; + /** The side where the tooltip should appear */ + side?: 'top' | 'right' | 'bottom' | 'left'; +} + +/** + * A reusable wrapper that conditionally adds a tooltip to its children. + * When showTooltip is false, it renders the children directly without any tooltip. + * This is useful for adding tooltips to disabled elements that need to show + * a reason for being disabled. + */ +export function TooltipWrapper({ + children, + tooltipContent, + showTooltip, + side = 'left', +}: TooltipWrapperProps) { + if (!showTooltip) { + return children; + } + + return ( + + + + {/* The div wrapper is necessary for tooltips to work on disabled elements */} +
{children}
+
+ +

{tooltipContent}

+
+
+
+ ); +} diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx index d5710f5e..c6542256 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx @@ -20,9 +20,11 @@ import { Globe, MessageSquare, GitMerge, + AlertCircle, } from 'lucide-react'; import { cn } from '@/lib/utils'; -import type { WorktreeInfo, DevServerInfo, PRInfo } from '../types'; +import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types'; +import { TooltipWrapper } from './tooltip-wrapper'; interface WorktreeActionsDropdownProps { worktree: WorktreeInfo; @@ -35,6 +37,7 @@ interface WorktreeActionsDropdownProps { isStartingDevServer: boolean; isDevServerRunning: boolean; devServerInfo?: DevServerInfo; + gitRepoStatus: GitRepoStatus; onOpenChange: (open: boolean) => void; onPull: (worktree: WorktreeInfo) => void; onPush: (worktree: WorktreeInfo) => void; @@ -60,6 +63,7 @@ export function WorktreeActionsDropdown({ isStartingDevServer, isDevServerRunning, devServerInfo, + gitRepoStatus, onOpenChange, onPull, onPush, @@ -76,6 +80,14 @@ export function WorktreeActionsDropdown({ // Check if there's a PR associated with this worktree from stored metadata const hasPR = !!worktree.pr; + // Check git operations availability + const canPerformGitOps = gitRepoStatus.isGitRepo && gitRepoStatus.hasCommits; + const gitOpsDisabledReason = !gitRepoStatus.isGitRepo + ? 'Not a git repository' + : !gitRepoStatus.hasCommits + ? 'Repository has no commits yet' + : null; + return ( @@ -92,6 +104,16 @@ export function WorktreeActionsDropdown({ + {/* Warning label when git operations are not available */} + {!canPerformGitOps && ( + <> + + + {gitOpsDisabledReason} + + + + )} {isDevServerRunning ? ( <> @@ -124,36 +146,58 @@ export function WorktreeActionsDropdown({ )} - onPull(worktree)} disabled={isPulling} className="text-xs"> - - {isPulling ? 'Pulling...' : 'Pull'} - {behindCount > 0 && ( - - {behindCount} behind - - )} - - onPush(worktree)} - disabled={isPushing || aheadCount === 0} - className="text-xs" - > - - {isPushing ? 'Pushing...' : 'Push'} - {aheadCount > 0 && ( - - {aheadCount} ahead - - )} - - {!worktree.isMain && ( + onResolveConflicts(worktree)} - className="text-xs text-purple-500 focus:text-purple-600" + onClick={() => canPerformGitOps && onPull(worktree)} + disabled={isPulling || !canPerformGitOps} + className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')} > - - Pull & Resolve Conflicts + + {isPulling ? 'Pulling...' : 'Pull'} + {!canPerformGitOps && } + {canPerformGitOps && behindCount > 0 && ( + + {behindCount} behind + + )} + + + canPerformGitOps && onPush(worktree)} + disabled={isPushing || aheadCount === 0 || !canPerformGitOps} + className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')} + > + + {isPushing ? 'Pushing...' : 'Push'} + {!canPerformGitOps && } + {canPerformGitOps && aheadCount > 0 && ( + + {aheadCount} ahead + + )} + + + {!worktree.isMain && ( + + canPerformGitOps && onResolveConflicts(worktree)} + disabled={!canPerformGitOps} + className={cn( + 'text-xs text-purple-500 focus:text-purple-600', + !canPerformGitOps && 'opacity-50 cursor-not-allowed' + )} + > + + Pull & Resolve Conflicts + {!canPerformGitOps && ( + + )} + + )} onOpenInEditor(worktree)} className="text-xs"> @@ -162,17 +206,41 @@ export function WorktreeActionsDropdown({ {worktree.hasChanges && ( - onCommit(worktree)} className="text-xs"> - - Commit Changes - + + gitRepoStatus.isGitRepo && onCommit(worktree)} + disabled={!gitRepoStatus.isGitRepo} + className={cn('text-xs', !gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed')} + > + + Commit Changes + {!gitRepoStatus.isGitRepo && ( + + )} + + )} {/* Show PR option for non-primary worktrees, or primary worktree with changes */} {(!worktree.isMain || worktree.hasChanges) && !hasPR && ( - onCreatePR(worktree)} className="text-xs"> - - Create Pull Request - + + canPerformGitOps && onCreatePR(worktree)} + disabled={!canPerformGitOps} + className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')} + > + + Create Pull Request + {!canPerformGitOps && ( + + )} + + )} {/* Show PR info and Address Comments button if PR exists */} {!worktree.isMain && hasPR && worktree.pr && ( diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx index 76c95db1..9e357231 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx @@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'; import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from '../types'; +import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types'; import { BranchSwitchDropdown } from './branch-switch-dropdown'; import { WorktreeActionsDropdown } from './worktree-actions-dropdown'; @@ -27,6 +27,7 @@ interface WorktreeTabProps { isStartingDevServer: boolean; aheadCount: number; behindCount: number; + gitRepoStatus: GitRepoStatus; onSelectWorktree: (worktree: WorktreeInfo) => void; onBranchDropdownOpenChange: (open: boolean) => void; onActionsDropdownOpenChange: (open: boolean) => void; @@ -67,6 +68,7 @@ export function WorktreeTab({ isStartingDevServer, aheadCount, behindCount, + gitRepoStatus, onSelectWorktree, onBranchDropdownOpenChange, onActionsDropdownOpenChange, @@ -320,6 +322,7 @@ export function WorktreeTab({ isStartingDevServer={isStartingDevServer} isDevServerRunning={isDevServerRunning} devServerInfo={devServerInfo} + gitRepoStatus={gitRepoStatus} onOpenChange={onActionsDropdownOpenChange} onPull={onPull} onPush={onPush} diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts index 69a78ec0..1291dfef 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { getElectronAPI } from '@/lib/electron'; -import type { BranchInfo } from '../types'; +import type { BranchInfo, GitRepoStatus } from '../types'; export function useBranches() { const [branches, setBranches] = useState([]); @@ -8,28 +8,58 @@ export function useBranches() { const [behindCount, setBehindCount] = useState(0); const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [branchFilter, setBranchFilter] = useState(''); + const [gitRepoStatus, setGitRepoStatus] = useState({ + isGitRepo: true, + hasCommits: true, + }); - const fetchBranches = useCallback(async (worktreePath: string) => { - setIsLoadingBranches(true); - try { - const api = getElectronAPI(); - if (!api?.worktree?.listBranches) { - console.warn('List branches API not available'); - return; - } - const result = await api.worktree.listBranches(worktreePath); - if (result.success && result.result) { - setBranches(result.result.branches); - setAheadCount(result.result.aheadCount || 0); - setBehindCount(result.result.behindCount || 0); - } - } catch (error) { - console.error('Failed to fetch branches:', error); - } finally { - setIsLoadingBranches(false); - } + /** Helper to reset branch state to initial values */ + const resetBranchState = useCallback(() => { + setBranches([]); + setAheadCount(0); + setBehindCount(0); }, []); + const fetchBranches = useCallback( + async (worktreePath: string) => { + setIsLoadingBranches(true); + try { + const api = getElectronAPI(); + if (!api?.worktree?.listBranches) { + console.warn('List branches API not available'); + return; + } + const result = await api.worktree.listBranches(worktreePath); + if (result.success && result.result) { + setBranches(result.result.branches); + setAheadCount(result.result.aheadCount || 0); + setBehindCount(result.result.behindCount || 0); + setGitRepoStatus({ isGitRepo: true, hasCommits: true }); + } else if (result.code === 'NOT_GIT_REPO') { + // Not a git repository - clear branches silently without logging an error + resetBranchState(); + setGitRepoStatus({ isGitRepo: false, hasCommits: false }); + } else if (result.code === 'NO_COMMITS') { + // Git repo but no commits yet - clear branches silently without logging an error + resetBranchState(); + setGitRepoStatus({ isGitRepo: true, hasCommits: false }); + } else if (!result.success) { + // Other errors - log them + console.warn('Failed to fetch branches:', result.error); + resetBranchState(); + } + } catch (error) { + console.error('Failed to fetch branches:', error); + resetBranchState(); + // Reset git status to unknown state on network/API errors + setGitRepoStatus({ isGitRepo: true, hasCommits: true }); + } finally { + setIsLoadingBranches(false); + } + }, + [resetBranchState] + ); + const resetBranchFilter = useCallback(() => { setBranchFilter(''); }, []); @@ -48,5 +78,6 @@ export function useBranches() { setBranchFilter, resetBranchFilter, fetchBranches, + gitRepoStatus, }; } diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts index 381975b1..62249f97 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts @@ -3,6 +3,29 @@ import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; import type { WorktreeInfo } from '../types'; +// Error codes that need special user-friendly handling +const GIT_STATUS_ERROR_CODES = ['NOT_GIT_REPO', 'NO_COMMITS'] as const; +type GitStatusErrorCode = (typeof GIT_STATUS_ERROR_CODES)[number]; + +// User-friendly messages for git status errors +const GIT_STATUS_ERROR_MESSAGES: Record = { + NOT_GIT_REPO: 'This directory is not a git repository', + NO_COMMITS: 'Repository has no commits yet. Create an initial commit first.', +}; + +/** + * Helper to handle git status errors with user-friendly messages. + * @returns true if the error was a git status error and was handled, false otherwise. + */ +function handleGitStatusError(result: { code?: string; error?: string }): boolean { + const errorCode = result.code as GitStatusErrorCode | undefined; + if (errorCode && GIT_STATUS_ERROR_CODES.includes(errorCode)) { + toast.info(GIT_STATUS_ERROR_MESSAGES[errorCode] || result.error); + return true; + } + return false; +} + interface UseWorktreeActionsOptions { fetchWorktrees: () => Promise | undefined>; fetchBranches: (worktreePath: string) => Promise; @@ -29,6 +52,7 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre toast.success(result.result.message); fetchWorktrees(); } else { + if (handleGitStatusError(result)) return; toast.error(result.error || 'Failed to switch branch'); } } catch (error) { @@ -56,6 +80,7 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre toast.success(result.result.message); fetchWorktrees(); } else { + if (handleGitStatusError(result)) return; toast.error(result.error || 'Failed to pull latest changes'); } } catch (error) { @@ -84,6 +109,7 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre fetchBranches(worktree.path); fetchWorktrees(); } else { + if (handleGitStatusError(result)) return; toast.error(result.error || 'Failed to push changes'); } } catch (error) { diff --git a/apps/ui/src/components/views/board-view/worktree-panel/types.ts b/apps/ui/src/components/views/board-view/worktree-panel/types.ts index 901ca357..c6ecefde 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/types.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/types.ts @@ -23,6 +23,11 @@ export interface BranchInfo { isRemote: boolean; } +export interface GitRepoStatus { + isGitRepo: boolean; + hasCommits: boolean; +} + export interface DevServerInfo { worktreePath: string; port: number; diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index 665f3434..0f4a1765 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -61,6 +61,7 @@ export function WorktreePanel({ setBranchFilter, resetBranchFilter, fetchBranches, + gitRepoStatus, } = useBranches(); const { @@ -210,6 +211,7 @@ export function WorktreePanel({ isStartingDevServer={isStartingDevServer} aheadCount={aheadCount} behindCount={behindCount} + gitRepoStatus={gitRepoStatus} onSelectWorktree={handleSelectWorktree} onBranchDropdownOpenChange={handleBranchDropdownOpenChange(mainWorktree)} onActionsDropdownOpenChange={handleActionsDropdownOpenChange(mainWorktree)} @@ -264,6 +266,7 @@ export function WorktreePanel({ isStartingDevServer={isStartingDevServer} aheadCount={aheadCount} behindCount={behindCount} + gitRepoStatus={gitRepoStatus} onSelectWorktree={handleSelectWorktree} onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)} onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)} diff --git a/apps/ui/src/types/electron.d.ts b/apps/ui/src/types/electron.d.ts index 3c07c2d6..e9f15cf9 100644 --- a/apps/ui/src/types/electron.d.ts +++ b/apps/ui/src/types/electron.d.ts @@ -733,6 +733,7 @@ export interface WorktreeAPI { message: string; }; error?: string; + code?: 'NOT_GIT_REPO' | 'NO_COMMITS'; }>; // Create a pull request from a worktree @@ -783,6 +784,7 @@ export interface WorktreeAPI { message: string; }; error?: string; + code?: 'NOT_GIT_REPO' | 'NO_COMMITS'; }>; // Create and checkout a new branch @@ -797,6 +799,7 @@ export interface WorktreeAPI { message: string; }; error?: string; + code?: 'NOT_GIT_REPO' | 'NO_COMMITS'; }>; // List all local branches @@ -813,6 +816,7 @@ export interface WorktreeAPI { behindCount: number; }; error?: string; + code?: 'NOT_GIT_REPO' | 'NO_COMMITS'; // Error codes for git status issues }>; // Switch to an existing branch @@ -827,6 +831,7 @@ export interface WorktreeAPI { message: string; }; error?: string; + code?: 'NOT_GIT_REPO' | 'NO_COMMITS' | 'UNCOMMITTED_CHANGES'; }>; // Open a worktree directory in the editor