import { NextRequest, NextResponse } from 'next/server'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); interface GitCommit { hash: string; date: string; author: string; message: string; filesChanged: number; insertions: number; deletions: number; } interface GitStats { totalCommits: number; firstCommit: string | null; lastCommit: string | null; totalFilesChanged: number; totalInsertions: number; totalDeletions: number; commits: GitCommit[]; topFiles: Array<{ filePath: string; changeCount: number }>; commitsByDay: Record; authors: Array<{ name: string; commitCount: number }>; } export async function GET( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; // For now, we'll use the current workspace // In the future, we can store git repo path in project metadata const repoPath = '/Users/markhenderson/ai-proxy'; // Get all commits with detailed stats const { stdout: commitsOutput } = await execAsync( `cd "${repoPath}" && git log --all --pretty=format:"%H|%ai|%an|%s" --numstat`, { maxBuffer: 10 * 1024 * 1024 } // 10MB buffer for large repos ); if (!commitsOutput.trim()) { return NextResponse.json({ totalCommits: 0, firstCommit: null, lastCommit: null, totalFilesChanged: 0, totalInsertions: 0, totalDeletions: 0, commits: [], topFiles: [], commitsByDay: {}, authors: [] }); } // Parse commit data const commits: GitCommit[] = []; const fileChangeCounts = new Map(); const commitsByDay: Record = {}; const authorCounts = new Map(); let totalFilesChanged = 0; let totalInsertions = 0; let totalDeletions = 0; const lines = commitsOutput.split('\n'); let currentCommit: Partial | null = null; for (const line of lines) { if (line.includes('|')) { // This is a commit header line if (currentCommit) { commits.push(currentCommit as GitCommit); } const [hash, date, author, message] = line.split('|'); currentCommit = { hash: hash.substring(0, 8), date, author, message, filesChanged: 0, insertions: 0, deletions: 0 }; // Count commits by day const day = date.split(' ')[0]; commitsByDay[day] = (commitsByDay[day] || 0) + 1; // Count commits by author authorCounts.set(author, (authorCounts.get(author) || 0) + 1); } else if (line.trim() && currentCommit) { // This is a file stat line (insertions, deletions, filename) const parts = line.trim().split('\t'); if (parts.length === 3) { const [insertStr, delStr, filepath] = parts; const insertions = insertStr === '-' ? 0 : parseInt(insertStr, 10) || 0; const deletions = delStr === '-' ? 0 : parseInt(delStr, 10) || 0; currentCommit.filesChanged!++; currentCommit.insertions! += insertions; currentCommit.deletions! += deletions; totalFilesChanged++; totalInsertions += insertions; totalDeletions += deletions; fileChangeCounts.set(filepath, (fileChangeCounts.get(filepath) || 0) + 1); } } } // Push the last commit if (currentCommit) { commits.push(currentCommit as GitCommit); } // Sort commits by date (most recent first) commits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); const firstCommit = commits.length > 0 ? commits[commits.length - 1].date : null; const lastCommit = commits.length > 0 ? commits[0].date : null; // Get top 20 most changed files const topFiles = Array.from(fileChangeCounts.entries()) .sort(([, countA], [, countB]) => countB - countA) .slice(0, 20) .map(([filePath, changeCount]) => ({ filePath, changeCount })); // Get author stats const authors = Array.from(authorCounts.entries()) .sort(([, countA], [, countB]) => countB - countA) .map(([name, commitCount]) => ({ name, commitCount })); const stats: GitStats = { totalCommits: commits.length, firstCommit, lastCommit, totalFilesChanged, totalInsertions, totalDeletions, commits: commits.slice(0, 50), // Return last 50 commits for display topFiles, commitsByDay, authors }; return NextResponse.json(stats); } catch (error) { console.error('Error loading Git history:', error); return NextResponse.json( { error: 'Could not load Git history', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } }