用Rust写了一个GitLib代码分析工具

前言

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

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

最初,我用js实现了一版,可以直接在node中使用,是很方便,但是性能太差,如果需要统计的仓库很多,那么会需要很长时间,所以索性我们将它改为Rust版本的试试,整体功能保持和js版本一致。

需求开发

功能描述

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

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

作者 邮箱 项目 提交次数 增加行数 删除行数 变更行数 文件数 代码量(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个,可以分页获取,按最后活跃时间排序
bash 复制代码
GET /groups/:id/projects?per_page=100&include_subgroups=true&order_by=last_activity_at&sort=desc
  • 获取该组下所有项目在指定时间范围内的提交信息,默认返回100个,可以分页获取,按提交时间排序,获取所有分支(因为此时有些开发分支还未合并),并带上统计信息
bash 复制代码
GET /projects/:id/repository/commits?since=2024-12-01&until=2024-12-31&per_page=100&page=1&all=true&with_stats=true
  • 获取某个提交的变更信息,统计新增、删除、修改行数,文件数
bash 复制代码
GET /projects/:id/repository/commits/:sha/diff
  • 获取提交对应的分支信息,拿到分支名、标签名
bash 复制代码
GET /projects/:id/repository/commits/:sha/refs

其他api的用法可以参考 gitlab.cn/docs/jh/api...

准备好这些api,还不着急开发,还有一些点需要考虑:

  • 并发请求控制

项目几十个,提交信息几百条,一个一个请求分析那得搞到猴年马月,所以需要并发请求。

  • 自动重试机制

但是并发请求如果服务器qps不高的话很容易超时,又会导致请求失败,那么统计的结果可能就是不准确的,所以要进行多次重试。多次重试仍旧失败的,则记录下来,后续人工处理。

  • 生成 Markdown 格式报告

再加一个错误报告

项目 作者 操作 URL 错误信息
  • 过滤掉一些不关心的项目
  • 支持自定义文件类型过滤

例如有些文件如 package.json 等,统计意义不大。

  • 支持忽略特定路径文件

例如有些文件夹如 dist 等,完全没必要统计。

用Rust开发

创建一个Rust项目:

  • 新建文件夹 gitlab-analysis
  • 在文件夹中执行 cargo init
  • Cargo.toml 中添加依赖
  • src/main.rs 中编写主入口
  • src/gitlab.rs 中编写gitlab api 请求
  • src/models.rs 中编写配置和模型
  • src/report.rs 中编写报告生成逻辑
toml 复制代码
# Cargo.toml
[package]
name = "gitlab_analysis"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.36", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
async-trait = "0.1"
futures = "0.3"
regex = "1.5"
lazy_static = "1.4"
rust 复制代码
// src/main.rs
mod gitlab;
mod models;
mod report;

use anyhow::Result;
use gitlab::GitLabClient;
use models::Config;

#[tokio::main]
async fn main() -> Result<()> {
    let start_time = std::time::Instant::now();

    let config = Config::default();
    let client = GitLabClient::new(config);

    println!("开始分析 GitLab 仓库...");
    client.analyze_gitlab_projects().await?;

    let duration = start_time.elapsed();
    println!("\n分析完成! 总耗时: {:.2}秒", duration.as_secs_f64());

    Ok(())
}
rust 复制代码
// src/gitlab.rs
use crate::models::*;
use crate::report::generate_report;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::Semaphore;

#[derive(Clone)]
pub struct GitLabClient {
    client: reqwest::Client,
    config: Config,
    failure_stats: Arc<Mutex<FailureStats>>,
}

// 添加预编译的正则表达式
lazy_static::lazy_static! {
    static ref MERGE_BRANCH_RE: regex::Regex = regex::Regex::new(r"Merge branch '([^']+)'").unwrap();
}

impl GitLabClient {
    pub fn new(config: Config) -> Self {
        let client = reqwest::Client::builder()
            .connect_timeout(std::time::Duration::from_secs(2))
            .timeout(std::time::Duration::from_secs(3))
            .build()
            .expect("Failed to create HTTP client");

        Self {
            client,
            config,
            failure_stats: Arc::new(Mutex::new(FailureStats::default())),
        }
    }

    async fn record_failure(&self, context: &RequestContext, url: &str, error: &str) {
        let mut stats = self.failure_stats.lock().await;
        let record = FailureRecord {
            url: url.to_string(),
            operation: context.operation.clone(),
            project_name: context.project_name.clone(),
            author_email: context.author_email.clone(),
            error: error.to_string(),
        };

        match context.operation.as_str() {
            "获取项目列表" => stats.projects.push(record),
            "获取提交记录" => stats.commits.push(record),
            "获取提交差异" => stats.diffs.push(record),
            _ => {}
        }
    }

    async fn fetch_with_retry<T>(&self, url: &str, context: RequestContext) -> Result<T>
    where
        T: for<'de> serde::de::Deserialize<'de>,
    {
        let mut attempts = 0;
        let max_attempts = 30;

        loop {
            attempts += 1;
            println!(
                "\n[{}请求开始] 项目: {}, 第 {} 次尝试",
                context.operation,
                context.project_name.clone().unwrap_or_default(),
                attempts
            );
            println!("URL: {}", url);

            let start = std::time::Instant::now();

            match self
                .client
                .get(url)
                .header("Private-Token", self.config.gitlab_token.clone())
                // .header("Cookie", self.config.gitlab_cookie.clone())
                .header("Content-Type", "application/json")
                .send()
                .await
            {
                Ok(response) => {
                    // let duration = start.elapsed();
                    // println!("[请求完成] 耗时: {}ms", duration.as_millis());

                    if response.status().is_success() {
                        match response.json::<T>().await {
                            Ok(data) => return Ok(data),
                            Err(e) => {
                                println!("解析响应失败: {}", e);
                                if attempts == max_attempts {
                                    self.record_failure(&context, url, &e.to_string()).await;
                                    return Err(e.into());
                                }
                            }
                        }
                    } else {
                        println!("HTTP 错误: {}", response.status());
                        if attempts == max_attempts {
                            self.record_failure(
                                &context,
                                url,
                                &format!("HTTP {}", response.status()),
                            )
                            .await;
                            return Err(anyhow::anyhow!("HTTP error: {}", response.status()));
                        }
                    }
                }
                Err(e) => {
                    let duration = start.elapsed();
                    println!(
                        "[请求失败] 第 {} 次尝试,耗时: {}ms",
                        attempts,
                        duration.as_millis()
                    );
                    println!("请求失败: {}", e);
                    if attempts == max_attempts {
                        self.record_failure(&context, url, &e.to_string()).await;
                        return Err(e.into());
                    }
                }
            }

            println!("立即开始第 {} 次重试...", attempts + 1);
        }
    }

    async fn get_group_projects(&self) -> Result<Vec<Project>> {
        let url = format!(
            "{}/groups/{}/projects?per_page={}&include_subgroups=true&order_by=last_activity_at&sort=desc",
            self.config.gitlab_api, self.config.group_id, self.config.projects_num
        );

        let context = RequestContext {
            operation: "获取项目列表".to_string(),
            project_name: None,
            author_email: None,
        };

        let projects: Vec<Project> = self.fetch_with_retry(&url, context).await?;
        println!("[获取成功] 找到 {} 个项目", projects.len());
        Ok(projects)
    }

    async fn get_project_commits(
        &self,
        project_id: i64,
        project_name: &str,
    ) -> Result<Vec<Commit>> {
        let mut all_commits = Vec::new();
        let mut page = 1;

        loop {
            let url = format!(
                "{}/projects/{}/repository/commits?since={}&until={}&per_page=100&page={}&all=true",
                self.config.gitlab_api,
                project_id,
                self.config.start_date,
                self.config.end_date,
                page
            );

            let context = RequestContext {
                operation: "获取提交记录".to_string(),
                project_name: Some(project_name.to_string()),
                author_email: None,
            };

            let commits: Vec<Commit> = self.fetch_with_retry(&url, context).await?;
            let commits_len = commits.len();
            all_commits.extend(commits);

            if commits_len < 100 {
                break;
            }

            page += 1;
            println!(
                "正在获取第 {} 页的提交记录...,本页 {} 条",
                page, commits_len
            );
        }

        println!(
            "找到 projectId: {} 的 {} 个提交",
            project_id,
            all_commits.len()
        );
        Ok(all_commits)
    }

    async fn analyze_commit_diffs(
        &self,
        project_id: i64,
        sha: &str,
        project_name: &str,
        author_email: &str,
    ) -> Result<ProjectStats> {
        let url = format!(
            "{}/projects/{}/repository/commits/{}/diff",
            self.config.gitlab_api, project_id, sha
        );

        let context = RequestContext {
            operation: "获取提交差异".to_string(),
            project_name: Some(project_name.to_string()),
            author_email: Some(author_email.to_string()),
        };

        let diffs: Vec<Diff> = self.fetch_with_retry(&url, context).await?;
        let mut stats = ProjectStats::default();

        for diff in diffs {
            let file_path = diff.new_path.or(diff.old_path).unwrap_or_default();

            if self
                .config
                .ignored_paths
                .iter()
                .any(|path| file_path.contains(path))
            {
                continue;
            }

            let ext = file_path
                .split('.')
                .last()
                .map(|s| format!(".{}", s))
                .unwrap_or_default();

            if !self.config.valid_extensions.contains(&ext) {
                continue;
            }

            stats.files += 1;

            if let Some(diff_content) = diff.diff {
                let lines = diff_content.lines();
                for line in lines {
                    if line.starts_with('+') && !line.starts_with("+++") {
                        stats.lines += 1;
                        stats.added_lines += 1;
                    } else if line.starts_with('-') && !line.starts_with("---") {
                        stats.lines += 1;
                        stats.deleted_lines += 1;
                    }
                }
                stats.size += diff_content.len() as i64;
            }
        }

        Ok(stats)
    }

    async fn get_commit_branches(
        &self,
        project_id: i64,
        commit_sha: &str,
        project_name: &str,
        author_email: &str,
    ) -> Result<(String, String)> {
        let url = format!(
            "{}/projects/{}/repository/commits/{}/refs",
            self.config.gitlab_api, project_id, commit_sha
        );

        let context = RequestContext {
            operation: "获取提交对应的分支信息".to_string(),
            project_name: Some(project_name.to_string()),
            author_email: Some(author_email.to_string()),
        };

        let refs: Vec<Ref> = self.fetch_with_retry(&url, context).await?;

        let branch = refs
            .iter()
            .find(|r| r.ref_type == "branch")
            .map(|r| r.name.clone())
            .unwrap_or_else(|| "unknown".to_string());

        let tag = refs
            .iter()
            .find(|r| r.ref_type == "tag")
            .map(|r| r.name.clone())
            .unwrap_or_else(|| "unknown".to_string());

        Ok((branch, tag))
    }

    pub async fn analyze_gitlab_projects(&self) -> Result<()> {
        let mut projects = self.get_group_projects().await?;
        projects.retain(|p| !self.config.excluded_projects.contains(&p.name));

        println!(
            "排除 {} 个项目,实际分析 {} 个项目",
            self.config.excluded_projects.len(),
            projects.len()
        );

        let author_stats = Arc::new(Mutex::new(HashMap::new()));

        // 创建信号量来限制并发,使用配置的并发量
        let semaphore = Arc::new(Semaphore::new(self.config.concurrency as usize));

        // 并发处理项目
        let project_futures: Vec<_> = projects
            .into_iter()
            .map(|project| {
                let author_stats = Arc::clone(&author_stats);
                let self_clone = self.clone();
                let sem = Arc::clone(&semaphore);

                async move {
                    // 获取信号量许可
                    let _permit = sem.acquire().await.unwrap();

                    println!("-----------正在分析项目------------: {}", project.name);

                    if let Ok(commits) = self_clone
                        .get_project_commits(project.id, &project.name)
                        .await
                    {
                        // 创建信号量来限制并发,使用配置的并发量
                        let commit_semaphore = Arc::new(Semaphore::new(self.config.concurrency as usize));

                        // 并发处理每个项目的提交
                        let commit_futures: Vec<_> = commits
                            .into_iter()
                            .map(|commit: Commit| {
                                let author_stats = Arc::clone(&author_stats);
                                let self_clone = self_clone.clone();
                                let project_name = project.name.clone();
                                let commit_sem = Arc::clone(&commit_semaphore);

                                async move {
                                    // 获取提交级别的信号量许可
                                    let _commit_permit = commit_sem.acquire().await.unwrap();

                                    let author_email = commit.author_email.clone();

                                    // 并发获取提交差异和分支信息
                                    let (diff_stats, branch_info) = tokio::join!(
                                        self_clone.analyze_commit_diffs(
                                            project.id,
                                            &commit.id,
                                            &project_name,
                                            &author_email
                                        ),
                                        self_clone.get_commit_branches(
                                            project.id,
                                            &commit.id,
                                            &project_name,
                                            &author_email
                                        )
                                    );

                                    if let (Ok(stats), Ok((branch, tag))) =
                                        (diff_stats, branch_info)
                                    {
                                        let mut author_stats = author_stats.lock().await;
                                        let author_stat = author_stats
                                            .entry(commit.author_name.clone())
                                            .or_insert_with(|| AuthorStats {
                                                author_name: commit.author_name.clone(),
                                                author_email: author_email.clone(),
                                                ..Default::default()
                                            });
                                        let project_stat = author_stat
                                            .projects
                                            .entry(project_name.clone())
                                            .or_default();

                                        // 更新项目统计
                                        project_stat.commits += 1;
                                        project_stat.lines += stats.lines;
                                        project_stat.added_lines += stats.added_lines;
                                        project_stat.deleted_lines += stats.deleted_lines;
                                        project_stat.files += stats.files;
                                        project_stat.size += stats.size;

                                        // 更新作者总计
                                        author_stat.total_commits += 1;
                                        author_stat.total_lines += stats.lines;
                                        author_stat.total_added_lines += stats.added_lines;
                                        author_stat.total_deleted_lines += stats.deleted_lines;
                                        author_stat.total_files += stats.files;
                                        author_stat.total_size += stats.size;

                                        let mut branch_name = branch;
                                        if commit.message.starts_with("Merge branch") {
                                            if let Some(captures) =
                                                MERGE_BRANCH_RE.captures(&commit.message)
                                            {
                                                if let Some(matched_branch) = captures.get(1) {
                                                    branch_name =
                                                        matched_branch.as_str().to_string();
                                                }
                                            }
                                        }

                                        author_stat.commit_details.push(CommitDetail {
                                            project: project_name,
                                            branch: branch_name,
                                            tag,
                                            message: commit.message,
                                            committed_date: commit.committed_date,
                                        });
                                    }
                                }
                            })
                            .collect();

                        // 等待所有提交处理完成
                        futures::future::join_all(commit_futures).await;
                    }
                }
            })
            .collect();

        // 等待所有项目处理完成
        futures::future::join_all(project_futures).await;

        let failure_stats = self.failure_stats.lock().await.clone();

        // 获取最终的统计数据
        let author_stats_map = {
            let lock = author_stats.lock().await;
            lock.clone() // 克隆 HashMap 的内容
        };

        generate_report(&author_stats_map, &self.config, &failure_stats).await?;

        Ok(())
    }
}
rust 复制代码
// src/models.rs
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct Config {
    pub gitlab_api: String,
    // pub gitlab_cookie: String,
    pub gitlab_token: String,
    pub group_id: String,
    pub start_date: String,
    pub end_date: String,
    pub excluded_projects: Vec<String>,
    pub projects_num: i32,
    pub valid_extensions: Vec<String>, // 支持的文件扩展名配置
    pub ignored_paths: Vec<String>,
    pub concurrency: i32,  // 并发量配置
}

impl Default for Config {
    fn default() -> Self {
        Self {
            gitlab_api: "http://gitlab.xxx/api/v4".to_string(),
            // gitlab_cookie: "xxx".to_string(), // 如果需要使用cookie,请在这里填写,cookie token 二选一
            gitlab_token: "xxx".to_string(),
            group_id: "2177".to_string(),
            start_date: "2024-12-01".to_string(),
            end_date: "2025-01-31".to_string(),
            excluded_projects: vec!["project1".to_string(), "project2".to_string()],
            projects_num: 10, // 获取该组下的项目数量最大100
            valid_extensions: vec![
              ".js".to_string(),
              ".cjs".to_string(),
              ".mjs".to_string(),
              ".ts".to_string(),
              ".jsx".to_string(),
              ".tsx".to_string(),
              ".css".to_string(),
              ".scss".to_string(),
              ".sass".to_string(),
              ".html".to_string(),
              ".sh".to_string(),
              ".vue".to_string(),
              ".svelte".to_string(),
              ".rs".to_string(),
          ],
          ignored_paths: vec![
              "dist".to_string(),
              "node_modules/".to_string(),
              "build/".to_string(),
              ".husky".to_string(),
              "lintrc".to_string(),
              "public/".to_string(),
          ],
          concurrency: 20,  // 默认并发量
        }
    }
}

#[derive(Debug, Default, Clone)]
pub struct FailureStats {
    pub projects: Vec<FailureRecord>,
    pub commits: Vec<FailureRecord>,
    pub diffs: Vec<FailureRecord>,
    pub refs: Vec<FailureRecord>,
}

#[derive(Debug, Clone)]
pub struct FailureRecord {
    pub url: String,
    pub operation: String,
    pub project_name: Option<String>,
    pub error: String,
    pub author_email: Option<String>,
}

#[derive(Debug, Default, Clone)]
pub struct ProjectStats {
    pub commits: i32,
    pub lines: i32,
    pub added_lines: i32,
    pub deleted_lines: i32,
    pub files: i32,
    pub size: i64,
}

#[derive(Debug, Default, Clone)]
pub struct AuthorStats {
    pub author_name: String,
    pub author_email: String,
    pub projects: HashMap<String, ProjectStats>,
    pub total_commits: i32,
    pub total_lines: i32,
    pub total_added_lines: i32,
    pub total_deleted_lines: i32,
    pub total_files: i32,
    pub total_size: i64,
    pub commit_details: Vec<CommitDetail>,
}

#[derive(Debug, Deserialize)]
pub struct Project {
    pub id: i64,
    pub name: String,
}

#[derive(Debug, Deserialize)]
pub struct Commit {
    pub id: String,
    pub author_email: String,
    pub author_name: String,
    pub message: String,
    pub committed_date: String
}

#[derive(Debug, Deserialize)]
pub struct Diff {
    pub old_path: Option<String>,
    pub new_path: Option<String>,
    pub diff: Option<String>,
}

#[derive(Debug)]
pub struct RequestContext {
    pub operation: String,
    pub project_name: Option<String>,
    pub author_email: Option<String>,
}

#[derive(Debug, Clone)]
pub struct CommitDetail {
    pub project: String,
    pub branch: String,
    pub tag: String,
    pub message: String,
    pub committed_date: String,
}

#[derive(Debug, Deserialize)]
pub struct Ref {
    pub name: String,
    #[serde(rename = "type")]
    pub ref_type: String,
}
rust 复制代码
// src/report.rs
use crate::models::*;
use anyhow::Result;
use std::collections::HashMap;
use tokio::fs;
use chrono::DateTime;

pub async fn generate_report(
    author_stats: &HashMap<String, AuthorStats>,
    config: &Config,
    failure_stats: &FailureStats,
) -> Result<()> {
    let mut sorted_authors: Vec<_> = author_stats.iter().collect();
    sorted_authors.sort_by_key(|(_, stats)| -stats.total_size);

    let mut report = vec![
        "# GitLab 代码提交统计报告\n".to_string(),
        format!("统计期间: {} 至 {}\n", config.start_date, config.end_date),
        "## 按作者统计代码信息\n".to_string(),
        "| 作者 | 邮箱 | 项目 | 提交次数 | 变更行数 | 增加行数 | 删除行数 | 文件数 | 代码量(KB) |".to_string(),
        "|--------|------|------|----------|----------|----------|----------|---------|------------|".to_string(),
    ];

    // 先生成代码统计信息表格
    for (_, stats) in &sorted_authors {
        report.push(format!(
            "| 【{}】 | {} | 【总计】 | {} | {} | {} | {} | {} | {:.2} |",
            stats.author_name,
            stats.author_email,
            stats.total_commits,
            stats.total_lines,
            stats.total_added_lines,
            stats.total_deleted_lines,
            stats.total_files,
            stats.total_size as f64 / 1024.0
        ));

        for (project, data) in &stats.projects {
            report.push(format!(
                "| {} | {} | {} | {} | {} | {} | {} | {} | {:.2} |",
                stats.author_name,
                stats.author_email,
                project,
                data.commits,
                data.lines,
                data.added_lines,
                data.deleted_lines,
                data.files,
                data.size as f64 / 1024.0
            ));
        }

        report.push("|--------|------|------|----------|----------|----------|----------|---------|------------|".to_string());
    }

    // 添加提交信息表格
    report.push("\n## 按作者统计提交信息\n".to_string());
    report.push("| 作者 | 邮箱 | 项目 | 分支名 | 标签 | 提交时间 | 提交信息 |".to_string());
    report.push("|--------|------|------|--------|------|----------|------------|".to_string());

    // 生成提交信息表格
    for (_, stats) in &sorted_authors {
        for detail in &stats.commit_details {
            let sanitized_message = detail.message
                .replace("|", "\\|")
                .replace("\n", " ");

            // 解析并格式化时间
            let datetime = DateTime::parse_from_rfc3339(&detail.committed_date)
                .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
                .unwrap_or_else(|_| detail.committed_date.clone());

            report.push(format!(
                "| {} | {} | {} | {} | {} | {} | {} |",
                stats.author_name,
                stats.author_email,
                detail.project,
                detail.branch,
                detail.tag,
                datetime,
                sanitized_message
            ));
        }
    }

    // 添加失败统计
    if !failure_stats.projects.is_empty() || !failure_stats.commits.is_empty() || !failure_stats.diffs.is_empty() {
        report.push("\n## 统计失败记录\n".to_string());

        if !failure_stats.projects.is_empty() {
            report.push("### 项目列表获取失败".to_string());
            report.push("| 操作 | URL | 错误信息 |".to_string());
            report.push("|------|-----|------------|".to_string());
            for failure in &failure_stats.projects {
                report.push(format!(
                    "| {} | {} | {} |",
                    failure.operation, failure.url, failure.error
                ));
            }
        }

        if !failure_stats.commits.is_empty() {
            report.push("\n### 提交记录获取失败".to_string());
            report.push("| 项目 | 操作 | URL | 错误信息 |".to_string());
            report.push("|------|------|-----|------------|".to_string());
            for failure in &failure_stats.commits {
                report.push(format!(
                    "| {} | {} | {} | {} |",
                    failure.project_name.as_deref().unwrap_or("-"),
                    failure.operation,
                    failure.url,
                    failure.error
                ));
            }
        }

        if !failure_stats.diffs.is_empty() {
            report.push("\n### 提交差异获取失败".to_string());
            report.push("| 项目 | 作者 | 操作 | URL | 错误信息 |".to_string());
            report.push("|------|------------|------|-----|------------|".to_string());
            for failure in &failure_stats.diffs {
                report.push(format!(
                    "| {} | {} | {} | {} | {} |",
                    failure.project_name.as_deref().unwrap_or("-"),
                    failure.author_email.as_deref().unwrap_or("-"),
                    failure.operation,
                    failure.url,
                    failure.error
                ));
            }
        }

        if !failure_stats.refs.is_empty() {
            report.push("\n### 提交分支信息获取失败".to_string());
            report.push("| 项目 | 作者 | 操作 | URL | 错误信息 |".to_string());
            report.push("|------|------------|------|-----|------------|".to_string());
            for failure in &failure_stats.refs {
                report.push(format!(
                    "| {} | {} | {} | {} | {} |",
                    failure.project_name.as_deref().unwrap_or("-"),
                    failure.author_email.as_deref().unwrap_or("-"),
                    failure.operation,
                    failure.url,
                    failure.error
                ));
            }
        }
    }

    fs::write("gitlab-stats.md", report.join("\n")).await?;
    println!("报告已生成: gitlab-stats.md");

    Ok(())
}

大功告成,经过多次测试,rust版本平均比js快5倍以上,当然此处的性能提升只是在处理循环遍历和并发上,对于接口本身的耗时肯定是没法提升的,即使这样,我们也看到了rust相较于js之间巨大的性能差异。

另外两个版本实现参考另一篇文章Rust + wasm-pack + WebAssembly 实现 Gitlab 代码统计,比JS快太多了

总结

以上是该工具的完整 Rust 代码,感兴趣的可以研究研究,自己尝试一下。

相关推荐
海上彼尚13 分钟前
npm、yarn、pnpm三者的异同
前端·npm·node.js
余生H14 分钟前
前端的 Python 入门指南(六):调试方式和技巧对比
开发语言·前端·javascript·python
m0_7482370517 分钟前
前端报错npm ERR cb() never called问题
前端·npm·node.js
818源码资源站29 分钟前
Ripro V5日主题 v8.3 开心授权版 wordpress主题虚拟资源下载站首选主题模板
前端
低代码布道师35 分钟前
第二篇:脚手架搭建 — React 和 Express 的搭建
前端·react.js·express
憨憨2号1 小时前
RUST学习笔记
笔记·学习·rust
m0_748238781 小时前
前端文件预览整合(一)
前端·状态模式
问道飞鱼1 小时前
【GIT知识】git进阶-hooks勾子脚本
git·hooks
程序员大金1 小时前
基于SpringBoot+Vue的高校电动车租赁系统
前端·javascript·vue.js·spring boot·mysql·intellij-idea·旅游
莫惊春1 小时前
HTML5 第七章
前端·html·html5