import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { GitMerge, AlertTriangle, Trash2, Wrench } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; import { BranchAutocomplete } from '@/components/ui/branch-autocomplete'; import type { WorktreeInfo, BranchInfo, MergeConflictInfo } from '../worktree-panel/types'; export type { MergeConflictInfo } from '../worktree-panel/types'; interface MergeWorktreeDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectPath: string; worktree: WorktreeInfo | null; /** Called when merge is successful. deletedBranch indicates if the branch was also deleted. */ onMerged: (mergedWorktree: WorktreeInfo, deletedBranch: boolean) => void; onCreateConflictResolutionFeature?: (conflictInfo: MergeConflictInfo) => void; } export function MergeWorktreeDialog({ open, onOpenChange, projectPath, worktree, onMerged, onCreateConflictResolutionFeature, }: MergeWorktreeDialogProps) { const [isLoading, setIsLoading] = useState(false); const [targetBranch, setTargetBranch] = useState('main'); const [availableBranches, setAvailableBranches] = useState([]); const [loadingBranches, setLoadingBranches] = useState(false); const [deleteWorktreeAndBranch, setDeleteWorktreeAndBranch] = useState(false); const [mergeConflict, setMergeConflict] = useState(null); // Fetch available branches when dialog opens useEffect(() => { if (open && worktree && projectPath) { setLoadingBranches(true); const api = getElectronAPI(); if (api?.worktree?.listBranches) { api.worktree .listBranches(projectPath, false) .then((result) => { if (result.success && result.result?.branches) { // Filter out the source branch (can't merge into itself) and remote branches const branches = result.result.branches .filter((b: BranchInfo) => !b.isRemote && b.name !== worktree.branch) .map((b: BranchInfo) => b.name); setAvailableBranches(branches); } }) .catch((err) => { console.error('Failed to fetch branches:', err); }) .finally(() => { setLoadingBranches(false); }); } else { setLoadingBranches(false); } } }, [open, worktree, projectPath]); // Reset state when dialog opens useEffect(() => { if (open) { setIsLoading(false); setTargetBranch('main'); setDeleteWorktreeAndBranch(false); setMergeConflict(null); } }, [open]); const handleMerge = async () => { if (!worktree) return; setIsLoading(true); try { const api = getElectronAPI(); if (!api?.worktree?.mergeFeature) { toast.error('Worktree API not available'); return; } // Pass branchName, worktreePath, targetBranch, and options to the API const result = await api.worktree.mergeFeature( projectPath, worktree.branch, worktree.path, targetBranch, { deleteWorktreeAndBranch } ); if (result.success) { const description = deleteWorktreeAndBranch ? `Branch "${worktree.branch}" has been merged into "${targetBranch}" and the worktree and branch were deleted` : `Branch "${worktree.branch}" has been merged into "${targetBranch}"`; toast.success(`Branch merged to ${targetBranch}`, { description }); onMerged(worktree, deleteWorktreeAndBranch); onOpenChange(false); } else { // Check if the error indicates merge conflicts const errorMessage = result.error || ''; const hasConflicts = errorMessage.toLowerCase().includes('conflict') || errorMessage.toLowerCase().includes('merge failed') || errorMessage.includes('CONFLICT'); if (hasConflicts && onCreateConflictResolutionFeature) { // Set merge conflict state to show the conflict resolution UI setMergeConflict({ sourceBranch: worktree.branch, targetBranch: targetBranch, targetWorktreePath: projectPath, // The merge happens in the target branch's worktree }); toast.error('Merge conflicts detected', { description: 'The merge has conflicts that need to be resolved manually.', }); } else { toast.error('Failed to merge branch', { description: result.error, }); } } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; // Check if the error indicates merge conflicts const hasConflicts = errorMessage.toLowerCase().includes('conflict') || errorMessage.toLowerCase().includes('merge failed') || errorMessage.includes('CONFLICT'); if (hasConflicts && onCreateConflictResolutionFeature) { setMergeConflict({ sourceBranch: worktree.branch, targetBranch: targetBranch, targetWorktreePath: projectPath, }); toast.error('Merge conflicts detected', { description: 'The merge has conflicts that need to be resolved manually.', }); } else { toast.error('Failed to merge branch', { description: errorMessage, }); } } finally { setIsLoading(false); } }; const handleCreateConflictResolutionFeature = () => { if (mergeConflict && onCreateConflictResolutionFeature) { onCreateConflictResolutionFeature(mergeConflict); onOpenChange(false); } }; if (!worktree) return null; // Show conflict resolution UI if there are merge conflicts if (mergeConflict) { return ( Merge Conflicts Detected
There are conflicts when merging{' '} {mergeConflict.sourceBranch} {' '} into{' '} {mergeConflict.targetBranch} .
The merge could not be completed automatically. You can create a feature task to resolve the conflicts in the{' '} {mergeConflict.targetBranch} {' '} branch.

This will create a high-priority feature task that will:

  • Resolve merge conflicts in the{' '} {mergeConflict.targetBranch} {' '} branch
  • Ensure the code compiles and tests pass
  • Complete the merge automatically
); } return ( Merge Branch
Merge {worktree.branch}{' '} into:
{loadingBranches ? (
Loading branches...
) : ( )}
{worktree.hasChanges && (
This worktree has {worktree.changedFilesCount} uncommitted change(s). Please commit or discard them before merging.
)}
setDeleteWorktreeAndBranch(checked === true)} />
{deleteWorktreeAndBranch && (
The worktree and branch will be permanently deleted. Any features assigned to this branch will be unassigned.
)}
); }