VIBN Frontend for Coolify deployment
This commit is contained in:
169
app/api/projects/[projectId]/git-history/route.ts
Normal file
169
app/api/projects/[projectId]/git-history/route.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user