import { useState, useEffect, useCallback } from "react"; import { FileCode, ChevronDown, ChevronRight, AlertCircle, GitCommit } from "lucide-react"; import type { MergeDetails } from "@fusion/core"; import { fetchCommitDiff } from "../api"; import { getErrorMessage } from "@fusion/core"; import { highlightDiff } from "../utils/highlightDiff"; import "./TaskDiffShared.css"; interface CommitDiffTabProps { commitSha: string; mergeDetails?: MergeDetails; } export interface ParsedFile { path: string; status: "added" | "modified" | "deleted" | "unknown"; additions: number; deletions: number; patch: string; } function getStatusLabel(status: ParsedFile["status"]): string { switch (status) { case "added": return "A"; case "deleted": return "D"; case "modified": return "M"; default: return "?"; } } export function parsePatch(rawPatch: string): ParsedFile[] { const files: ParsedFile[] = []; // Split on diff boundaries, keeping the delimiter const parts = rawPatch.split(/(?=^diff --git )/m); for (const part of parts) { const trimmed = part.trim(); if (!trimmed.startsWith("diff --git ")) continue; // Extract file path from "diff --git a/path b/path" const headerMatch = trimmed.match(/^diff --git a\/(.+?) b\/(.+)/m); const path = headerMatch ? headerMatch[2] : "unknown"; // Determine status let status: ParsedFile["status"] = "modified"; if (trimmed.includes("new file mode")) status = "added"; else if (trimmed.includes("deleted file mode")) status = "deleted"; // Count additions and deletions from diff lines let additions = 0; let deletions = 0; const lines = trimmed.split("\n"); for (const line of lines) { if (line.startsWith("+") && !line.startsWith("+++")) additions++; else if (line.startsWith("-") && !line.startsWith("---")) deletions++; } files.push({ path, status, additions, deletions, patch: trimmed }); } return files; } /** * CommitDiffTab displays the file-by-file diff for a merge commit. * * It fetches the diff using the commit SHA from `mergeDetails` and renders * an expandable file list with syntax-highlighted diff output, similar to * the in-progress `TaskChangesTab` but sourced from git history. */ export function CommitDiffTab({ commitSha, mergeDetails }: CommitDiffTabProps) { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expandedFiles, setExpandedFiles] = useState>(new Set()); const loadDiff = useCallback(async () => { if (!commitSha) { setLoading(false); return; } try { setLoading(true); setError(null); const data = await fetchCommitDiff(commitSha); const parsed = parsePatch(data.patch || ""); setFiles(parsed); // Auto-expand first file if (parsed.length > 0) { setExpandedFiles(new Set([parsed[0].path])); } } catch (err) { setError(getErrorMessage(err) || "Failed to load commit diff"); } finally { setLoading(false); } }, [commitSha]); useEffect(() => { loadDiff(); }, [loadDiff]); const toggleFile = (path: string) => { setExpandedFiles((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }; if (!commitSha) { return (

No commit SHA available.

Commit diff is only available for tasks that were merged.
); } if (loading) { return (
Loading commit diff...
); } if (error) { return (
Error loading commit diff: {error}
); } if (files.length === 0) { return (

No files changed in this commit.

); } const totalAdditions = mergeDetails?.insertions ?? files.reduce((sum, f) => sum + f.additions, 0); const totalDeletions = mergeDetails?.deletions ?? files.reduce((sum, f) => sum + f.deletions, 0); const totalFiles = mergeDetails?.filesChanged ?? files.length; return (
{/* Commit metadata */} {mergeDetails && (
{commitSha.slice(0, 7)}
{mergeDetails.mergeCommitMessage && (
{mergeDetails.mergeCommitMessage}
)}
)}

Files Changed ({totalFiles}) +{totalAdditions}{" "} -{totalDeletions}

{files.map((file) => { const isExpanded = expandedFiles.has(file.path); return (
{isExpanded && file.patch && (
                    {highlightDiff(file.patch)}
                  
)}
); })}
); }