feat: sync full workspace including web modules, docs, and configurations to Gitea
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>
This commit is contained in:
635
axhub-make/vite-plugins/gitVersionApiPlugin.ts
Normal file
635
axhub-make/vite-plugins/gitVersionApiPlugin.ts
Normal file
@@ -0,0 +1,635 @@
|
||||
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' });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user