Files
foka-ci/apps/server/libs/git-manager.ts
hurole d22fdc9618 feat: 实现环境变量预设功能 & 移除稀疏检出
## 后端改动
- 添加 Project.envPresets 字段(JSON 格式)
- 移除 Deployment.env 字段,统一使用 envVars
- 更新部署 DTO,支持 envVars (Record<string, string>)
- pipeline-runner 支持解析并注入 envVars 到环境
- 移除稀疏检出模板和相关环境变量
- 优化代码格式(Biome lint & format)

## 前端改动
- 新增 EnvPresetsEditor 组件(支持单选/多选/输入框类型)
- 项目创建/编辑界面集成环境预设编辑器
- 部署界面基于预设动态生成环境变量表单
- 移除稀疏检出表单项
- 项目详情页添加环境变量预设配置 tab
- 优化部署界面布局(基本参数 & 环境变量分区)

## 文档
- 添加完整文档目录结构(docs/)
- 创建设计文档 design-0005(部署流程重构)
- 添加 API 文档、架构设计文档等

## 数据库
- 执行 prisma db push 同步 schema 变更
2026-01-03 22:59:20 +08:00

281 lines
6.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Git 管理器
* 封装 Git 操作:克隆、更新、分支切换等
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { $ } from 'zx';
import { log } from './logger';
/**
* 工作目录状态
*/
export const WorkspaceDirStatus = {
NOT_CREATED: 'not_created', // 目录不存在
EMPTY: 'empty', // 目录存在但为空
NO_GIT: 'no_git', // 目录存在但不是 Git 仓库
READY: 'ready', // 目录存在且包含 Git 仓库
} as const;
export type WorkspaceDirStatus =
(typeof WorkspaceDirStatus)[keyof typeof WorkspaceDirStatus];
/**
* 工作目录状态信息
*/
export interface WorkspaceStatus {
status: WorkspaceDirStatus;
exists: boolean;
isEmpty?: boolean;
hasGit?: boolean;
}
/**
* Git仓库信息
*/
export interface GitInfo {
branch?: string;
lastCommit?: string;
lastCommitMessage?: string;
}
/**
* Git管理器类
*/
export class GitManager {
static readonly TAG = 'GitManager';
/**
* 检查工作目录状态
*/
static async checkWorkspaceStatus(dirPath: string): Promise<WorkspaceStatus> {
try {
// 检查目录是否存在
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
return {
status: WorkspaceDirStatus.NOT_CREATED,
exists: false,
};
}
// 检查目录是否为空
const files = await fs.readdir(dirPath);
if (files.length === 0) {
return {
status: WorkspaceDirStatus.EMPTY,
exists: true,
isEmpty: true,
};
}
// 检查是否包含 .git 目录
const gitDir = path.join(dirPath, '.git');
try {
const gitStats = await fs.stat(gitDir);
if (gitStats.isDirectory()) {
return {
status: WorkspaceDirStatus.READY,
exists: true,
isEmpty: false,
hasGit: true,
};
}
} catch {
return {
status: WorkspaceDirStatus.NO_GIT,
exists: true,
isEmpty: false,
hasGit: false,
};
}
return {
status: WorkspaceDirStatus.NO_GIT,
exists: true,
isEmpty: false,
hasGit: false,
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return {
status: WorkspaceDirStatus.NOT_CREATED,
exists: false,
};
}
throw error;
}
}
/**
* 克隆仓库到指定目录
* @param repoUrl 仓库URL
* @param dirPath 目标目录
* @param branch 分支名
* @param token Gitea access token可选
*/
static async cloneRepository(
repoUrl: string,
dirPath: string,
branch: string,
token?: string,
): Promise<void> {
try {
log.info(
GitManager.TAG,
'Cloning repository: %s to %s (branch: %s)',
repoUrl,
dirPath,
branch,
);
// 如果提供了token嵌入到URL中
let cloneUrl = repoUrl;
if (token) {
const url = new URL(repoUrl);
url.username = token;
cloneUrl = url.toString();
}
// 使用 zx 执行 git clone浅克隆
$.verbose = false; // 禁止打印敏感信息
await $`git clone --depth 1 --branch ${branch} ${cloneUrl} ${dirPath}`;
$.verbose = true;
log.info(GitManager.TAG, 'Repository cloned successfully: %s', dirPath);
} catch (error) {
log.error(
GitManager.TAG,
'Failed to clone repository: %s to %s, error: %s',
repoUrl,
dirPath,
(error as Error).message,
);
throw new Error(`克隆仓库失败: ${(error as Error).message}`);
}
}
/**
* 更新已存在的仓库
* @param dirPath 仓库目录
* @param branch 目标分支
*/
static async updateRepository(
dirPath: string,
branch: string,
): Promise<void> {
try {
log.info(
GitManager.TAG,
'Updating repository: %s (branch: %s)',
dirPath,
branch,
);
$.verbose = false;
// 切换到仓库目录
const originalCwd = process.cwd();
process.chdir(dirPath);
try {
// 获取最新代码
await $`git fetch --depth 1 origin ${branch}`;
// 切换到目标分支
await $`git checkout ${branch}`;
// 拉取最新代码
await $`git pull origin ${branch}`;
log.info(
GitManager.TAG,
'Repository updated successfully: %s (branch: %s)',
dirPath,
branch,
);
} finally {
process.chdir(originalCwd);
$.verbose = true;
}
} catch (error) {
log.error(
GitManager.TAG,
'Failed to update repository: %s (branch: %s), error: %s',
dirPath,
branch,
(error as Error).message,
);
throw new Error(`更新仓库失败: ${(error as Error).message}`);
}
}
/**
* 获取Git仓库信息
*/
static async getGitInfo(dirPath: string): Promise<GitInfo> {
try {
const originalCwd = process.cwd();
process.chdir(dirPath);
try {
$.verbose = false;
const branchResult = await $`git branch --show-current`;
const commitResult = await $`git rev-parse --short HEAD`;
const messageResult = await $`git log -1 --pretty=%B`;
$.verbose = true;
return {
branch: branchResult.stdout.trim(),
lastCommit: commitResult.stdout.trim(),
lastCommitMessage: messageResult.stdout.trim(),
};
} finally {
process.chdir(originalCwd);
}
} catch (error) {
log.error(
GitManager.TAG,
'Failed to get git info: %s, error: %s',
dirPath,
(error as Error).message,
);
return {};
}
}
/**
* 创建目录(递归)
*/
static async ensureDirectory(dirPath: string): Promise<void> {
try {
await fs.mkdir(dirPath, { recursive: true });
log.info(GitManager.TAG, 'Directory created: %s', dirPath);
} catch (error) {
log.error(
GitManager.TAG,
'Failed to create directory: %s, error: %s',
dirPath,
(error as Error).message,
);
throw new Error(`创建目录失败: ${(error as Error).message}`);
}
}
/**
* 获取目录大小
*/
static async getDirectorySize(dirPath: string): Promise<number> {
try {
const { stdout } = await $`du -sb ${dirPath}`;
const size = Number.parseInt(stdout.split('\t')[0], 10);
return size;
} catch (error) {
log.error(
GitManager.TAG,
'Failed to get directory size: %s, error: %s',
dirPath,
(error as Error).message,
);
return 0;
}
}
}