git_statistics/
stats.rs

1use std::path::Path;
2use std::process::Command;
3#[cfg(target_os = "windows")]
4use std::os::windows::process::CommandExt;
5use itertools::Itertools;
6use serde::Serialize;
7
8// List of all process creation flags:
9// https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
10#[allow(dead_code)]
11const CREATE_NO_WINDOW: u32 = 0x08000000;
12#[derive(Serialize)]
13pub struct FilesChanged {
14    pub insertions: usize,
15    pub deletions: usize,
16    pub filename: String,
17}
18
19#[derive(Serialize)]
20pub struct Commit {
21    pub author: String,
22    // pub files_changed_list: Vec<FilesChanged>,
23    pub insertions: usize,
24    pub deletions: usize,
25    pub files_changed: usize,
26}
27
28pub fn get_commits<P: AsRef<Path>>(
29    repo: P,
30    pathspec: Option<&Vec<String>>,
31) -> Result<Vec<Commit>, String> {
32    let mut cmd = Command::new("git");
33    #[cfg(target_os = "windows")]
34    cmd.creation_flags(CREATE_NO_WINDOW);
35    cmd.current_dir(repo)
36        .arg("log")
37        .arg("--no-merges")
38        .arg("--format=author %aN")
39        .arg("--numstat")
40        .arg("--");
41    if let Some(pathspec) = pathspec {
42        cmd.args(pathspec);
43    }
44
45    // println!("cmd: {:?}", cmd);
46    let output = cmd.output().expect("failed to execute process");
47
48    if !output.status.success() {
49        return Err(String::from_utf8(output.stderr).unwrap());
50    }
51
52    Ok(parse_git_log(&String::from_utf8(output.stdout).unwrap()))
53}
54
55fn parse_git_log(log: &String) -> Vec<Commit> {
56    let mut commits = vec![];
57
58    for line in log.lines() {
59        if line.is_empty() {
60            continue;
61        }
62
63        if line.starts_with("author ") {
64            let author = line.trim_start_matches("author ");
65            commits.push(Commit {
66                author: String::from(author),
67                insertions: 0,
68                deletions: 0,
69                files_changed: 0,
70                // files_changed_list: vec![],
71            });
72        } else {
73            let commit = commits.last_mut().unwrap();
74            let (insertions, deletions, _file) = line.splitn(3, '\t').collect_tuple().unwrap();
75            let insertions = insertions.parse::<usize>().unwrap_or(0);
76            let deletions = deletions.parse::<usize>().unwrap_or(0);
77            commit.insertions += insertions;
78            commit.deletions += deletions;
79            commit.files_changed += 1;
80            // commit.files_changed_list.push(FilesChanged {
81            //     insertions,
82            //     deletions,
83            //     filename: String::from(file),
84            // });
85        }
86    }
87
88    commits
89}