apply the patches

This commit is contained in:
webdevcody
2026-01-20 10:24:38 -05:00
parent 179c5ae9c2
commit 76eb3a2ac2
42 changed files with 2679 additions and 757 deletions

View File

@@ -8,58 +8,81 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { GitMerge, AlertTriangle, CheckCircle2 } from 'lucide-react';
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';
interface WorktreeInfo {
path: string;
branch: string;
isMain: boolean;
hasChanges?: boolean;
changedFilesCount?: number;
}
export type { MergeConflictInfo } from '../worktree-panel/types';
interface MergeWorktreeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
projectPath: string;
worktree: WorktreeInfo | null;
onMerged: (mergedWorktree: WorktreeInfo) => void;
/** Number of features assigned to this worktree's branch */
affectedFeatureCount?: number;
/** Called when merge is successful. deletedBranch indicates if the branch was also deleted. */
onMerged: (mergedWorktree: WorktreeInfo, deletedBranch: boolean) => void;
onCreateConflictResolutionFeature?: (conflictInfo: MergeConflictInfo) => void;
}
type DialogStep = 'confirm' | 'verify';
export function MergeWorktreeDialog({
open,
onOpenChange,
projectPath,
worktree,
onMerged,
affectedFeatureCount = 0,
onCreateConflictResolutionFeature,
}: MergeWorktreeDialogProps) {
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<DialogStep>('confirm');
const [confirmText, setConfirmText] = useState('');
const [targetBranch, setTargetBranch] = useState('main');
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
const [loadingBranches, setLoadingBranches] = useState(false);
const [deleteWorktreeAndBranch, setDeleteWorktreeAndBranch] = useState(false);
const [mergeConflict, setMergeConflict] = useState<MergeConflictInfo | null>(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);
setStep('confirm');
setConfirmText('');
setTargetBranch('main');
setDeleteWorktreeAndBranch(false);
setMergeConflict(null);
}
}, [open]);
const handleProceedToVerify = () => {
setStep('verify');
};
const handleMerge = async () => {
if (!worktree) return;
@@ -71,96 +94,151 @@ export function MergeWorktreeDialog({
return;
}
// Pass branchName and worktreePath directly to the API
const result = await api.worktree.mergeFeature(projectPath, worktree.branch, worktree.path);
// 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) {
toast.success('Branch merged to main', {
description: `Branch "${worktree.branch}" has been merged and cleaned up`,
});
onMerged(worktree);
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 {
toast.error('Failed to merge branch', {
description: result.error,
});
// 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) {
toast.error('Failed to merge branch', {
description: err instanceof Error ? err.message : 'Unknown error',
});
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;
const confirmationWord = 'merge';
const isConfirmValid = confirmText.toLowerCase() === confirmationWord;
// First step: Show what will happen and ask for confirmation
if (step === 'confirm') {
// Show conflict resolution UI if there are merge conflicts
if (mergeConflict) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<GitMerge className="w-5 h-5 text-green-600" />
Merge to Main
<AlertTriangle className="w-5 h-5 text-orange-500" />
Merge Conflicts Detected
</DialogTitle>
<DialogDescription asChild>
<div className="space-y-3">
<div className="space-y-4">
<span className="block">
Merge branch{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code> into
main?
There are conflicts when merging{' '}
<code className="font-mono bg-muted px-1 rounded">
{mergeConflict.sourceBranch}
</code>{' '}
into{' '}
<code className="font-mono bg-muted px-1 rounded">
{mergeConflict.targetBranch}
</code>
.
</span>
<div className="text-sm text-muted-foreground mt-2">
This will:
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Merge the branch into the main branch</li>
<li>Remove the worktree directory</li>
<li>Delete the branch</li>
</ul>
<div className="flex items-start gap-2 p-3 rounded-md bg-orange-500/10 border border-orange-500/20">
<AlertTriangle className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
<span className="text-orange-500 text-sm">
The merge could not be completed automatically. You can create a feature task to
resolve the conflicts in the{' '}
<code className="font-mono bg-muted px-0.5 rounded">
{mergeConflict.targetBranch}
</code>{' '}
branch.
</span>
</div>
{worktree.hasChanges && (
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-500/10 border border-yellow-500/20 mt-2">
<AlertTriangle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
<span className="text-yellow-500 text-sm">
This worktree has {worktree.changedFilesCount} uncommitted change(s). Please
commit or discard them before merging.
</span>
</div>
)}
{affectedFeatureCount > 0 && (
<div className="flex items-start gap-2 p-3 rounded-md bg-blue-500/10 border border-blue-500/20 mt-2">
<AlertTriangle className="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" />
<span className="text-blue-500 text-sm">
{affectedFeatureCount} feature{affectedFeatureCount !== 1 ? 's' : ''}{' '}
{affectedFeatureCount !== 1 ? 'are' : 'is'} assigned to this branch and will
be unassigned after merge.
</span>
</div>
)}
<div className="mt-2 p-3 rounded-md bg-muted/50 border border-border">
<p className="text-sm text-muted-foreground">
This will create a high-priority feature task that will:
</p>
<ul className="text-sm text-muted-foreground mt-2 list-disc list-inside space-y-1">
<li>
Resolve merge conflicts in the{' '}
<code className="font-mono bg-muted px-0.5 rounded">
{mergeConflict.targetBranch}
</code>{' '}
branch
</li>
<li>Ensure the code compiles and tests pass</li>
<li>Complete the merge automatically</li>
</ul>
</div>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange(false)}>
<Button variant="ghost" onClick={() => setMergeConflict(null)}>
Back
</Button>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleProceedToVerify}
disabled={worktree.hasChanges}
className="bg-green-600 hover:bg-green-700 text-white"
onClick={handleCreateConflictResolutionFeature}
className="bg-purple-600 hover:bg-purple-700 text-white"
>
<GitMerge className="w-4 h-4 mr-2" />
Continue
<Wrench className="w-4 h-4 mr-2" />
Create Resolve Conflicts Feature
</Button>
</DialogFooter>
</DialogContent>
@@ -168,52 +246,86 @@ export function MergeWorktreeDialog({
);
}
// Second step: Type confirmation
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-orange-500" />
Confirm Merge
<GitMerge className="w-5 h-5 text-green-600" />
Merge Branch
</DialogTitle>
<DialogDescription asChild>
<div className="space-y-4">
<div className="flex items-start gap-2 p-3 rounded-md bg-orange-500/10 border border-orange-500/20">
<AlertTriangle className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
<span className="text-orange-600 dark:text-orange-400 text-sm">
This action cannot be undone. The branch{' '}
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code> will be
permanently deleted after merging.
</span>
</div>
<span className="block">
Merge <code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>{' '}
into:
</span>
<div className="space-y-2">
<Label htmlFor="confirm-merge" className="text-sm text-foreground">
Type <span className="font-bold text-foreground">{confirmationWord}</span> to
confirm:
<Label htmlFor="target-branch" className="text-sm text-foreground">
Target Branch
</Label>
<Input
id="confirm-merge"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={confirmationWord}
disabled={isLoading}
className="font-mono"
autoComplete="off"
/>
{loadingBranches ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Spinner size="sm" />
Loading branches...
</div>
) : (
<BranchAutocomplete
value={targetBranch}
onChange={setTargetBranch}
branches={availableBranches}
placeholder="Select target branch..."
data-testid="merge-target-branch"
/>
)}
</div>
{worktree.hasChanges && (
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-500/10 border border-yellow-500/20">
<AlertTriangle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
<span className="text-yellow-500 text-sm">
This worktree has {worktree.changedFilesCount} uncommitted change(s). Please
commit or discard them before merging.
</span>
</div>
)}
</div>
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2 py-2">
<Checkbox
id="delete-worktree-branch"
checked={deleteWorktreeAndBranch}
onCheckedChange={(checked) => setDeleteWorktreeAndBranch(checked === true)}
/>
<Label
htmlFor="delete-worktree-branch"
className="text-sm cursor-pointer flex items-center gap-1.5"
>
<Trash2 className="w-3.5 h-3.5 text-destructive" />
Delete worktree and branch after merging
</Label>
</div>
{deleteWorktreeAndBranch && (
<div className="flex items-start gap-2 p-3 rounded-md bg-orange-500/10 border border-orange-500/20">
<AlertTriangle className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
<span className="text-orange-500 text-sm">
The worktree and branch will be permanently deleted. Any features assigned to this
branch will be unassigned.
</span>
</div>
)}
<DialogFooter>
<Button variant="ghost" onClick={() => setStep('confirm')} disabled={isLoading}>
Back
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
<Button
onClick={handleMerge}
disabled={isLoading || !isConfirmValid}
disabled={worktree.hasChanges || !targetBranch || loadingBranches || isLoading}
className="bg-green-600 hover:bg-green-700 text-white"
>
{isLoading ? (
@@ -223,8 +335,8 @@ export function MergeWorktreeDialog({
</>
) : (
<>
<CheckCircle2 className="w-4 h-4 mr-2" />
Merge to Main
<GitMerge className="w-4 h-4 mr-2" />
Merge
</>
)}
</Button>