import { useMemo, useState } from "react" interface Commit { sha: string shortSha: string message: string author: { name: string email: string avatar?: string } date: string timestamp: number parents: string[] branch?: string tags?: string[] refs?: string[] } interface CommitGraphProps { commits: Commit[] branches: string[] onCommitClick?: (commit: Commit) => void onBranchClick?: (branch: string) => void } // Color palette for branches const branchColors = [ "#2ecc71", // green "#3498db", // blue "#9b59b6", // purple "#e74c3c", // red "#f1c40f", // yellow "#1abc9c", // teal "#e67e22", // orange "#95a5a6", // gray ] export function CommitGraph({ commits, branches, onCommitClick, onBranchClick, }: CommitGraphProps) { const [selectedCommit, setSelectedCommit] = useState(null) const [hoveredCommit, setHoveredCommit] = useState(null) // Calculate graph layout const graphData = useMemo(() => { const columns: Map = new Map() const branchHeads: Map = new Map() const commitColumns: Map = new Map() const connections: Array<{ from: { row: number; col: number } to: { row: number; col: number } color: string }> = [] let maxColumn = 0 // Assign columns to branches branches.forEach((branch, index) => { columns.set(branch, index) maxColumn = Math.max(maxColumn, index) }) // Process commits commits.forEach((commit, index) => { // Find which column this commit belongs to let col = 0 if (commit.branch) { col = columns.get(commit.branch) || 0 } else { // Find an available column col = maxColumn + 1 maxColumn = col } commitColumns.set(commit.sha, col) // Create connections to parents commit.parents.forEach((parentSha) => { const parentIndex = commits.findIndex((c) => c.sha === parentSha) if (parentIndex !== -1) { const parentCol = commitColumns.get(parentSha) || col connections.push({ from: { row: index, col }, to: { row: parentIndex, col: parentCol }, color: branchColors[col % branchColors.length], }) } }) }) return { commitColumns, connections, maxColumn, } }, [commits, branches]) const handleCommitClick = (commit: Commit) => { setSelectedCommit(commit.sha) onCommitClick?.(commit) } const rowHeight = 36 const columnWidth = 16 const nodeRadius = 5 const graphWidth = (graphData.maxColumn + 2) * columnWidth return (
{/* Header */}

Commit History

{branches.slice(0, 5).map((branch, index) => ( ))} {branches.length > 5 && ( +{branches.length - 5} more )}
{/* Graph */}
{/* Graph Column */}
{/* Draw connections */} {graphData.connections.map((conn, i) => { const x1 = conn.from.col * columnWidth + columnWidth / 2 + 4 const y1 = conn.from.row * rowHeight + rowHeight / 2 const x2 = conn.to.col * columnWidth + columnWidth / 2 + 4 const y2 = conn.to.row * rowHeight + rowHeight / 2 // Create curved path for merge commits const path = x1 === x2 ? `M ${x1} ${y1} L ${x2} ${y2}` : `M ${x1} ${y1} C ${x1} ${y1 + (y2 - y1) / 2} ${x2} ${y1 + (y2 - y1) / 2} ${x2} ${y2}` return ( ) })} {/* Draw nodes */} {commits.map((commit, index) => { const col = graphData.commitColumns.get(commit.sha) || 0 const x = col * columnWidth + columnWidth / 2 + 4 const y = index * rowHeight + rowHeight / 2 const color = branchColors[col % branchColors.length] const isSelected = selectedCommit === commit.sha const isHovered = hoveredCommit === commit.sha return ( setHoveredCommit(commit.sha)} onMouseLeave={() => setHoveredCommit(null)} onClick={() => handleCommitClick(commit)} /> ) })}
{/* Commit List */}
{commits.map((commit, index) => { const col = graphData.commitColumns.get(commit.sha) || 0 const color = branchColors[col % branchColors.length] const isSelected = selectedCommit === commit.sha const isHovered = hoveredCommit === commit.sha return (
setHoveredCommit(commit.sha)} onMouseLeave={() => setHoveredCommit(null)} onClick={() => handleCommitClick(commit)} > {/* Refs (branches, tags) */}
{commit.refs?.map((ref, i) => ( {ref} ))} {commit.tags?.map((tag, i) => ( 🏷️ {tag} ))}
{/* Commit SHA */} {commit.shortSha} {/* Commit Message */} {commit.message} {/* Author */}
{commit.author.avatar ? ( {commit.author.name} ) : ( {commit.author.name.charAt(0).toUpperCase()} )}
{commit.author.name}
{/* Date */} {commit.date}
) })}
) } // Simplified linear commit list interface CommitListProps { commits: Commit[] onCommitClick?: (commit: Commit) => void } export function CommitList({ commits, onCommitClick }: CommitListProps) { return (
{commits.map((commit) => (
onCommitClick?.(commit)} >
{commit.author.avatar ? ( {commit.author.name} ) : ( {commit.author.name.charAt(0).toUpperCase()} )}

{commit.message}

{commit.author.name} committed {commit.shortSha} · {commit.date}
{(commit.refs?.length || commit.tags?.length) && (
{commit.refs?.map((ref, i) => ( {ref} ))} {commit.tags?.map((tag, i) => ( 🏷️ {tag} ))}
)}
))}
) } export type { Commit, CommitGraphProps, CommitListProps }