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#[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 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 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 });
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 }
86 }
87
88 commits
89}