Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
636 lines
24 KiB
TypeScript
636 lines
24 KiB
TypeScript
import type { Plugin } from 'vite';
|
||
import { exec } from 'child_process';
|
||
import { promisify } from 'util';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
|
||
const execAsync = promisify(exec);
|
||
|
||
/**
|
||
* Git 版本管理 API 插件
|
||
* 提供基于 Git 的版本控制功能,用于页面和元素的文件夹级别版本管理
|
||
*/
|
||
export function gitVersionApiPlugin(): Plugin {
|
||
let gitAvailable = false;
|
||
let gitCheckError: string | null = null;
|
||
|
||
const resolveModuleFile = (basePath: string): string | null => {
|
||
const fileCandidates = [basePath, `${basePath}.ts`, `${basePath}.tsx`, `${basePath}.js`, `${basePath}.jsx`];
|
||
for (const candidate of fileCandidates) {
|
||
try {
|
||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||
return candidate;
|
||
}
|
||
} catch {
|
||
// Ignore fs race/errors and continue probing other candidates.
|
||
}
|
||
}
|
||
|
||
const indexCandidates = [
|
||
path.join(basePath, 'index.ts'),
|
||
path.join(basePath, 'index.tsx'),
|
||
path.join(basePath, 'index.js'),
|
||
path.join(basePath, 'index.jsx'),
|
||
];
|
||
|
||
for (const candidate of indexCandidates) {
|
||
try {
|
||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||
return candidate;
|
||
}
|
||
} catch {
|
||
// Ignore fs race/errors and continue probing other candidates.
|
||
}
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
return {
|
||
name: 'git-version-api',
|
||
|
||
/**
|
||
* 解决跨文件夹导入问题:
|
||
* 当从 .git-versions/{id}/src/... 中的文件发起相对导入时,如果目标文件
|
||
* 不在已提取的版本目录中(如 ../../components/side-menu),则回退到
|
||
* 当前工作目录的 src/ 下去解析。
|
||
*/
|
||
resolveId(source, importer) {
|
||
if (!importer) return null;
|
||
|
||
const projectRoot = process.cwd();
|
||
const gitVersionsDir = path.join(projectRoot, '.git-versions');
|
||
const normalizedImporter = path.normalize(importer);
|
||
|
||
// 只处理来自 .git-versions 目录中的导入
|
||
if (!normalizedImporter.startsWith(gitVersionsDir)) return null;
|
||
|
||
// 解析相对路径导入
|
||
const resolved = path.resolve(path.dirname(normalizedImporter), source);
|
||
|
||
// 如果解析后的路径仍在 .git-versions 内且文件不存在,则回退到真实 src/
|
||
if (!resolved.startsWith(gitVersionsDir)) return null;
|
||
|
||
const versionResolvedPath = resolveModuleFile(resolved);
|
||
if (versionResolvedPath) {
|
||
return versionResolvedPath;
|
||
}
|
||
|
||
// 文件不存在 → 提取 .git-versions/{id}/ 之后的相对路径,映射到真实 src/
|
||
const relativeTail = path.relative(gitVersionsDir, resolved);
|
||
// relativeTail 形如 "67c09dc5/src/components/side-menu"
|
||
const slashIdx = relativeTail.indexOf(path.sep);
|
||
if (slashIdx < 0) return null;
|
||
const pathAfterVersionId = relativeTail.substring(slashIdx + 1);
|
||
// pathAfterVersionId 形如 "src/components/side-menu"
|
||
|
||
const realPath = path.join(projectRoot, pathAfterVersionId);
|
||
|
||
return resolveModuleFile(realPath);
|
||
},
|
||
|
||
configureServer(server) {
|
||
const projectRoot = process.cwd();
|
||
|
||
// 检查 Git 是否可用
|
||
(async () => {
|
||
try {
|
||
await execAsync('git --version', { cwd: projectRoot });
|
||
gitAvailable = true;
|
||
console.log('[Git 版本管理] ✅ Git 已就绪');
|
||
} catch (error: any) {
|
||
gitAvailable = false;
|
||
gitCheckError = error.message;
|
||
console.warn('[Git 版本管理] ⚠️ Git 未安装或不可用');
|
||
console.warn('[Git 版本管理] 💡 请安装 Git 以使用版本管理功能: https://git-scm.com/downloads');
|
||
}
|
||
})();
|
||
|
||
// 服务器启动时清理临时版本文件
|
||
const gitVersionsDir = path.join(projectRoot, '.git-versions');
|
||
|
||
if (fs.existsSync(gitVersionsDir)) {
|
||
try {
|
||
console.log('[Git 版本管理] 清理临时版本文件...');
|
||
fs.rmSync(gitVersionsDir, { recursive: true, force: true });
|
||
console.log('[Git 版本管理] ✅ 临时版本文件已清理');
|
||
} catch (error) {
|
||
console.error('[Git 版本管理] ⚠️ 清理临时文件失败:', error);
|
||
}
|
||
}
|
||
|
||
// Helper function to parse JSON body
|
||
const parseBody = (req: any): Promise<any> => {
|
||
return new Promise((resolve, reject) => {
|
||
let body = '';
|
||
req.on('data', (chunk: any) => body += chunk);
|
||
req.on('end', () => {
|
||
try {
|
||
resolve(body ? JSON.parse(body) : {});
|
||
} catch (e) {
|
||
reject(new Error('Invalid JSON in request body'));
|
||
}
|
||
});
|
||
req.on('error', reject);
|
||
});
|
||
};
|
||
|
||
// Helper function to send JSON response
|
||
const sendJSON = (res: any, statusCode: number, data: any) => {
|
||
res.statusCode = statusCode;
|
||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||
res.end(JSON.stringify(data));
|
||
};
|
||
|
||
// Helper function to check git availability and return error response if not available
|
||
const checkGitAvailable = (res: any): boolean => {
|
||
if (!gitAvailable) {
|
||
sendJSON(res, 503, {
|
||
error: 'Git 未安装或不可用',
|
||
message: '版本管理功能需要 Git 支持。请先安装 Git 后重启开发服务器。',
|
||
details: gitCheckError || undefined
|
||
});
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// Helper function to execute git command
|
||
const execGit = async (command: string, cwd: string) => {
|
||
try {
|
||
const { stdout, stderr } = await execAsync(command, { cwd, maxBuffer: 1024 * 1024 * 10 });
|
||
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
||
} catch (error: any) {
|
||
// 提供更友好的错误信息
|
||
if (error.message.includes('not a git repository')) {
|
||
throw new Error('当前项目不是 Git 仓库。请先运行 "git init" 初始化仓库。');
|
||
}
|
||
throw new Error(`Git 命令执行失败: ${error.message}`);
|
||
}
|
||
};
|
||
|
||
const normalizeTargetPath = (rawTargetPath: string, projectRoot: string) => {
|
||
const sourceRoot = path.resolve(projectRoot, 'src');
|
||
const trimmedPath = String(rawTargetPath || '').trim();
|
||
|
||
if (!trimmedPath) {
|
||
throw new Error('Missing path parameter');
|
||
}
|
||
|
||
let normalizedPath = trimmedPath.replace(/\\/g, '/');
|
||
const normalizedProjectRoot = projectRoot.replace(/\\/g, '/').replace(/\/+$/, '');
|
||
const normalizedSourceRoot = sourceRoot.replace(/\\/g, '/').replace(/\/+$/, '');
|
||
|
||
if (normalizedPath.startsWith(normalizedSourceRoot + '/')) {
|
||
normalizedPath = normalizedPath.slice(normalizedSourceRoot.length + 1);
|
||
} else if (normalizedPath.startsWith(normalizedProjectRoot + '/src/')) {
|
||
normalizedPath = normalizedPath.slice(normalizedProjectRoot.length + '/src/'.length);
|
||
} else {
|
||
const srcMarkerIndex = normalizedPath.lastIndexOf('/src/');
|
||
if (srcMarkerIndex >= 0) {
|
||
normalizedPath = normalizedPath.slice(srcMarkerIndex + '/src/'.length);
|
||
} else if (normalizedPath.startsWith('src/')) {
|
||
normalizedPath = normalizedPath.slice('src/'.length);
|
||
}
|
||
}
|
||
|
||
normalizedPath = normalizedPath
|
||
.replace(/^\/+/, '')
|
||
.replace(/\/index\.(t|j)sx?$/i, '')
|
||
.replace(/\/+$/, '');
|
||
|
||
if (!normalizedPath) {
|
||
throw new Error('Invalid path');
|
||
}
|
||
|
||
const segments = normalizedPath.split('/').filter(Boolean);
|
||
if (segments.length === 0 || segments.some((segment) => segment === '.' || segment === '..')) {
|
||
throw new Error('Invalid path');
|
||
}
|
||
|
||
const resolvedFolderPath = path.resolve(sourceRoot, normalizedPath);
|
||
const relativeToSourceRoot = path.relative(sourceRoot, resolvedFolderPath);
|
||
|
||
if (!relativeToSourceRoot || relativeToSourceRoot.startsWith('..') || path.isAbsolute(relativeToSourceRoot)) {
|
||
throw new Error('Invalid path');
|
||
}
|
||
|
||
return {
|
||
sourceRoot,
|
||
targetPath: segments.join('/'),
|
||
folderPath: resolvedFolderPath,
|
||
};
|
||
};
|
||
|
||
// Main middleware for git version API
|
||
server.middlewares.use(async (req: any, res: any, next: any) => {
|
||
// Only handle /api/git/* routes
|
||
if (!req.url.startsWith('/api/git')) {
|
||
return next();
|
||
}
|
||
|
||
try {
|
||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||
const pathname = url.pathname;
|
||
|
||
// Route: GET /api/git/history - Get git history for a folder
|
||
if (pathname === '/api/git/history' && req.method === 'GET') {
|
||
if (!checkGitAvailable(res)) return;
|
||
|
||
const rawTargetPath = url.searchParams.get('path'); // e.g., 'prototypes/home' or 'components/button'
|
||
|
||
if (!rawTargetPath) {
|
||
sendJSON(res, 400, { error: 'Missing path parameter' });
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
let targetPath = '';
|
||
let folderPath = '';
|
||
|
||
try {
|
||
({ targetPath, folderPath } = normalizeTargetPath(rawTargetPath, projectRoot));
|
||
} catch (error: any) {
|
||
sendJSON(res, 403, { error: 'Invalid path' });
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(folderPath)) {
|
||
sendJSON(res, 404, { error: 'Folder not found' });
|
||
return;
|
||
}
|
||
|
||
// Get git log for the folder (last 20 commits)
|
||
const gitCommand = `git log -20 --pretty=format:'%H|%an|%ae|%at|%s' -- src/${targetPath}`;
|
||
const { stdout } = await execGit(gitCommand, projectRoot);
|
||
|
||
if (!stdout) {
|
||
sendJSON(res, 200, { commits: [], hasUncommitted: false });
|
||
return;
|
||
}
|
||
|
||
const commits = stdout.split('\n').map(line => {
|
||
const [hash, author, email, timestamp, message] = line.split('|');
|
||
return {
|
||
hash,
|
||
author,
|
||
email,
|
||
timestamp: parseInt(timestamp) * 1000, // Convert to milliseconds
|
||
message,
|
||
date: new Date(parseInt(timestamp) * 1000).toISOString()
|
||
};
|
||
});
|
||
|
||
// Check for uncommitted changes in the folder
|
||
const statusCommand = `git status --porcelain -- src/${targetPath}`;
|
||
const { stdout: statusOutput } = await execGit(statusCommand, projectRoot);
|
||
const hasUncommitted = statusOutput.length > 0;
|
||
|
||
sendJSON(res, 200, { commits, hasUncommitted, uncommittedFiles: statusOutput });
|
||
return;
|
||
}
|
||
|
||
// Route: POST /api/git/restore - Restore folder to a specific commit
|
||
if (pathname === '/api/git/restore' && req.method === 'POST') {
|
||
if (!checkGitAvailable(res)) return;
|
||
|
||
const body = await parseBody(req);
|
||
const { path: rawTargetPath, commitHash } = body;
|
||
|
||
if (!rawTargetPath || !commitHash) {
|
||
sendJSON(res, 400, { error: 'Missing path or commitHash parameter' });
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
let targetPath = '';
|
||
let folderPath = '';
|
||
|
||
try {
|
||
({ targetPath, folderPath } = normalizeTargetPath(rawTargetPath, projectRoot));
|
||
} catch (error: any) {
|
||
sendJSON(res, 403, { error: 'Invalid path' });
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(folderPath)) {
|
||
sendJSON(res, 404, { error: 'Folder not found' });
|
||
return;
|
||
}
|
||
|
||
// Verify commit exists
|
||
const verifyCommand = `git cat-file -t ${commitHash}`;
|
||
try {
|
||
await execGit(verifyCommand, projectRoot);
|
||
} catch (error) {
|
||
sendJSON(res, 400, { error: 'Invalid commit hash' });
|
||
return;
|
||
}
|
||
|
||
// Restore folder to specific commit
|
||
const restoreCommand = `git checkout ${commitHash} -- src/${targetPath}`;
|
||
await execGit(restoreCommand, projectRoot);
|
||
|
||
sendJSON(res, 200, { success: true, message: 'Folder restored successfully' });
|
||
return;
|
||
}
|
||
|
||
// Route: POST /api/git/commit - Commit changes for a folder
|
||
if (pathname === '/api/git/commit' && req.method === 'POST') {
|
||
if (!checkGitAvailable(res)) return;
|
||
|
||
const body = await parseBody(req);
|
||
const { path: rawTargetPath, message } = body;
|
||
|
||
if (!rawTargetPath || !message) {
|
||
sendJSON(res, 400, { error: 'Missing path or message parameter' });
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
let targetPath = '';
|
||
let folderPath = '';
|
||
|
||
try {
|
||
({ targetPath, folderPath } = normalizeTargetPath(rawTargetPath, projectRoot));
|
||
} catch (error: any) {
|
||
sendJSON(res, 403, { error: 'Invalid path' });
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(folderPath)) {
|
||
sendJSON(res, 404, { error: 'Folder not found' });
|
||
return;
|
||
}
|
||
|
||
// Check if there are changes to commit
|
||
const statusCommand = `git status --porcelain -- src/${targetPath}`;
|
||
const { stdout: statusOutput } = await execGit(statusCommand, projectRoot);
|
||
|
||
if (!statusOutput) {
|
||
sendJSON(res, 400, { error: 'No changes to commit' });
|
||
return;
|
||
}
|
||
|
||
// Add and commit changes
|
||
const addCommand = `git add src/${targetPath}`;
|
||
await execGit(addCommand, projectRoot);
|
||
|
||
const commitCommand = `git commit -m "${message.replace(/"/g, '\\"')}"`;
|
||
const { stdout: commitOutput } = await execGit(commitCommand, projectRoot);
|
||
|
||
sendJSON(res, 200, { success: true, message: 'Changes committed successfully', output: commitOutput });
|
||
return;
|
||
}
|
||
|
||
// Route: GET /api/git/diff - Get diff for uncommitted changes
|
||
if (pathname === '/api/git/diff' && req.method === 'GET') {
|
||
if (!checkGitAvailable(res)) return;
|
||
|
||
const rawTargetPath = url.searchParams.get('path');
|
||
|
||
if (!rawTargetPath) {
|
||
sendJSON(res, 400, { error: 'Missing path parameter' });
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
let targetPath = '';
|
||
let folderPath = '';
|
||
|
||
try {
|
||
({ targetPath, folderPath } = normalizeTargetPath(rawTargetPath, projectRoot));
|
||
} catch (error: any) {
|
||
sendJSON(res, 403, { error: 'Invalid path' });
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(folderPath)) {
|
||
sendJSON(res, 404, { error: 'Folder not found' });
|
||
return;
|
||
}
|
||
|
||
// Get diff for uncommitted changes
|
||
const diffCommand = `git diff -- src/${targetPath}`;
|
||
const { stdout: diffOutput } = await execGit(diffCommand, projectRoot);
|
||
|
||
// Get list of changed files
|
||
const statusCommand = `git status --porcelain -- src/${targetPath}`;
|
||
const { stdout: statusOutput } = await execGit(statusCommand, projectRoot);
|
||
|
||
const changedFiles = statusOutput.split('\n').filter(line => line.trim()).map(line => {
|
||
const status = line.substring(0, 2).trim();
|
||
const file = line.substring(3);
|
||
return { status, file };
|
||
});
|
||
|
||
sendJSON(res, 200, { diff: diffOutput, changedFiles });
|
||
return;
|
||
}
|
||
|
||
// Route: POST /api/git/build-version - Extract version files (no build)
|
||
if (pathname === '/api/git/build-version' && req.method === 'POST') {
|
||
if (!checkGitAvailable(res)) return;
|
||
|
||
const body = await parseBody(req);
|
||
const { path: rawTargetPath, commitHash } = body;
|
||
|
||
if (!rawTargetPath || !commitHash) {
|
||
sendJSON(res, 400, { error: 'Missing path or commitHash parameter' });
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
let targetPath = '';
|
||
let folderPath = '';
|
||
|
||
try {
|
||
({ targetPath, folderPath } = normalizeTargetPath(rawTargetPath, projectRoot));
|
||
} catch (error: any) {
|
||
sendJSON(res, 403, { error: 'Invalid path' });
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(folderPath)) {
|
||
sendJSON(res, 404, { error: 'Folder not found' });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Create temporary directory for version files
|
||
const versionId = commitHash.substring(0, 8);
|
||
const tempDir = path.join(projectRoot, '.git-versions', versionId);
|
||
const srcPath = path.join(tempDir, 'src', targetPath);
|
||
|
||
// Check if already extracted
|
||
if (fs.existsSync(srcPath)) {
|
||
console.log('[Git 版本管理] 使用已提取的版本:', versionId);
|
||
const specPath = path.join(srcPath, 'spec.md');
|
||
const indexPath = path.join(srcPath, 'index.tsx');
|
||
const hasSpec = fs.existsSync(specPath);
|
||
const hasPrototype = fs.existsSync(indexPath);
|
||
|
||
sendJSON(res, 200, {
|
||
success: true,
|
||
versionId,
|
||
hasSpec,
|
||
hasPrototype,
|
||
specUrl: hasSpec ? `/${targetPath}/spec.html?ver=${versionId}` : null,
|
||
prototypeUrl: hasPrototype ? `/${targetPath}/index.html?ver=${versionId}` : null,
|
||
cached: true
|
||
});
|
||
return;
|
||
}
|
||
|
||
fs.mkdirSync(srcPath, { recursive: true });
|
||
|
||
console.log('[Git 版本管理] 开始提取版本文件:', versionId, targetPath);
|
||
|
||
// Get list of files in the commit
|
||
const listCommand = `git ls-tree -r --name-only ${commitHash} src/${targetPath}`;
|
||
const { stdout: fileList } = await execGit(listCommand, projectRoot);
|
||
|
||
if (!fileList.trim()) {
|
||
sendJSON(res, 404, { error: 'No files found in this version' });
|
||
return;
|
||
}
|
||
|
||
// Extract each file
|
||
const files = fileList.trim().split('\n');
|
||
for (const file of files) {
|
||
const targetFile = path.join(tempDir, file);
|
||
const targetDir = path.dirname(targetFile);
|
||
|
||
// Create directory if needed
|
||
fs.mkdirSync(targetDir, { recursive: true });
|
||
|
||
// Get file content from git
|
||
const showCommand = `git show ${commitHash}:${file}`;
|
||
const { stdout: content } = await execGit(showCommand, projectRoot);
|
||
fs.writeFileSync(targetFile, content);
|
||
}
|
||
|
||
console.log('[Git 版本管理] ✅ 文件提取完成');
|
||
|
||
// Check what files exist in the version
|
||
const specPath = path.join(srcPath, 'spec.md');
|
||
const indexPath = path.join(srcPath, 'index.tsx');
|
||
const hasSpec = fs.existsSync(specPath);
|
||
const hasPrototype = fs.existsSync(indexPath);
|
||
|
||
// Generate URLs for accessing the version
|
||
const specUrl = hasSpec ? `/${targetPath}/spec.html?ver=${versionId}` : null;
|
||
const prototypeUrl = hasPrototype ? `/${targetPath}/index.html?ver=${versionId}` : null;
|
||
|
||
sendJSON(res, 200, {
|
||
success: true,
|
||
versionId,
|
||
hasSpec,
|
||
hasPrototype,
|
||
specUrl,
|
||
prototypeUrl
|
||
});
|
||
} catch (error: any) {
|
||
console.error('Extract version error:', error);
|
||
sendJSON(res, 500, { error: error.message || 'Failed to extract version' });
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Route: GET /api/git/version-file - Get file from specific version
|
||
if (pathname.startsWith('/api/git/version-file/') && req.method === 'GET') {
|
||
try {
|
||
// Parse URL: /api/git/version-file/{versionId}/{path}/spec.md
|
||
const parts = pathname.replace('/api/git/version-file/', '').split('/');
|
||
if (parts.length < 3) {
|
||
sendJSON(res, 400, { error: 'Invalid URL format' });
|
||
return;
|
||
}
|
||
|
||
const versionId = parts[0];
|
||
const fileName = parts[parts.length - 1];
|
||
const targetPath = parts.slice(1, -1).join('/');
|
||
|
||
// Validate
|
||
if (targetPath.includes('..') || targetPath.startsWith('/')) {
|
||
sendJSON(res, 403, { error: 'Invalid path' });
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
const tempDir = path.join(projectRoot, '.git-versions', versionId);
|
||
const filePath = path.join(tempDir, 'src', targetPath, fileName);
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
sendJSON(res, 404, { error: 'File not found in version' });
|
||
return;
|
||
}
|
||
|
||
const content = fs.readFileSync(filePath, 'utf8');
|
||
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
||
res.end(content);
|
||
} catch (error: any) {
|
||
console.error('Get version file error:', error);
|
||
sendJSON(res, 500, { error: error.message });
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Route: GET /api/git/status - Check git repository status
|
||
if (pathname === '/api/git/status' && req.method === 'GET') {
|
||
// 对于 status 接口,即使 Git 不可用也要返回状态信息
|
||
if (!gitAvailable) {
|
||
sendJSON(res, 200, {
|
||
initialized: false,
|
||
isGitRepo: false,
|
||
gitAvailable: false,
|
||
error: 'Git 未安装或不可用',
|
||
message: '版本管理功能需要 Git 支持。请先安装 Git 后重启开发服务器。'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const projectRoot = process.cwd();
|
||
|
||
try {
|
||
// Check if git is initialized
|
||
const { stdout: branchOutput } = await execGit('git branch --show-current', projectRoot);
|
||
const currentBranch = branchOutput || 'main';
|
||
|
||
// Get repository status
|
||
const { stdout: statusOutput } = await execGit('git status --porcelain', projectRoot);
|
||
const hasChanges = statusOutput.length > 0;
|
||
|
||
sendJSON(res, 200, {
|
||
initialized: true,
|
||
currentBranch,
|
||
hasChanges,
|
||
isGitRepo: true,
|
||
gitAvailable: true
|
||
});
|
||
} catch (error: any) {
|
||
sendJSON(res, 200, {
|
||
initialized: false,
|
||
isGitRepo: false,
|
||
gitAvailable: true,
|
||
error: 'Git 仓库未初始化',
|
||
message: error.message.includes('not a git repository')
|
||
? '当前项目不是 Git 仓库。请先运行 "git init" 初始化仓库。'
|
||
: error.message
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// No route matched
|
||
sendJSON(res, 404, { error: 'API endpoint not found' });
|
||
|
||
} catch (error: any) {
|
||
console.error('Git version API error:', error);
|
||
sendJSON(res, 500, { error: error.message || 'Internal server error' });
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|