import React, { useState } from 'react'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay, type DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, } from "@/components/ui/dialog"; import { GripVertical, GitCommit, Trash2, Edit2, ArrowUpFromLine, Save, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; interface Commit { hash: string; message: string; author: string; } export interface InteractiveRebaseProps { owner: string; repo: string; prNumber: number; headBranch: string; baseBranch: string; commits: Commit[]; // Initial commit list } type ActionType = "pick" | "reword" | "squash" | "drop"; interface RebaseItem extends Commit { id: string; // Use hash as ID action: ActionType; newMessage?: string; } function SortableItem({ item, index, onActionChange, onMessageChange }: { item: RebaseItem, index: number, onActionChange: (id: string, action: ActionType) => void, onMessageChange: (id: string, msg: string) => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id }); const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 10 : 1, opacity: isDragging ? 0.5 : 1 }; return (
{item.action === "reword" ? ( onMessageChange(item.id, e.target.value)} /> ) : (
{item.message}
)}
{item.id.substring(0, 7)}
); } export function InteractiveRebase({ owner, repo, prNumber, commits: initialCommits }: InteractiveRebaseProps) { const [isOpen, setIsOpen] = useState(false); const [items, setItems] = useState( initialCommits.map(c => ({ ...c, id: c.hash, action: "pick" })) ); const [loading, setLoading] = useState(false); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (active.id !== over?.id) { setItems((items) => { const oldIndex = items.findIndex(i => i.id === active.id); const newIndex = items.findIndex(i => i.id === over?.id); return arrayMove(items, oldIndex, newIndex); }); } }; const handleActionChange = (id: string, action: ActionType) => { setItems(items => items.map(item => item.id === id ? { ...item, action } : item )); }; const handleMessageChange = (id: string, msg: string) => { setItems(items => items.map(item => item.id === id ? { ...item, newMessage: msg } : item )); }; const handleSave = async () => { // Validation: Ensure we don't drop all commits if (items.every(i => i.action === 'drop')) { toast.error("Cannot drop all commits. Use 'Delete Branch' instead."); return; } // Validation: First commit cannot be squashed if (items[0].action === 'squash') { toast.error("Cannot squash the first commit. Pick or Reword it."); return; } setLoading(true); // Transform items to operations const operations = items.map(item => ({ type: item.action, hash: item.id, newMessage: item.newMessage || item.message })); try { const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/rewrite`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ operations }) }); const data = await res.json().catch(() => ({})); if (!res.ok) { // Handle specific error codes if available const errorMessage = data.message || data.error || "Failed to rewrite history. Please try again."; throw new Error(errorMessage); } toast.success("History rewritten successfully!", { description: "The branch has been force-pushed with your new history." }); setIsOpen(false); // Reload after a short delay to let toast show setTimeout(() => window.location.reload(), 1500); } catch (e: any) { console.error("Rebase error:", e); toast.error("Rebase Failed", { description: e.message || "An unexpected network error occurred.", duration: 5000, }); } finally { setLoading(false); } }; return ( Interactive Rebase Drag to reorder. Squash commits into the one above. Drop unwanted commits.
i.id)} strategy={verticalListSortingStrategy} > {items.map((item, index) => ( ))}
{items.filter(i => i.action === 'drop').length} dropped, {items.filter(i => i.action === 'squash').length} squashed
); }