Changes from fix/list-branch-issue-on-fresh-repo

This commit is contained in:
Kacper
2025-12-23 20:46:10 +01:00
parent 629b7e7433
commit 4958ee1dda
15 changed files with 386 additions and 47 deletions

View File

@@ -111,6 +111,19 @@ export async function isGitRepo(repoPath: string): Promise<boolean> {
} }
} }
/**
* 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<boolean> {
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) * Check if an error is ENOENT (file/path not found or spawn failed)
* These are expected in test environments with mock paths * These are expected in test environments with mock paths

View File

@@ -5,7 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError, isGitRepo, hasCommits } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -43,6 +43,26 @@ export function createCheckoutBranchHandler() {
return; return;
} }
// Check if path is a git repository
if (!(await isGitRepo(worktreePath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check if repository has at least one commit
if (!(await hasCommits(worktreePath))) {
res.status(400).json({
success: false,
error: 'Repository has no commits yet',
code: 'NO_COMMITS',
});
return;
}
// Get current branch for reference // Get current branch for reference
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath, cwd: worktreePath,

View File

@@ -5,7 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError, isGitRepo } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -25,6 +25,16 @@ export function createCommitHandler() {
return; return;
} }
// Check if path is a git repository
if (!(await isGitRepo(worktreePath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check for uncommitted changes // Check for uncommitted changes
const { stdout: status } = await execAsync('git status --porcelain', { const { stdout: status } = await execAsync('git status --porcelain', {
cwd: worktreePath, cwd: worktreePath,

View File

@@ -5,7 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getErrorMessage, logWorktreeError } from '../common.js'; import { getErrorMessage, logWorktreeError, isGitRepo, hasCommits } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -30,6 +30,26 @@ export function createListBranchesHandler() {
return; return;
} }
// Check if the path is a git repository before running git commands
if (!(await isGitRepo(worktreePath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check if the repository has any commits (freshly init'd repos have no HEAD)
if (!(await hasCommits(worktreePath))) {
res.status(400).json({
success: false,
error: 'Repository has no commits yet',
code: 'NO_COMMITS',
});
return;
}
// Get current branch // Get current branch
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath, cwd: worktreePath,

View File

@@ -6,7 +6,7 @@ import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import path from 'path'; import path from 'path';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError, isGitRepo, hasCommits } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -27,6 +27,26 @@ export function createMergeHandler() {
return; return;
} }
// Check if path is a git repository
if (!(await isGitRepo(projectPath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check if repository has at least one commit
if (!(await hasCommits(projectPath))) {
res.status(400).json({
success: false,
error: 'Repository has no commits yet',
code: 'NO_COMMITS',
});
return;
}
const branchName = `feature/${featureId}`; const branchName = `feature/${featureId}`;
// Git worktrees are stored in project directory // Git worktrees are stored in project directory
const worktreePath = path.join(projectPath, '.worktrees', featureId); const worktreePath = path.join(projectPath, '.worktrees', featureId);

View File

@@ -5,7 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError, isGitRepo, hasCommits } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -24,6 +24,26 @@ export function createPullHandler() {
return; return;
} }
// Check if path is a git repository
if (!(await isGitRepo(worktreePath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check if repository has at least one commit
if (!(await hasCommits(worktreePath))) {
res.status(400).json({
success: false,
error: 'Repository has no commits yet',
code: 'NO_COMMITS',
});
return;
}
// Get current branch name // Get current branch name
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath, cwd: worktreePath,

View File

@@ -5,7 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError, isGitRepo, hasCommits } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -25,6 +25,26 @@ export function createPushHandler() {
return; return;
} }
// Check if path is a git repository
if (!(await isGitRepo(worktreePath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check if repository has at least one commit
if (!(await hasCommits(worktreePath))) {
res.status(400).json({
success: false,
error: 'Repository has no commits yet',
code: 'NO_COMMITS',
});
return;
}
// Get branch name // Get branch name
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath, cwd: worktreePath,

View File

@@ -9,7 +9,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError, isGitRepo, hasCommits } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -83,6 +83,26 @@ export function createSwitchBranchHandler() {
return; return;
} }
// Check if path is a git repository
if (!(await isGitRepo(worktreePath))) {
res.status(400).json({
success: false,
error: 'Not a git repository',
code: 'NOT_GIT_REPO',
});
return;
}
// Check if repository has at least one commit
if (!(await hasCommits(worktreePath))) {
res.status(400).json({
success: false,
error: 'Repository has no commits yet',
code: 'NO_COMMITS',
});
return;
}
// Get current branch // Get current branch
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath, cwd: worktreePath,

View File

@@ -7,6 +7,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuLabel, DropdownMenuLabel,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { import {
Trash2, Trash2,
MoreHorizontal, MoreHorizontal,
@@ -20,9 +21,10 @@ import {
Globe, Globe,
MessageSquare, MessageSquare,
GitMerge, GitMerge,
AlertCircle,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { WorktreeInfo, DevServerInfo, PRInfo } from '../types'; import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
interface WorktreeActionsDropdownProps { interface WorktreeActionsDropdownProps {
worktree: WorktreeInfo; worktree: WorktreeInfo;
@@ -35,6 +37,7 @@ interface WorktreeActionsDropdownProps {
isStartingDevServer: boolean; isStartingDevServer: boolean;
isDevServerRunning: boolean; isDevServerRunning: boolean;
devServerInfo?: DevServerInfo; devServerInfo?: DevServerInfo;
gitRepoStatus: GitRepoStatus;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onPull: (worktree: WorktreeInfo) => void; onPull: (worktree: WorktreeInfo) => void;
onPush: (worktree: WorktreeInfo) => void; onPush: (worktree: WorktreeInfo) => void;
@@ -60,6 +63,7 @@ export function WorktreeActionsDropdown({
isStartingDevServer, isStartingDevServer,
isDevServerRunning, isDevServerRunning,
devServerInfo, devServerInfo,
gitRepoStatus,
onOpenChange, onOpenChange,
onPull, onPull,
onPush, onPush,
@@ -76,6 +80,14 @@ export function WorktreeActionsDropdown({
// Check if there's a PR associated with this worktree from stored metadata // Check if there's a PR associated with this worktree from stored metadata
const hasPR = !!worktree.pr; 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 ( return (
<DropdownMenu onOpenChange={onOpenChange}> <DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -92,6 +104,16 @@ export function WorktreeActionsDropdown({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56"> <DropdownMenuContent align="start" className="w-56">
{/* Warning label when git operations are not available */}
{!canPerformGitOps && (
<>
<DropdownMenuLabel className="text-xs flex items-center gap-2 text-amber-600 dark:text-amber-400">
<AlertCircle className="w-3.5 h-3.5" />
{gitOpsDisabledReason}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
{isDevServerRunning ? ( {isDevServerRunning ? (
<> <>
<DropdownMenuLabel className="text-xs flex items-center gap-2"> <DropdownMenuLabel className="text-xs flex items-center gap-2">
@@ -124,36 +146,92 @@ export function WorktreeActionsDropdown({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>
)} )}
<DropdownMenuItem onClick={() => onPull(worktree)} disabled={isPulling} className="text-xs"> <TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DropdownMenuItem
onClick={() => canPerformGitOps && onPull(worktree)}
disabled={isPulling || !canPerformGitOps}
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
>
<Download className={cn('w-3.5 h-3.5 mr-2', isPulling && 'animate-pulse')} /> <Download className={cn('w-3.5 h-3.5 mr-2', isPulling && 'animate-pulse')} />
{isPulling ? 'Pulling...' : 'Pull'} {isPulling ? 'Pulling...' : 'Pull'}
{behindCount > 0 && ( {!canPerformGitOps && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
{canPerformGitOps && behindCount > 0 && (
<span className="ml-auto text-[10px] bg-muted px-1.5 py-0.5 rounded"> <span className="ml-auto text-[10px] bg-muted px-1.5 py-0.5 rounded">
{behindCount} behind {behindCount} behind
</span> </span>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
</div>
</TooltipTrigger>
{gitOpsDisabledReason && (
<TooltipContent side="left">
<p>{gitOpsDisabledReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onPush(worktree)} onClick={() => canPerformGitOps && onPush(worktree)}
disabled={isPushing || aheadCount === 0} disabled={isPushing || aheadCount === 0 || !canPerformGitOps}
className="text-xs" className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
> >
<Upload className={cn('w-3.5 h-3.5 mr-2', isPushing && 'animate-pulse')} /> <Upload className={cn('w-3.5 h-3.5 mr-2', isPushing && 'animate-pulse')} />
{isPushing ? 'Pushing...' : 'Push'} {isPushing ? 'Pushing...' : 'Push'}
{aheadCount > 0 && ( {!canPerformGitOps && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
{canPerformGitOps && aheadCount > 0 && (
<span className="ml-auto text-[10px] bg-primary/20 text-primary px-1.5 py-0.5 rounded"> <span className="ml-auto text-[10px] bg-primary/20 text-primary px-1.5 py-0.5 rounded">
{aheadCount} ahead {aheadCount} ahead
</span> </span>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
</div>
</TooltipTrigger>
{gitOpsDisabledReason && (
<TooltipContent side="left">
<p>{gitOpsDisabledReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{!worktree.isMain && ( {!worktree.isMain && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onResolveConflicts(worktree)} onClick={() => canPerformGitOps && onResolveConflicts(worktree)}
className="text-xs text-purple-500 focus:text-purple-600" disabled={!canPerformGitOps}
className={cn(
'text-xs text-purple-500 focus:text-purple-600',
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
)}
> >
<GitMerge className="w-3.5 h-3.5 mr-2" /> <GitMerge className="w-3.5 h-3.5 mr-2" />
Pull & Resolve Conflicts Pull & Resolve Conflicts
{!canPerformGitOps && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
</DropdownMenuItem> </DropdownMenuItem>
</div>
</TooltipTrigger>
{gitOpsDisabledReason && (
<TooltipContent side="left">
<p>{gitOpsDisabledReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)} )}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onOpenInEditor(worktree)} className="text-xs"> <DropdownMenuItem onClick={() => onOpenInEditor(worktree)} className="text-xs">
@@ -162,17 +240,60 @@ export function WorktreeActionsDropdown({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{worktree.hasChanges && ( {worktree.hasChanges && (
<DropdownMenuItem onClick={() => onCommit(worktree)} className="text-xs"> <TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DropdownMenuItem
onClick={() => gitRepoStatus.isGitRepo && onCommit(worktree)}
disabled={!gitRepoStatus.isGitRepo}
className={cn(
'text-xs',
!gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed'
)}
>
<GitCommit className="w-3.5 h-3.5 mr-2" /> <GitCommit className="w-3.5 h-3.5 mr-2" />
Commit Changes Commit Changes
{!gitRepoStatus.isGitRepo && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
</DropdownMenuItem> </DropdownMenuItem>
</div>
</TooltipTrigger>
{!gitRepoStatus.isGitRepo && (
<TooltipContent side="left">
<p>Not a git repository</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)} )}
{/* Show PR option for non-primary worktrees, or primary worktree with changes */} {/* Show PR option for non-primary worktrees, or primary worktree with changes */}
{(!worktree.isMain || worktree.hasChanges) && !hasPR && ( {(!worktree.isMain || worktree.hasChanges) && !hasPR && (
<DropdownMenuItem onClick={() => onCreatePR(worktree)} className="text-xs"> <TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<DropdownMenuItem
onClick={() => canPerformGitOps && onCreatePR(worktree)}
disabled={!canPerformGitOps}
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
>
<GitPullRequest className="w-3.5 h-3.5 mr-2" /> <GitPullRequest className="w-3.5 h-3.5 mr-2" />
Create Pull Request Create Pull Request
{!canPerformGitOps && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
</DropdownMenuItem> </DropdownMenuItem>
</div>
</TooltipTrigger>
{gitOpsDisabledReason && (
<TooltipContent side="left">
<p>{gitOpsDisabledReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)} )}
{/* Show PR info and Address Comments button if PR exists */} {/* Show PR info and Address Comments button if PR exists */}
{!worktree.isMain && hasPR && worktree.pr && ( {!worktree.isMain && hasPR && worktree.pr && (

View File

@@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button';
import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from 'lucide-react'; import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; 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 { BranchSwitchDropdown } from './branch-switch-dropdown';
import { WorktreeActionsDropdown } from './worktree-actions-dropdown'; import { WorktreeActionsDropdown } from './worktree-actions-dropdown';
@@ -27,6 +27,7 @@ interface WorktreeTabProps {
isStartingDevServer: boolean; isStartingDevServer: boolean;
aheadCount: number; aheadCount: number;
behindCount: number; behindCount: number;
gitRepoStatus: GitRepoStatus;
onSelectWorktree: (worktree: WorktreeInfo) => void; onSelectWorktree: (worktree: WorktreeInfo) => void;
onBranchDropdownOpenChange: (open: boolean) => void; onBranchDropdownOpenChange: (open: boolean) => void;
onActionsDropdownOpenChange: (open: boolean) => void; onActionsDropdownOpenChange: (open: boolean) => void;
@@ -67,6 +68,7 @@ export function WorktreeTab({
isStartingDevServer, isStartingDevServer,
aheadCount, aheadCount,
behindCount, behindCount,
gitRepoStatus,
onSelectWorktree, onSelectWorktree,
onBranchDropdownOpenChange, onBranchDropdownOpenChange,
onActionsDropdownOpenChange, onActionsDropdownOpenChange,
@@ -320,6 +322,7 @@ export function WorktreeTab({
isStartingDevServer={isStartingDevServer} isStartingDevServer={isStartingDevServer}
isDevServerRunning={isDevServerRunning} isDevServerRunning={isDevServerRunning}
devServerInfo={devServerInfo} devServerInfo={devServerInfo}
gitRepoStatus={gitRepoStatus}
onOpenChange={onActionsDropdownOpenChange} onOpenChange={onActionsDropdownOpenChange}
onPull={onPull} onPull={onPull}
onPush={onPush} onPush={onPush}

View File

@@ -1,6 +1,6 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import type { BranchInfo } from '../types'; import type { BranchInfo, GitRepoStatus } from '../types';
export function useBranches() { export function useBranches() {
const [branches, setBranches] = useState<BranchInfo[]>([]); const [branches, setBranches] = useState<BranchInfo[]>([]);
@@ -8,6 +8,10 @@ export function useBranches() {
const [behindCount, setBehindCount] = useState(0); const [behindCount, setBehindCount] = useState(0);
const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [branchFilter, setBranchFilter] = useState(''); const [branchFilter, setBranchFilter] = useState('');
const [gitRepoStatus, setGitRepoStatus] = useState<GitRepoStatus>({
isGitRepo: true,
hasCommits: true,
});
const fetchBranches = useCallback(async (worktreePath: string) => { const fetchBranches = useCallback(async (worktreePath: string) => {
setIsLoadingBranches(true); setIsLoadingBranches(true);
@@ -22,9 +26,31 @@ export function useBranches() {
setBranches(result.result.branches); setBranches(result.result.branches);
setAheadCount(result.result.aheadCount || 0); setAheadCount(result.result.aheadCount || 0);
setBehindCount(result.result.behindCount || 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
setBranches([]);
setAheadCount(0);
setBehindCount(0);
setGitRepoStatus({ isGitRepo: false, hasCommits: false });
} else if (result.code === 'NO_COMMITS') {
// Git repo but no commits yet - clear branches silently without logging an error
setBranches([]);
setAheadCount(0);
setBehindCount(0);
setGitRepoStatus({ isGitRepo: true, hasCommits: false });
} else if (!result.success) {
// Other errors - log them
console.warn('Failed to fetch branches:', result.error);
setBranches([]);
setAheadCount(0);
setBehindCount(0);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch branches:', error); console.error('Failed to fetch branches:', error);
setBranches([]);
setAheadCount(0);
setBehindCount(0);
} finally { } finally {
setIsLoadingBranches(false); setIsLoadingBranches(false);
} }
@@ -48,5 +74,6 @@ export function useBranches() {
setBranchFilter, setBranchFilter,
resetBranchFilter, resetBranchFilter,
fetchBranches, fetchBranches,
gitRepoStatus,
}; };
} }

View File

@@ -3,6 +3,15 @@ import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { WorktreeInfo } from '../types'; 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;
// User-friendly messages for git status errors
const GIT_STATUS_ERROR_MESSAGES: Record<string, string> = {
NOT_GIT_REPO: 'This directory is not a git repository',
NO_COMMITS: 'Repository has no commits yet. Create an initial commit first.',
};
interface UseWorktreeActionsOptions { interface UseWorktreeActionsOptions {
fetchWorktrees: () => Promise<Array<{ path: string; branch: string }> | undefined>; fetchWorktrees: () => Promise<Array<{ path: string; branch: string }> | undefined>;
fetchBranches: (worktreePath: string) => Promise<void>; fetchBranches: (worktreePath: string) => Promise<void>;
@@ -29,6 +38,15 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
toast.success(result.result.message); toast.success(result.result.message);
fetchWorktrees(); fetchWorktrees();
} else { } else {
// Handle git status errors with informative messages
const errorCode = (result as { code?: string }).code;
if (
errorCode &&
GIT_STATUS_ERROR_CODES.includes(errorCode as (typeof GIT_STATUS_ERROR_CODES)[number])
) {
toast.info(GIT_STATUS_ERROR_MESSAGES[errorCode] || result.error);
return;
}
toast.error(result.error || 'Failed to switch branch'); toast.error(result.error || 'Failed to switch branch');
} }
} catch (error) { } catch (error) {
@@ -56,6 +74,15 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
toast.success(result.result.message); toast.success(result.result.message);
fetchWorktrees(); fetchWorktrees();
} else { } else {
// Handle git status errors with informative messages
const errorCode = (result as { code?: string }).code;
if (
errorCode &&
GIT_STATUS_ERROR_CODES.includes(errorCode as (typeof GIT_STATUS_ERROR_CODES)[number])
) {
toast.info(GIT_STATUS_ERROR_MESSAGES[errorCode] || result.error);
return;
}
toast.error(result.error || 'Failed to pull latest changes'); toast.error(result.error || 'Failed to pull latest changes');
} }
} catch (error) { } catch (error) {
@@ -84,6 +111,15 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
fetchBranches(worktree.path); fetchBranches(worktree.path);
fetchWorktrees(); fetchWorktrees();
} else { } else {
// Handle git status errors with informative messages
const errorCode = (result as { code?: string }).code;
if (
errorCode &&
GIT_STATUS_ERROR_CODES.includes(errorCode as (typeof GIT_STATUS_ERROR_CODES)[number])
) {
toast.info(GIT_STATUS_ERROR_MESSAGES[errorCode] || result.error);
return;
}
toast.error(result.error || 'Failed to push changes'); toast.error(result.error || 'Failed to push changes');
} }
} catch (error) { } catch (error) {

View File

@@ -23,6 +23,11 @@ export interface BranchInfo {
isRemote: boolean; isRemote: boolean;
} }
export interface GitRepoStatus {
isGitRepo: boolean;
hasCommits: boolean;
}
export interface DevServerInfo { export interface DevServerInfo {
worktreePath: string; worktreePath: string;
port: number; port: number;

View File

@@ -61,6 +61,7 @@ export function WorktreePanel({
setBranchFilter, setBranchFilter,
resetBranchFilter, resetBranchFilter,
fetchBranches, fetchBranches,
gitRepoStatus,
} = useBranches(); } = useBranches();
const { const {
@@ -210,6 +211,7 @@ export function WorktreePanel({
isStartingDevServer={isStartingDevServer} isStartingDevServer={isStartingDevServer}
aheadCount={aheadCount} aheadCount={aheadCount}
behindCount={behindCount} behindCount={behindCount}
gitRepoStatus={gitRepoStatus}
onSelectWorktree={handleSelectWorktree} onSelectWorktree={handleSelectWorktree}
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(mainWorktree)} onBranchDropdownOpenChange={handleBranchDropdownOpenChange(mainWorktree)}
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(mainWorktree)} onActionsDropdownOpenChange={handleActionsDropdownOpenChange(mainWorktree)}
@@ -264,6 +266,7 @@ export function WorktreePanel({
isStartingDevServer={isStartingDevServer} isStartingDevServer={isStartingDevServer}
aheadCount={aheadCount} aheadCount={aheadCount}
behindCount={behindCount} behindCount={behindCount}
gitRepoStatus={gitRepoStatus}
onSelectWorktree={handleSelectWorktree} onSelectWorktree={handleSelectWorktree}
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)} onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)}
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)} onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)}

View File

@@ -813,6 +813,7 @@ export interface WorktreeAPI {
behindCount: number; behindCount: number;
}; };
error?: string; error?: string;
code?: 'NOT_GIT_REPO'; // Error code for non-git directories
}>; }>;
// Switch to an existing branch // Switch to an existing branch