Rust + wasm-pack + WebAssembly 实现 Gitlab 代码统计,比JS快太多了

687 阅读12分钟

前言

gitlib 本身没有比较详细的代码贡献视图,如果我们想查看每一个开发人员在不同项目上的贡献情况,包括提交次数、删除数、增加数、代码总量等等指标,以及拉出来每次提交的具体信息来看看是否符合团队规范,目前存在的一些开源工具是完全无法实现的,相信很多小伙伴和我一样找了半天没找到想要的。

所以这种高度定制化的需求那就必须要自己写一个脚本去跑数据了。

需求开发

功能描述

那么我们要实现哪些功能呢?

首先,按作者统计代码提交量,统计不同开发人员在一个项目组内不同项目上的贡献量和总计,如下表格:

作者邮箱项目提交次数增加行数删除行数变更行数文件数代码量(KB)
zhangsanzhangsan@gmail.com【总计】399520190011420151352.17
zhangsanzhangsan@gmail.comA765981631010.79
zhangsanzhangsan@gmail.comB268953148010433118307.64
zhangsanzhangsan@gmail.comC65023228242333.74

按作者列出所有提交,可以查看具体提交信息,如下表格:

作者邮箱项目分支名标签提交时间提交信息
zhangsanzhangsan@gmail.comAdevv1.0.02024-12-04 10:12:44feat: xxx
zhangsanzhangsan@gmail.comBdevv1.0.12024-12-03 17:01:12chore: xxx
zhangsanzhangsan@gmail.comCdevv1.0.02024-12-03 18:06:50fix: 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 插件

参照:github.com/Menci/vite-…

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 插件

参照:github.com/wasm-tool/w…

npm install @wasm-tool/wasm-pack-plugin
import wasmPackPlugin from '@wasm-tool/wasm-pack-plugin';

export default defineConfig({
  plugins: [wasmPackPlugin()],
});

在rollup中使用,需要使用 rollup-plugin-esmwasm 插件

参照:github.com/Pursue-LLL/…

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;         // 错误信息
}

注意事项

  1. GitLab Token 权限要求:

    • 需要 read_api 权限
    • 需要 read_repository 权限
  2. 性能优化建议:

    • 根据接口qps适当调整 max_concurrent_requests
    • 使用 excluded_projects 排除不需要分析的项目
    • 使用 valid_extensions 过滤不需要分析的文件类型
  3. 错误处理:

    • 内置自动重试机制
    • 重试多次依旧失败的请求会记录在 failureStats
    • 可在控制台中查看错误详情

开发指南

# 安装依赖
npm install

# 构建 WASM
npm run build

# 启动示例
npm run test:html

效果展示

结合前端框架可以搭建自己的分析页面

分析提交代码 image.png

image-1.png

分析提交记录 image-2.png

该项目代码参照:react 版本gitlib可视化分析项目

注意事项

  • 不支持nodejs

总结

以上三个版本的工具,大家根据自己的需求灵活选用。如果还有其他需求,欢迎大家提issue,一起交流,好用记得给星星~