前言
gitlib 本身没有比较详细的代码贡献视图,如果我们想查看每一个开发人员在不同项目上的贡献情况,包括提交次数、删除数、增加数、代码总量等等指标,以及拉出来每次提交的具体信息来看看是否符合团队规范,目前存在的一些开源工具是完全无法实现的,相信很多小伙伴和我一样找了半天没找到想要的。
所以这种高度定制化的需求那就必须要自己写一个脚本去跑数据了。
需求开发
功能描述
那么我们要实现哪些功能呢?
首先,按作者统计代码提交量,统计不同开发人员在一个项目组内不同项目上的贡献量和总计,如下表格:
作者 | 邮箱 | 项目 | 提交次数 | 增加行数 | 删除行数 | 变更行数 | 文件数 | 代码量(KB) |
---|---|---|---|---|---|---|---|---|
zhangsan | zhangsan@gmail.com | 【总计】 | 39 | 9520 | 1900 | 11420 | 151 | 352.17 |
zhangsan | zhangsan@gmail.com | A | 7 | 65 | 98 | 163 | 10 | 10.79 |
zhangsan | zhangsan@gmail.com | B | 26 | 8953 | 1480 | 10433 | 118 | 307.64 |
zhangsan | zhangsan@gmail.com | C | 6 | 502 | 322 | 824 | 23 | 33.74 |
按作者列出所有提交,可以查看具体提交信息,如下表格:
作者 | 邮箱 | 项目 | 分支名 | 标签 | 提交时间 | 提交信息 |
---|---|---|---|---|---|---|
zhangsan | zhangsan@gmail.com | A | dev | v1.0.0 | 2024-12-04 10:12:44 | feat: xxx |
zhangsan | zhangsan@gmail.com | B | dev | v1.0.1 | 2024-12-03 17:01:12 | chore: xxx |
zhangsan | zhangsan@gmail.com | C | dev | v1.0.0 | 2024-12-03 18:06:50 | fix: xxx |
当你发现某个人在某个项目上有大量代码提交,就可以查看他的提交都是什么,或者某个人在多个项目上有相同的代码量提交,也可以看一下具体提交信息是否是对框架进行了统一改动等等。以上两种统计结果可以配合使用。
开发思路
gitlib 提供 api 可以让我们在脚本中调用,来获取仓库、代码提交等信息
具体用到的 api 有:
- 根据组id获取该组下的所有项目,最大返回100个,可以分页获取,按最后活跃时间排序
GET /groups/:id/projects?per_page=100&include_subgroups=true&order_by=last_activity_at&sort=desc
- 获取该组下所有项目在指定时间范围内的提交信息,默认返回100个,可以分页获取,按提交时间排序,获取所有分支(因为此时有些开发分支还未合并),并带上统计信息
GET /projects/:id/repository/commits?since=2024-12-01&until=2024-12-31&per_page=100&page=1&all=true&with_stats=true
- 获取某个提交的变更信息,统计新增、删除、修改行数,文件数
GET /projects/:id/repository/commits/:sha/diff
- 获取提交对应的分支信息,拿到分支名、标签名
GET /projects/:id/repository/commits/:sha/refs
其他api的用法可以参考 gitlab.cn/docs/jh/api…
准备好这些api,还不着急开发,还有一些点需要考虑:
- 并发请求控制
项目几十个,提交信息几百条,一个一个请求分析那得搞到猴年马月,所以需要并发请求。
- 自动重试机制
但是并发请求如果服务器qps不高的话很容易超时,又会导致请求失败,那么统计的结果可能就是不准确的,所以要进行多次重试。多次重试仍旧失败的,则记录下来,后续人工处理。
- 生成 Markdown 格式报告
再加一个错误报告
项目 | 作者 | 操作 | URL | 错误信息 |
---|
- 过滤掉一些不关心的项目
- 支持自定义文件类型过滤
例如有些文件如 package.json
等,统计意义不大。
- 支持忽略特定路径文件
例如有些文件夹如 dist
等,完全没必要统计。
编写js代码
import fetch from 'node-fetch';
import fs from 'fs/promises';
const config = {
GITLAB_API: 'http://gitlab.xx.cn/api/v4',
// GITLAB_COOKIE: `xxx`, // 如果需要使用cookie,请在这里填写,cookie token 二选一
GITLAB_TOKEN: "",
GROUP_ID: '2177',
START_DATE: '2024-12-1',
END_DATE: '2025-01-31',
PROJECTS_NUM: 10, // 获取该组下需要统计的项目数量最大100
EXCLUDED_PROJECTS: ['project1', 'project2'],
VALID_EXTENSIONS: [
'.js', '.mjs','.cjs', '.ts', '.jsx', '.tsx', '.css', '.scss', '.sass', '.html', '.sh', '.vue', '.svelte', '.rs'
],
MAX_CONCURRENT_REQUESTS: 20, // 添加最大并发请求数配置
IGNORED_PATHS: [
"dist", "node_modules/", "build/", ".husky", "lintrc", "public/"
]
};
// 添加全局失败记录对象
const failureStats = {
projects: [], // 获取项目列表失败
commits: [], // 获取提交记录失败
diffs: [] // 获取差异失败
};
// 添加请求计时和细节输出的重试函数
async function fetchWithRetry(url, options, context = {}) {
const retries = 30;
for (let i = 0; i < retries; i++) {
// 每次重试都创建新的 AbortController
const controller = new AbortController();
const timeout = options.timeout || 5000;
// 为每次尝试创建新的 options 对象
const currentOptions = {
...options,
signal: controller.signal
};
const startTime = Date.now();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, currentOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
clearTimeout(timeoutId);
return response;
} catch (error) {
const endTime = Date.now();
const duration = endTime - startTime;
console.error(`[请求失败] 第 ${i + 1}/${retries} 次尝试,耗时: ${duration}ms`);
console.error(`错误信息: ${error.name === 'AbortError' ? '请求超时' : error.message}`);
clearTimeout(timeoutId);
if (i === retries - 1) {
// 记录最终失败的请求
if (context.type && context.details) {
failureStats[context.type].push({
url,
...context.details,
error: error.message
});
}
throw error;
}
console.log(`立即开始第 ${i + 2} 次重试...`);
}
}
}
// 获取群组项目
async function getGroupProjects() {
try {
const url = `${config.GITLAB_API}/groups/${config.GROUP_ID}/projects?per_page=${config.PROJECTS_NUM}&include_subgroups=true&order_by=last_activity_at&sort=desc`;
const response = await fetchWithRetry(
url,
{
headers: {
// 'Cookie': config.GITLAB_COOKIE,
'Private-Token': config.GITLAB_TOKEN,
'Content-Type': 'application/json',
},
},
{
type: 'projects',
details: {
operation: '获取项目列表'
}
}
);
const projects = await response.json();
console.log(`[获取成功] 找到 ${projects.length} 个项目`);
return projects;
} catch (error) {
console.error('[获取失败]', error.message);
throw error;
}
}
// 获取项目提交统计
async function getProjectCommitStats(projectId, authorEmail, since, until, projectName) {
try {
let page = 1;
let allCommits = [];
console.log(`正在处理项目: ${projectName}`);
while (true) {
// &with_stats=true 可以获取提交的统计信息,但是无法筛选文件
const url = `${config.GITLAB_API}/projects/${projectId}/repository/commits?since=${since}&until=${until}&per_page=100&page=${page}&all=true&with_stats=true`;
const response = await fetchWithRetry(
url,
{
headers: {
// 'Cookie': config.GITLAB_COOKIE,
'Private-Token': config.GITLAB_TOKEN,
'Content-Type': 'application/json',
},
},
{
type: 'commits',
details: {
projectName,
authorEmail: authorEmail,
operation: `获取提交记录`
}
}
);
// 获取当前页的提交
const commits = await response.json();
allCommits = allCommits.concat(commits);
// 获取下一页页码
const nextPage = response.headers.get('x-next-page');
// 如果没有下一页或者本页没有数据,就退出循环
if (!nextPage || commits.length === 0) {
break;
}
// 继续获取下一页
page = parseInt(nextPage);
}
return allCommits;
} catch (error) {
console.error(`获取项目 ${projectId} 的提交失败:`, error.message);
return [];
}
}
// 分析文件变更
async function analyzeCommitDiffs(projectId, projectName, sha, authorEmail) {
try {
const url = `${config.GITLAB_API}/projects/${projectId}/repository/commits/${sha}/diff`;
const response = await fetchWithRetry(
url,
{
headers: {
// 'Cookie': config.GITLAB_COOKIE,
'Private-Token': config.GITLAB_TOKEN,
'Content-Type': 'application/json',
},
},
{
type: 'diffs',
details: {
projectName,
authorEmail: authorEmail,
operation: '获取提交差异'
}
}
);
const diffs = await response.json();
let stats = {
additions: 0,
deletions: 0,
lines: 0,
files: 0,
size: 0
};
const validExtensions = config.VALID_EXTENSIONS;
const ignoredPaths = config.IGNORED_PATHS;
for (const diff of diffs) {
const filePath = diff.new_path || diff.old_path;
const ext = '.' + filePath.split('.').pop();
// 检查是否应该忽略此文件
if (ignoredPaths.some(path => filePath.includes(path))) {
continue;
}
// 检查文件扩展名是否在允许列表中
if (!validExtensions.includes(ext)) {
continue;
}
stats.files++;
if (diff.diff) {
const lines = diff.diff.split('\n');
let additions = 0;
let deletions = 0;
for (const line of lines) {
if (line.startsWith('+') && !line.startsWith('+++')) {
additions++;
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions++;
}
}
stats.additions += additions;
stats.deletions += deletions;
stats.lines += additions + deletions;
stats.size += new TextEncoder().encode(diff.diff).length; // 转换为UTF-8字节
}
}
return stats;
} catch (error) {
console.error(`分析提交 ${sha} 的差异失败:`, error.message);
return { additions: 0, deletions: 0, lines: 0, files: 0, size: 0 };
}
}
// 获取提交所属的分支
async function getCommitBranches(projectId, commitSha, projectName, authorEmail) {
try {
const url = `${config.GITLAB_API}/projects/${projectId}/repository/commits/${commitSha}/refs`;
const response = await fetchWithRetry(
url,
{
headers: {
'Private-Token': config.GITLAB_TOKEN,
'Content-Type': 'application/json',
},
},
{
type: 'refs',
details: {
authorEmail,
projectName,
operation: '获取提交对应的分支信息'
}
}
);
const refs = await response.json();
// 过滤出分支(type === 'branch')
const branches = refs.find(ref => ref.type === 'branch');
const tags = refs.find(ref => ref.type === 'tag');
return {
branches: branches ? branches.name : 'unknown',
tags: tags ? tags.name : 'unknown',
};
} catch (error) {
console.error(`获取提交 ${commitSha} 的分支信息失败:`, error.message);
return 'unknown';
}
}
// 生成Markdown报告
async function generateReport(authorStats) {
// 将作者数据转换为数组并按总代码量排序
const sortedAuthors = Object.entries(authorStats)
.map(([authorName, stats]) => ({
authorName,
...stats,
}))
.sort((a, b) => b.totalSize - a.totalSize); // 按总代码量降序排序
const report = [`# GitLab 代码提交统计报告\n`,
`统计期间: ${config.START_DATE} 至 ${config.END_DATE}\n`,
'## 按作者统计代码信息\n',
'| 作者 | 邮箱 | 项目 | 提交次数 | 增加行数 | 删除行数 | 变更行数 | 文件数 | 代码量(KB) |',
'|--------|------|------|----------|----------|----------|----------|---------|------------|'];
// 使用排序后的数组生成报告
for (const authorData of sortedAuthors) {
const { authorEmail, authorName, projects, totalCommits, totalAdditions, totalDeletions, totalLines, totalFiles, totalSize } = authorData;
// 输出作者总计
report.push(
`| 【${authorName || '未知'}】 | ${authorEmail} | 【总计】 | ${totalCommits} | ${totalAdditions} | ${totalDeletions} | ${totalLines} | ${totalFiles} | ${(totalSize / 1024).toFixed(2)} |`
);
// 输出各个项目的详细数据
for (const [project, data] of Object.entries(projects)) {
report.push(
`| ${authorName || '未知'} | ${authorEmail} | ${project} | ${data.commits} | ${data.additions} | ${data.deletions} | ${data.lines} | ${data.files} | ${(data.size / 1024).toFixed(2)} |`
);
}
// 添加分隔线
report.push('|--------|------|------|----------|----------|----------|----------|---------|------------|');
}
// 添加新的提交信息表格
report.push('\n## 按作者统计提交信息\n');
report.push('| 作者 | 邮箱 | 项目 | 分支名 | 标签 | 提交时间 | 提交信息 |');
report.push('|--------|------|------|--------|------|----------|------------|');
for (const authorData of sortedAuthors) {
const { authorEmail, authorName, commitDetails } = authorData;
if (commitDetails && commitDetails.length > 0) {
commitDetails.forEach(detail => {
const sanitizedMessage = detail.message
.replace(/\|/g, '\\|')
.replace(/\n/g, ' ');
// 格式化时间
const datetime = new Date(detail.committed_date);
const formattedDate = datetime.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-');
report.push(
`| ${authorName || '未知'} | ${authorEmail} | ${detail.project} | ${detail.branch} | ${detail.tag} | ${formattedDate} | ${sanitizedMessage} |`
);
});
}
}
// 多次重试之后仍然获取失败,添加失败统计部分
if (failureStats.projects.length > 0 || failureStats.commits.length > 0 || failureStats.diffs.length > 0) {
report.push('\n## 统计失败记录\n');
if (failureStats.projects.length > 0) {
report.push('### 项目列表获取失败');
report.push('| 操作 | URL | 错误信息 |');
report.push('|------|-----|------------|');
failureStats.projects.forEach(failure => {
report.push(`| ${failure.operation} | ${failure.url} | ${failure.error} |`);
});
}
if (failureStats.commits.length > 0) {
report.push('\n### 提交记录获取失败');
report.push('| 项目 | 操作 | URL | 错误信息 |');
report.push('|------|------|-----|------------|');
failureStats.commits.forEach(failure => {
report.push(`| ${failure.projectName} | ${failure.operation} | ${failure.url} | ${failure.error} |`);
});
}
if (failureStats.diffs.length > 0) {
report.push('\n### 提交差异获取失败');
report.push('| 项目 | 作者 | 操作 | URL | 错误信息 |');
report.push('|------|------------|------|-----|------------|');
failureStats.diffs.forEach(failure => {
report.push(`| ${failure.projectName} | ${failure.authorEmail} | ${failure.operation} | ${failure.url} | ${failure.error} |`);
});
}
if (failureStats.refs.length > 0) {
report.push('\n### 提交分支信息获取失败');
report.push('| 项目 | 作者 | 操作 | URL | 错误信息 |');
report.push('|------|------------|------|-----|------------|');
failureStats.refs.forEach(failure => {
report.push(`| ${failure.projectName} | ${failure.authorEmail} | ${failure.operation} | ${failure.url} | ${failure.error} |`);
});
}
}
await fs.writeFile('gitlab-stats.md', report.join('\n'));
console.log('报告已生成: gitlab-stats.md');
}
// 添加正则表达式常量
const MERGE_BRANCH_RE = /Merge branch '([^']+)'/;
// 主函数
async function analyzeGitLabProjects() {
const startTime = Date.now();
console.log('开始分析 GitLab 仓库...');
try {
const projects = await getGroupProjects();
const filteredProjects = projects.filter(project =>
!config.EXCLUDED_PROJECTS.includes(project.name)
);
console.log(`排除 ${projects.length - filteredProjects.length} 个项目,实际分析 ${filteredProjects.length} 个项目`);
const authorStats = {};
// 使用分批处理的方式控制并发
for (let i = 0; i < filteredProjects.length; i += config.MAX_CONCURRENT_REQUESTS) {
const projectBatch = filteredProjects.slice(i, i + config.MAX_CONCURRENT_REQUESTS);
console.log(`处理项目批次 ${i / config.MAX_CONCURRENT_REQUESTS + 1}, 包含 ${projectBatch.length} 个项目`);
const projectPromises = projectBatch.map(async (project) => {
const commits = await getProjectCommitStats(
project.id,
null,
config.START_DATE,
config.END_DATE,
project.name
);
// 对每个项目的提交也使用分批处理
for (let j = 0; j < commits.length; j += config.MAX_CONCURRENT_REQUESTS) {
const commitBatch = commits.slice(j, j + config.MAX_CONCURRENT_REQUESTS);
const commitPromises = commitBatch.map(async (commit) => {
const authorEmail = commit.author_email;
const authorName = commit.author_name;
if (!authorStats[authorName]) {
authorStats[authorName] = {
authorName: authorName,
authorEmail: authorEmail,
projects: {},
totalCommits: 0,
totalAdditions: 0,
totalDeletions: 0,
totalLines: 0,
totalFiles: 0,
totalSize: 0,
commitDetails: []
};
}
if (!authorStats[authorName].projects[project.name]) {
authorStats[authorName].projects[project.name] = {
commits: 0,
additions: 0,
deletions: 0,
lines: 0,
files: 0,
size: 0
};
}
const [stats, branchInfo] = await Promise.all([
analyzeCommitDiffs(project.id, project.name, commit.id, authorEmail),
getCommitBranches(project.id, commit.id, project.name, authorEmail)
]);
const projectStats = authorStats[authorName].projects[project.name];
projectStats.commits++;
projectStats.additions += stats.additions;
projectStats.deletions += stats.deletions;
projectStats.lines += stats.lines;
projectStats.files += stats.files;
projectStats.size += stats.size;
authorStats[authorName].totalCommits++;
authorStats[authorName].totalAdditions += stats.additions;
authorStats[authorName].totalDeletions += stats.deletions;
authorStats[authorName].totalLines += stats.lines;
authorStats[authorName].totalFiles += stats.files;
authorStats[authorName].totalSize += stats.size;
// 如果是合并提交,从提交信息中提取分支名
if (commit.message.startsWith("Merge branch")) {
const matches = commit.message.match(MERGE_BRANCH_RE);
if (matches && matches[1]) {
branchInfo.branches = matches[1];
}
}
authorStats[authorName].commitDetails.push({
project: project.name,
branch: branchInfo.branches,
tag: branchInfo.tags,
message: commit.message,
committed_date: commit.committed_date
});
return { stats, branchInfo };
});
await Promise.allSettled(commitPromises);
}
});
await Promise.allSettled(projectPromises);
}
await generateReport(authorStats);
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(`\n分析完成! 总耗时: ${duration.toFixed(2)}秒`);
} catch (error) {
console.error('分析失败:', error.message);
process.exit(1);
}
}
analyzeGitLabProjects();
以上js代码可以直接在node环境中运行,结果也是完全符合我们的预期,但是有个问题,慢!
js 是很方便,但是性能太差,如果需要统计的仓库很多,那么会需要很长时间,所以索性我们将它改为Rust版本的试试,整体功能保持和js版本一致。
Rust 版本的代码可以看另一篇文章 用 Rust 开发了 GitLab 代码统计分析工具
经过多次测试,rust版本平均比js快5倍以上,当然此处的性能提升只是在处理循环遍历和并发上,对于接口本身的耗时肯定是没法提升的,即使这样,我们也看到了rust相较于js之间巨大的性能差异。
分析太慢的问题我们解决了,但是还有个问题,脚本毕竟用起来不太方便,运行需要有依赖的环境,查看起来也不直观,如果可以生成一个web页面,直接在浏览器中查看,那就完美了。
js版本的可以在浏览器中用,但是慢,rust快,但是不能在浏览器中使用。
那么怎么才能在浏览器中使用又能保证性能呢?答案是 webassembly
。
Rust 开发的 webassembly 版本的 GitLab 代码统计分析工具
使用 wasm-pack 将 Rust 代码编译为 webassembly 版本,发布npm包,然后在React等前端项目中使用。
具体项目实现可以参考 gitlab-analysis-wasm
实际上,webassembly版本的肯定是没有原生Rust版本快的,因为会有一些rust和js之间的转换和粘合,但是性能差距不会太大。
功能特点
- 🚀 基于Rust开发,使用 WebAssembly 实现高性能分析
- 📊 统计代码提交数据(新增、删除、修改行数等)
- 👥 按作者统计项目贡献
- 📈 生成详细的代码统计报告
- 🔄 支持并发请求和自动重试机制
- ⚡ 支持分组代码仓库分析
安装
npm install @gogors/gitlab-analysis-wasm
使用方法
1. 基本配置
const config = {
// GitLab API 配置
gitlab_api: 'http://gitlab.xxx.cn/api/v4',
gitlab_token: "your-gitlab-token",
group_id: 'your-group-id',
// 时间范围配置
start_date: '2024-11-01',
end_date: '2024-12-31',
// 项目配置
projects_num: 100,
excluded_projects: ['project1', 'project2'],
// 文件类型配置,如前端常用配置
valid_extensions: [
'.js', '.cjs', '.ts', '.jsx', '.tsx',
'.css', '.scss', '.sass', '.html',
'.sh', '.vue', '.svelte'
],
// 并发处理数
max_concurrent_requests: 30,
// 过滤配置
ignored_paths: [
"dist", "node_modules/", "build/",
".husky", "lintrc", "public/"
]
};
2. 使用
在构建工具中使用
在 vite 中使用,需要使用 vite-plugin-wasm 插件
npm install vite-plugin-wasm
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
plugins: [
wasm(),
topLevelAwait()
]
});
在webpack中使用,需要使用 @wasm-tool/wasm-pack-plugin 插件
npm install @wasm-tool/wasm-pack-plugin
import wasmPackPlugin from '@wasm-tool/wasm-pack-plugin';
export default defineConfig({
plugins: [wasmPackPlugin()],
});
在rollup中使用,需要使用 rollup-plugin-esmwasm 插件
npm install rollup-plugin-wasm
import wasm from 'rollup-plugin-esmwasm';
export default {
plugins: [wasm()],
};
在构建工具中使用时,引入 bundler 模式的包,可直接导入使用,无需手动初始化
// 可直接导入使用,无需手动初始化
import { analyze_gitlab_projects } from '@gogors/gitlab-analysis-wasm';
async function startAnalysis() {
try {
// 开始分析
const result = await analyze_gitlab_projects(config);
console.log(result);
} catch (error) {
console.error('分析失败:', error);
}
}
在浏览器中使用
导入后需要先初始化再使用
es 模块
<script type="module">
import init, { analyze_gitlab_projects } from 'https://unpkg.com/@gogors/gitlab-analysis-wasm/pkg/web/gitlab_analysis_wasm.js';
init().then(() => {
try {
// 开始分析
const result = await analyze_gitlab_projects(config);
console.log(result);
} catch (error) {
console.error('分析失败:', error);
}
});
</script>
非模块
考虑不兼容esm的浏览器,使用非模块
wasm_bindgen 为wasm-bindgen库的初始化函数,在no-modules模式下,需要手动初始化wasm
<script src="https://unpkg.com/@gogors/gitlab-analysis-wasm/pkg/no-modules/gitlab_analysis_wasm.js"></script>
<script>
(async () => {
// 初始化wasm
await wasm_bindgen();
// 获取分析函数
const { analyze_gitlab_projects } = wasm_bindgen;
try {
// 开始分析
const result = await analyze_gitlab_projects(config);
console.log(result);
} catch (error) {
console.error('分析失败:', error);
}
})();
</script>
unpkg 的使用参照 unpkg.com/
返回数据结构
分析完成后会返回包含以下信息的报告:
1. 代码统计 (codeStats)
interface CodeStat {
key: string; // 统计项唯一标识
author: string; // 作者名称
email: string; // 作者邮箱
project: string; // 项目名称
commits: number; // 提交次数
additions: number; // 新增行数
deletions: number; // 删除行数
lines: number; // 总行数变更
files: number; // 影响文件数
size: number; // 代码体积(KB)
isTotal?: boolean; // 是否为总计数据
children?: CodeStat[]; // 子统计项
}
2. 提交统计 (commitStats)
interface CommitStat {
author: string; // 作者名称
email: string; // 作者邮箱
project: string; // 项目名称
branch: string; // 分支名称
tag: string; // 标签名称
committedDate: string; // 提交时间
message: string; // 提交信息
}
3. 错误统计 (failureStats)
interface FailureRecord {
url: string; // 失败的请求URL
projectName?: string; // 相关项目名称
author?: string; // 相关作者
operation: string; // 操作类型
error: string; // 错误信息
}
注意事项
-
GitLab Token 权限要求:
- 需要
read_api
权限 - 需要
read_repository
权限
- 需要
-
性能优化建议:
- 根据接口qps适当调整
max_concurrent_requests
值 - 使用
excluded_projects
排除不需要分析的项目 - 使用
valid_extensions
过滤不需要分析的文件类型
- 根据接口qps适当调整
-
错误处理:
- 内置自动重试机制
- 重试多次依旧失败的请求会记录在
failureStats
中 - 可在控制台中查看错误详情
开发指南
# 安装依赖
npm install
# 构建 WASM
npm run build
# 启动示例
npm run test:html
效果展示
结合前端框架可以搭建自己的分析页面
分析提交代码
分析提交记录
该项目代码参照:react 版本gitlib可视化分析项目
注意事项
- 不支持nodejs
总结
以上三个版本的工具,大家根据自己的需求灵活选用。如果还有其他需求,欢迎大家提issue,一起交流,好用记得给星星~