用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 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead15 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409198 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app