170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
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<string, number>;
|
|
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<string, number>();
|
|
const commitsByDay: Record<string, number> = {};
|
|
const authorCounts = new Map<string, number>();
|
|
|
|
let totalFilesChanged = 0;
|
|
let totalInsertions = 0;
|
|
let totalDeletions = 0;
|
|
|
|
const lines = commitsOutput.split('\n');
|
|
let currentCommit: Partial<GitCommit> | 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 }
|
|
);
|
|
}
|
|
}
|
|
|