import { useState, useEffect, useCallback } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { getHttpApiClient } from '@/lib/http-api-client'; import { toast } from 'sonner'; import { GitMerge, RefreshCw, AlertTriangle, GitBranch, Wrench, Sparkles, XCircle, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import type { WorktreeInfo, MergeConflictInfo } from '../worktree-panel/types'; export type PullStrategy = 'merge' | 'rebase'; type DialogStep = 'select' | 'executing' | 'conflict' | 'success'; interface ConflictState { conflictFiles: string[]; remoteBranch: string; strategy: PullStrategy; } interface RemoteBranch { name: string; fullRef: string; } interface RemoteInfo { name: string; url: string; branches: RemoteBranch[]; } const logger = createLogger('MergeRebaseDialog'); interface MergeRebaseDialogProps { open: boolean; onOpenChange: (open: boolean) => void; worktree: WorktreeInfo | null; onCreateConflictResolutionFeature?: (conflictInfo: MergeConflictInfo) => void; } export function MergeRebaseDialog({ open, onOpenChange, worktree, onCreateConflictResolutionFeature, }: MergeRebaseDialogProps) { const [remotes, setRemotes] = useState([]); const [selectedRemote, setSelectedRemote] = useState(''); const [selectedBranch, setSelectedBranch] = useState(''); const [selectedStrategy, setSelectedStrategy] = useState('merge'); const [isLoading, setIsLoading] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [step, setStep] = useState('select'); const [conflictState, setConflictState] = useState(null); // Fetch remotes when dialog opens useEffect(() => { if (open && worktree) { fetchRemotes(); } }, [open, worktree]); // eslint-disable-line react-hooks/exhaustive-deps // Reset state when dialog closes useEffect(() => { if (!open) { setSelectedRemote(''); setSelectedBranch(''); setSelectedStrategy('merge'); setError(null); setStep('select'); setConflictState(null); } }, [open]); // Auto-select default remote and branch when remotes are loaded useEffect(() => { if (remotes.length > 0 && !selectedRemote) { // Default to 'origin' if available, otherwise first remote const defaultRemote = remotes.find((r) => r.name === 'origin') || remotes[0]; setSelectedRemote(defaultRemote.name); // Try to select a matching branch name or default to main/master if (defaultRemote.branches.length > 0 && worktree) { const matchingBranch = defaultRemote.branches.find((b) => b.name === worktree.branch); const mainBranch = defaultRemote.branches.find( (b) => b.name === 'main' || b.name === 'master' ); const defaultBranch = matchingBranch || mainBranch || defaultRemote.branches[0]; setSelectedBranch(defaultBranch.fullRef); } } }, [remotes, selectedRemote, worktree]); // Update selected branch when remote changes useEffect(() => { if (selectedRemote && remotes.length > 0 && worktree) { const remote = remotes.find((r) => r.name === selectedRemote); if (remote && remote.branches.length > 0) { // Try to select a matching branch name or default to main/master const matchingBranch = remote.branches.find((b) => b.name === worktree.branch); const mainBranch = remote.branches.find((b) => b.name === 'main' || b.name === 'master'); const defaultBranch = matchingBranch || mainBranch || remote.branches[0]; setSelectedBranch(defaultBranch.fullRef); } else { setSelectedBranch(''); } } }, [selectedRemote, remotes, worktree]); const fetchRemotes = async () => { if (!worktree) return; setIsLoading(true); setError(null); try { const api = getHttpApiClient(); const result = await api.worktree.listRemotes(worktree.path); if (result.success && result.result) { setRemotes(result.result.remotes); if (result.result.remotes.length === 0) { setError('No remotes found in this repository'); } } else { setError(result.error || 'Failed to fetch remotes'); } } catch (err) { logger.error('Failed to fetch remotes:', err); setError('Failed to fetch remotes'); } finally { setIsLoading(false); } }; const handleRefresh = async () => { if (!worktree) return; setIsRefreshing(true); setError(null); try { const api = getHttpApiClient(); const result = await api.worktree.listRemotes(worktree.path); if (result.success && result.result) { setRemotes(result.result.remotes); toast.success('Remotes refreshed'); } else { toast.error(result.error || 'Failed to refresh remotes'); } } catch (err) { logger.error('Failed to refresh remotes:', err); toast.error('Failed to refresh remotes'); } finally { setIsRefreshing(false); } }; const handleExecuteOperation = useCallback(async () => { if (!worktree || !selectedBranch) return; setStep('executing'); try { const api = getHttpApiClient(); if (selectedStrategy === 'rebase') { // First fetch the remote to ensure we have latest refs try { await api.worktree.pull(worktree.path, selectedRemote); } catch { // Fetch may fail if no upstream - that's okay, we'll try rebase anyway } // Attempt the rebase operation const result = await api.worktree.rebase(worktree.path, selectedBranch); if (result.success) { toast.success(`Rebased onto ${selectedBranch}`, { description: result.result?.message || 'Rebase completed successfully', }); setStep('success'); onOpenChange(false); } else if (result.hasConflicts) { // Rebase had conflicts - show conflict resolution UI setConflictState({ conflictFiles: result.conflictFiles || [], remoteBranch: selectedBranch, strategy: 'rebase', }); setStep('conflict'); } else { toast.error('Rebase failed', { description: result.error || 'Unknown error', }); setStep('select'); } } else { // Merge strategy - attempt to merge the remote branch // Use the pull endpoint for merging remote branches const result = await api.worktree.pull(worktree.path, selectedRemote, true); if (result.success && result.result) { if (result.result.hasConflicts) { // Pull had conflicts setConflictState({ conflictFiles: result.result.conflictFiles || [], remoteBranch: selectedBranch, strategy: 'merge', }); setStep('conflict'); } else { toast.success(`Merged ${selectedBranch}`, { description: result.result.message || 'Merge completed successfully', }); setStep('success'); onOpenChange(false); } } else { // Check for conflict indicators in error const errorMessage = result.error || ''; const hasConflicts = errorMessage.toLowerCase().includes('conflict') || errorMessage.includes('CONFLICT'); if (hasConflicts) { setConflictState({ conflictFiles: [], remoteBranch: selectedBranch, strategy: 'merge', }); setStep('conflict'); } else { // Non-conflict failure - show conflict resolution UI so user can choose // how to handle it (resolve manually or with AI) rather than auto-creating a task setConflictState({ conflictFiles: [], remoteBranch: selectedBranch, strategy: 'merge', }); setStep('conflict'); } } } } catch (err) { logger.error('Failed to execute operation:', err); // Show conflict resolution UI so user can choose how to handle it setConflictState({ conflictFiles: [], remoteBranch: selectedBranch, strategy: selectedStrategy, }); setStep('conflict'); } }, [worktree, selectedBranch, selectedStrategy, selectedRemote, onOpenChange]); const handleResolveWithAI = useCallback(() => { if (!worktree || !conflictState) return; if (onCreateConflictResolutionFeature) { const conflictInfo: MergeConflictInfo = { sourceBranch: conflictState.remoteBranch, targetBranch: worktree.branch, targetWorktreePath: worktree.path, conflictFiles: conflictState.conflictFiles, operationType: conflictState.strategy, }; onCreateConflictResolutionFeature(conflictInfo); } onOpenChange(false); }, [worktree, conflictState, onCreateConflictResolutionFeature, onOpenChange]); const handleResolveManually = useCallback(() => { toast.info('Conflict markers left in place', { description: 'Edit the conflicting files to resolve conflicts manually.', duration: 6000, }); onOpenChange(false); }, [onOpenChange]); const selectedRemoteData = remotes.find((r) => r.name === selectedRemote); const branches = selectedRemoteData?.branches || []; if (!worktree) return null; // Conflict resolution UI if (step === 'conflict' && conflictState) { const isRebase = conflictState.strategy === 'rebase'; return ( {isRebase ? 'Rebase' : 'Merge'} Conflicts Detected
{isRebase ? ( <> Conflicts detected when rebasing{' '} {worktree.branch}{' '} onto{' '} {conflictState.remoteBranch} . The rebase was aborted and no changes were applied. ) : ( <> Conflicts detected when merging{' '} {conflictState.remoteBranch} {' '} into{' '} {worktree.branch}. )} {conflictState.conflictFiles.length > 0 && (
Conflicting files ({conflictState.conflictFiles.length}):
{conflictState.conflictFiles.map((file) => (
{file}
))}
)}

Choose how to resolve:

  • Resolve with AI — Creates a task to{' '} {isRebase ? 'rebase and ' : ''}resolve conflicts automatically
  • Resolve Manually —{' '} {isRebase ? 'Leaves the branch unchanged for you to rebase manually' : 'Leaves conflict markers in place for you to edit directly'}
); } // Executing phase if (step === 'executing') { return ( {selectedStrategy === 'rebase' ? ( ) : ( )} {selectedStrategy === 'rebase' ? 'Rebasing...' : 'Merging...'} {selectedStrategy === 'rebase' ? `Rebasing ${worktree.branch} onto ${selectedBranch}...` : `Merging ${selectedBranch} into ${worktree.branch}...`}
This may take a moment...
); } // Selection UI return ( Merge & Rebase Select a remote branch to merge or rebase with{' '} {worktree?.branch || 'current branch'} {isLoading ? (
) : error ? (
{error}
) : (
{selectedRemote && branches.length === 0 && (

No branches found for this remote

)}
{selectedBranch && (

This will attempt to{' '} {selectedStrategy === 'rebase' ? ( <> rebase {worktree?.branch}{' '} onto {selectedBranch} ) : ( <> merge {selectedBranch} into{' '} {worktree?.branch} )} . If conflicts arise, you can choose to resolve them manually or with AI.

)}
)}
); }