Compare commits
21 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
1d97cbfa45 | |
|
|
8f4a8c18f9 | |
|
|
66e7a76956 | |
|
|
f04b6b2b2e | |
|
|
df4b44eba1 | |
|
|
38daf4ac4c | |
|
|
5f830049dd | |
|
|
a748f78a94 | |
|
|
e608cd9958 | |
|
|
fe02a4bf0d | |
|
|
437e3689d5 | |
|
|
728ad3ff26 | |
|
|
1d48f9ffc5 | |
|
|
21e0247d7b | |
|
|
54d36fafcb | |
|
|
b1a5df8659 | |
|
|
8084309af5 | |
|
|
055744285a | |
|
|
fd7e8c2e88 | |
|
|
de2b6190d5 | |
|
|
50b3f95b1d |
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
profile.json
|
||||
perf.data*
|
||||
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
|
|
@ -2,25 +2,46 @@ cargo-features = ["codegen-backend"]
|
|||
|
||||
[package]
|
||||
name = "git-heatmap"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
version = "1.4.1"
|
||||
edition = "2024"
|
||||
authors = ["Wynd <wyndftw@proton.me>"]
|
||||
description = "A simple and customizable heatmap for git repos"
|
||||
readme = "README.md"
|
||||
repository = "https://codeberg.org/WyndFTW/git-heatmap"
|
||||
license = "MIT"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "libgitheatmap"
|
||||
path = "src/lib.rs"
|
||||
bench = false
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = { level = "forbid" }
|
||||
|
||||
[dependencies]
|
||||
gix = { version = "0.66.0", default-features = false, features = ["mailmap"] }
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
chrono = { version = "0.4.38" }
|
||||
itertools = { version = "0.13.0" }
|
||||
anyhow = { version = "1.0.89" }
|
||||
gix = { version = "0.73", default-features = false, features = [
|
||||
"mailmap",
|
||||
"parallel",
|
||||
] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
chrono = { version = "0.4" }
|
||||
itertools = { version = "0.14" }
|
||||
anyhow = { version = "1.0" }
|
||||
rayon = { version = "1.11" }
|
||||
|
||||
[dev-dependencies]
|
||||
divan = { version = "0.1" }
|
||||
mockd = { version = "0.4", features = ["datetime", "words", "name", "contact"] }
|
||||
|
||||
[[bench]]
|
||||
name = "commits"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "heatmap"
|
||||
harness = false
|
||||
|
||||
[profile.dev]
|
||||
codegen-backend = "cranelift"
|
||||
|
|
|
|||
|
|
@ -84,5 +84,6 @@ $ git-heatmap -a "username" -a "other"
|
|||
$ git-heatmap --since "2013-08-23"
|
||||
|
||||
# or choose a time span, both --since and --until must use a YYYY-MM-DD format
|
||||
# --until can optionally use `today` as a keyword for the current date
|
||||
$ git-heatmap --since "2013-08-23" --until "2024-08-23"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
use anyhow::Context;
|
||||
use chrono::NaiveDate;
|
||||
use libgitheatmap::{self, cli::CliArgs};
|
||||
|
||||
fn main() {
|
||||
divan::main();
|
||||
}
|
||||
|
||||
#[divan::bench]
|
||||
fn current_repo_commits() {
|
||||
let home_dir = std::env::current_dir().unwrap();
|
||||
|
||||
let args = CliArgs {
|
||||
root_dir: Some(vec![home_dir]),
|
||||
authors: None,
|
||||
char: '▩',
|
||||
repos: None,
|
||||
ignored_repos: None,
|
||||
branches: None,
|
||||
since: "2024-01-01".to_string(),
|
||||
until: None,
|
||||
split_months: false,
|
||||
months_per_row: 13,
|
||||
no_merges: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let since = NaiveDate::parse_from_str(&args.since, "%Y-%m-%d").unwrap();
|
||||
|
||||
let until = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap();
|
||||
|
||||
libgitheatmap::get_commits(args, since, until)
|
||||
.with_context(|| "Could not fetch commit list")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[divan::bench(sample_count = 20)]
|
||||
fn all_repos_commits() {
|
||||
let home_dir = std::path::PathBuf::from("/home");
|
||||
|
||||
let args = CliArgs {
|
||||
root_dir: Some(vec![home_dir]),
|
||||
authors: None,
|
||||
char: '▩',
|
||||
repos: None,
|
||||
ignored_repos: None,
|
||||
branches: None,
|
||||
since: "2024-01-01".to_string(),
|
||||
until: None,
|
||||
split_months: false,
|
||||
months_per_row: 13,
|
||||
no_merges: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let since = NaiveDate::parse_from_str(&args.since, "%Y-%m-%d").unwrap();
|
||||
|
||||
let until = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap();
|
||||
|
||||
libgitheatmap::get_commits(args, since, until)
|
||||
.with_context(|| "Could not fetch commit list")
|
||||
.unwrap();
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use chrono::{Local, NaiveDate};
|
||||
use gix::ObjectId;
|
||||
use libgitheatmap::{
|
||||
Commit,
|
||||
heatmap::{self, Heatmap},
|
||||
};
|
||||
|
||||
static COMMITS: OnceLock<Vec<Commit>> = OnceLock::new();
|
||||
|
||||
fn main() {
|
||||
let mut commits: Vec<Commit> = vec![];
|
||||
for _n in 0..1000 {
|
||||
let id = ObjectId::Sha1([0u8; 20]);
|
||||
let title = mockd::words::sentence(10);
|
||||
let author = mockd::name::full();
|
||||
let email = mockd::contact::email();
|
||||
let repo = "project".to_string();
|
||||
let time = mockd::datetime::date_range(
|
||||
"2024-01-01T00:00:00Z".to_string(),
|
||||
"2025-01-01T00:00:00Z".to_string(),
|
||||
)
|
||||
.with_timezone(&Local);
|
||||
commits.push(Commit::new(id, title, author, email, repo, time));
|
||||
}
|
||||
|
||||
COMMITS.set(commits).expect("unable to generate commits");
|
||||
|
||||
divan::main();
|
||||
}
|
||||
|
||||
#[divan::bench]
|
||||
fn heatmap_generation() {
|
||||
let since = NaiveDate::parse_from_str("2024-01-01", "%Y-%m-%d").unwrap();
|
||||
|
||||
let until = NaiveDate::parse_from_str("2025-01-01", "%Y-%m-%d").unwrap();
|
||||
|
||||
let commits_vec = COMMITS
|
||||
.get()
|
||||
.expect("unable to access commits list")
|
||||
.to_vec();
|
||||
let commits = commits_vec.len();
|
||||
let repos = 1;
|
||||
|
||||
let _heatmap = Heatmap::new(
|
||||
since,
|
||||
until,
|
||||
commits,
|
||||
repos,
|
||||
commits_vec,
|
||||
false,
|
||||
13,
|
||||
heatmap::Format::Chars,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2024-12-02"
|
||||
channel = "nightly-2025-04-07"
|
||||
31
src/cli.rs
31
src/cli.rs
|
|
@ -1,15 +1,15 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{Duration, Local};
|
||||
use clap::{arg, Parser, ValueHint};
|
||||
use clap::{Parser, ValueHint, arg};
|
||||
|
||||
use crate::heatmap::{ColorLogic, Format, HeatmapColors};
|
||||
|
||||
#[derive(Clone, Debug, Parser, PartialEq, Eq)]
|
||||
#[derive(Clone, Default, Debug, Parser, PartialEq, Eq)]
|
||||
#[command(version, about, author, long_about = None, args_override_self = true)]
|
||||
pub struct CliArgs {
|
||||
#[arg(long("root-dir"), value_hint = ValueHint::DirPath)]
|
||||
pub root_dir: Option<PathBuf>,
|
||||
pub root_dir: Option<Vec<PathBuf>>,
|
||||
|
||||
#[arg(short, long, num_args(0..))]
|
||||
pub authors: Option<Vec<String>>,
|
||||
|
|
@ -32,7 +32,12 @@ pub struct CliArgs {
|
|||
#[arg(long("since"), default_value_t = get_since_date())]
|
||||
pub since: String,
|
||||
|
||||
#[arg(long("until"))]
|
||||
#[arg(
|
||||
long("until"),
|
||||
help(
|
||||
"Optional upper limit for the time slice being checked, defaults to 365 days after the `since` date.\n`today` can be used for current date."
|
||||
)
|
||||
)]
|
||||
pub until: Option<String>,
|
||||
|
||||
#[arg(long("split-months"), help("Split months"), default_value_t = false)]
|
||||
|
|
@ -48,14 +53,30 @@ pub struct CliArgs {
|
|||
#[arg(long("no-merges"), default_value_t = false)]
|
||||
pub no_merges: bool,
|
||||
|
||||
// Experimental
|
||||
#[arg(long("no-diff"), default_value_t = false)]
|
||||
pub no_diff: bool,
|
||||
|
||||
#[arg(long("use-author-time"), default_value_t = false)]
|
||||
pub use_author_time: bool,
|
||||
|
||||
#[arg(long("counting"), value_enum, default_value_t = ColorLogic::ByWeight)]
|
||||
pub counting: ColorLogic,
|
||||
|
||||
#[arg(short, long("format"), value_enum, default_value_t = Format::Chars)]
|
||||
pub format: Format,
|
||||
|
||||
#[arg(long("list-repos"), default_value_t = false)]
|
||||
pub list_repos: bool,
|
||||
|
||||
#[arg(long("list-days"), default_value_t = false)]
|
||||
pub list_days: bool,
|
||||
|
||||
#[arg(long("list-commits"), default_value_t = false)]
|
||||
pub list_commits: bool,
|
||||
}
|
||||
|
||||
fn get_since_date() -> String {
|
||||
let date = Local::now() - Duration::days(365);
|
||||
let date = Local::now() - Duration::days(364);
|
||||
date.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
|
|
|||
123
src/heatmap.rs
123
src/heatmap.rs
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
|
|
@ -7,20 +7,26 @@ use chrono::{Datelike, Duration, NaiveDate};
|
|||
use clap::ValueEnum;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{get_char, get_color, get_color_map, Commit, DAYS, RESET};
|
||||
use crate::{Commit, DAYS, RESET, get_char, get_color, get_color_map};
|
||||
|
||||
pub struct Heatmap {
|
||||
since: NaiveDate,
|
||||
until: NaiveDate,
|
||||
commits: Vec<Commit>,
|
||||
repo_commits: Vec<(String, u64)>,
|
||||
highest_count: i32,
|
||||
branches: usize,
|
||||
repos: usize,
|
||||
chunks: Vec<Chunk>,
|
||||
streak: u32,
|
||||
max_streak: u32,
|
||||
|
||||
format: Format,
|
||||
list_repos: bool,
|
||||
list_days: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl Heatmap {
|
||||
pub fn new(
|
||||
since: NaiveDate,
|
||||
|
|
@ -31,24 +37,33 @@ impl Heatmap {
|
|||
split_months: bool,
|
||||
months_per_row: u16,
|
||||
format: Format,
|
||||
list_repos: bool,
|
||||
list_days: bool,
|
||||
) -> Self {
|
||||
let mut heatmap = Self {
|
||||
since,
|
||||
until,
|
||||
commits,
|
||||
highest_count: 0,
|
||||
branches,
|
||||
repos,
|
||||
chunks: vec![],
|
||||
format,
|
||||
};
|
||||
|
||||
let mut chunks = vec![];
|
||||
let mut highest_count: i32 = 0;
|
||||
let mut grouped_commits = BTreeMap::new();
|
||||
let mut repo_commits_map: HashMap<String, u64> = HashMap::new();
|
||||
let mut repo_commits = Vec::<(String, u64)>::new();
|
||||
|
||||
for commit in &heatmap.commits {
|
||||
for commit in &commits {
|
||||
let commit_day = commit.time.date_naive();
|
||||
let record = grouped_commits.entry(commit_day).or_insert(0);
|
||||
*record += 1;
|
||||
|
||||
if list_repos {
|
||||
let commits = repo_commits_map.entry(commit.repo.clone()).or_insert(0);
|
||||
*commits += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if list_repos {
|
||||
repo_commits.extend(
|
||||
repo_commits_map
|
||||
.into_iter()
|
||||
.sorted_by(|c1, c2| c2.1.cmp(&c1.1))
|
||||
.map(|e| (e.0, e.1)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut current_day = since;
|
||||
|
|
@ -56,6 +71,8 @@ impl Heatmap {
|
|||
|
||||
let mut chunk = Chunk::new(day_of_week);
|
||||
let mut chunk_idx = 0;
|
||||
let mut streak = 0;
|
||||
let mut max_streak = 0;
|
||||
|
||||
// Track the very first day of the heatmap, as we don't want the extra spacing in front of
|
||||
// those.
|
||||
|
|
@ -82,7 +99,7 @@ impl Heatmap {
|
|||
chunk_idx += 1;
|
||||
|
||||
if chunk_idx > months_per_row - 1 {
|
||||
heatmap.chunks.push(chunk);
|
||||
chunks.push(chunk);
|
||||
chunk = Chunk::new(day_of_week);
|
||||
chunk_idx = 0;
|
||||
}
|
||||
|
|
@ -95,12 +112,18 @@ impl Heatmap {
|
|||
let value = grouped_commits.get(¤t_day);
|
||||
match value {
|
||||
Some(val) => {
|
||||
streak += 1;
|
||||
if streak > max_streak {
|
||||
max_streak = streak;
|
||||
}
|
||||
|
||||
chunk.data[day_of_week as usize].push(*val);
|
||||
if *val > heatmap.highest_count {
|
||||
heatmap.highest_count = *val;
|
||||
if *val > highest_count {
|
||||
highest_count = *val;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
streak = 0;
|
||||
chunk.data[day_of_week as usize].push(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,10 +133,25 @@ impl Heatmap {
|
|||
}
|
||||
|
||||
if chunk_idx <= months_per_row {
|
||||
heatmap.chunks.push(chunk);
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
heatmap
|
||||
Self {
|
||||
since,
|
||||
until,
|
||||
commits,
|
||||
repo_commits,
|
||||
highest_count,
|
||||
branches,
|
||||
repos,
|
||||
chunks,
|
||||
streak,
|
||||
max_streak,
|
||||
|
||||
format,
|
||||
list_repos,
|
||||
list_days,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,11 +180,40 @@ impl Display for Heatmap {
|
|||
)
|
||||
.unwrap();
|
||||
writeln!(f, "{} {}", authors, authors_label).unwrap();
|
||||
writeln!(f, "{} {}\n", commits, commits_label).unwrap();
|
||||
writeln!(f, "{} {}", commits, commits_label).unwrap();
|
||||
|
||||
if self.list_repos {
|
||||
for (repo, repo_commits) in &self.repo_commits {
|
||||
let commits_label = if *repo_commits == 1 {
|
||||
"commit"
|
||||
}
|
||||
else {
|
||||
"commits"
|
||||
};
|
||||
writeln!(f, " {}: {} {}", repo, repo_commits, commits_label).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(
|
||||
f,
|
||||
"{} current streak | {} longest streak",
|
||||
self.streak, self.max_streak
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(f).unwrap();
|
||||
|
||||
let mut per_day_commits: [i32; 7] = [0, 0, 0, 0, 0, 0, 0];
|
||||
|
||||
for chunk in &self.chunks {
|
||||
chunk.display(self, f);
|
||||
writeln!(f).unwrap();
|
||||
if self.list_days {
|
||||
per_day_commits
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, v)| *v += chunk.data[i].iter().sum::<i32>());
|
||||
}
|
||||
}
|
||||
|
||||
write!(f, "\nLess ").unwrap();
|
||||
|
|
@ -155,6 +222,13 @@ impl Display for Heatmap {
|
|||
}
|
||||
writeln!(f, " More").unwrap();
|
||||
|
||||
if self.list_days {
|
||||
writeln!(f).unwrap();
|
||||
for day in 0..DAYS.len() {
|
||||
writeln!(f, "{}: {}", DAYS[day], per_day_commits[day]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -234,20 +308,23 @@ impl Chunk {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum HeatmapColors {
|
||||
#[default]
|
||||
Green,
|
||||
Red,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum ColorLogic {
|
||||
#[default]
|
||||
ByAmount,
|
||||
ByWeight,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Chars,
|
||||
Numbers,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,439 @@
|
|||
#![feature(let_chains)]
|
||||
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
fmt::Display,
|
||||
path::{self, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveTime, TimeZone};
|
||||
use clap::Parser;
|
||||
use cli::CliArgs;
|
||||
use gix::{
|
||||
ObjectId, ThreadSafeRepository, bstr::ByteSlice, revision::walk::Sorting,
|
||||
traverse::commit::simple::CommitTimeOrder,
|
||||
};
|
||||
use heatmap::{ColorLogic, HeatmapColors};
|
||||
use itertools::Itertools;
|
||||
use mailmap::Mailmap;
|
||||
use rayon::prelude::*;
|
||||
use rgb::Rgb;
|
||||
|
||||
pub mod cli;
|
||||
pub mod heatmap;
|
||||
pub mod mailmap;
|
||||
pub mod rgb;
|
||||
|
||||
pub const ESCAPE: &str = "\x1B";
|
||||
pub const RESET: &str = "\x1B[0m";
|
||||
pub const DAYS: [&str; 7] = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||
pub static CHAR: OnceLock<char> = OnceLock::new();
|
||||
pub static COLOR_LOGIC: OnceLock<ColorLogic> = OnceLock::new();
|
||||
pub static COLOR_MAP: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
pub const GREEN_COLOR_MAP: [Rgb; 5] = [
|
||||
Rgb(0, 0, 0),
|
||||
Rgb(14, 68, 41),
|
||||
Rgb(0, 109, 50),
|
||||
Rgb(38, 166, 65),
|
||||
Rgb(25, 255, 64),
|
||||
];
|
||||
|
||||
pub const RED_COLOR_MAP: [Rgb; 5] = [
|
||||
Rgb(0, 0, 0),
|
||||
Rgb(208, 169, 35),
|
||||
Rgb(208, 128, 35),
|
||||
Rgb(208, 78, 35),
|
||||
Rgb(255, 0, 0),
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Commit {
|
||||
pub id: ObjectId,
|
||||
pub title: String,
|
||||
pub author: Author,
|
||||
pub repo: String,
|
||||
pub time: DateTime<Local>,
|
||||
}
|
||||
|
||||
impl Commit {
|
||||
pub fn new(
|
||||
id: ObjectId,
|
||||
title: String,
|
||||
author: String,
|
||||
email: String,
|
||||
repo: String,
|
||||
time: DateTime<Local>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
author: Author {
|
||||
name: author,
|
||||
email,
|
||||
},
|
||||
repo,
|
||||
time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Author {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Display for Author {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{} <{}>", self.name, self.email))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn args() -> CliArgs {
|
||||
let args = CliArgs::parse();
|
||||
|
||||
CHAR.set(args.char).unwrap();
|
||||
COLOR_LOGIC.set(args.counting).unwrap();
|
||||
let color_map = match args.color_scheme {
|
||||
HeatmapColors::Green => GREEN_COLOR_MAP,
|
||||
HeatmapColors::Red => RED_COLOR_MAP,
|
||||
};
|
||||
let color_map = color_map
|
||||
.into_iter()
|
||||
.map(|c| c.to_ansi())
|
||||
.collect::<Vec<_>>();
|
||||
COLOR_MAP.set(color_map).unwrap();
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
pub fn get_commits(
|
||||
args: CliArgs,
|
||||
start_date: NaiveDate,
|
||||
end_date: NaiveDate,
|
||||
) -> anyhow::Result<(usize, usize, Vec<Commit>)> {
|
||||
let mut commits: Vec<Commit> = vec![];
|
||||
|
||||
let ignored_repos = args.ignored_repos.as_ref().unwrap_or(&vec![]).to_owned();
|
||||
|
||||
let (repos, branches) = match &args.root_dir {
|
||||
Some(roots) => {
|
||||
let mut repos: Vec<PathBuf> = vec![];
|
||||
for root in roots {
|
||||
find_git_repos(root, &mut repos, &ignored_repos);
|
||||
}
|
||||
let branches = vec!["".to_string(); repos.len()];
|
||||
(repos, branches)
|
||||
}
|
||||
None => {
|
||||
let mut repos = match args.repos {
|
||||
Some(r) => r,
|
||||
None => vec![PathBuf::from(".")],
|
||||
};
|
||||
|
||||
// Turn any potential relative paths (such as `.`) into an absolute path
|
||||
repos = repos
|
||||
.iter_mut()
|
||||
.filter_map(|p| std::fs::canonicalize(p).ok())
|
||||
.collect_vec();
|
||||
|
||||
let branches = args.branches.unwrap_or_else(|| vec!["".to_string()]);
|
||||
|
||||
if repos.len() > 1 && repos.len() != branches.len() {
|
||||
return Err(anyhow!(
|
||||
"Number of repos ({}) needs to match the number of branch lists ({})!",
|
||||
repos.len(),
|
||||
branches.len()
|
||||
));
|
||||
}
|
||||
(repos, branches)
|
||||
}
|
||||
};
|
||||
|
||||
let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||
let start_date = start_date.and_time(midnight);
|
||||
let start_date = Local.from_local_datetime(&start_date).unwrap();
|
||||
|
||||
let current_time = Local::now().time();
|
||||
let end_date = end_date.and_time(current_time);
|
||||
let end_date = Local.from_local_datetime(&end_date).unwrap();
|
||||
|
||||
let authors = args.authors.unwrap_or_default();
|
||||
|
||||
let mut repos_count: usize = 0;
|
||||
let mut branches_count: usize = 0;
|
||||
let mut cached_commits: Vec<Commit> = vec![];
|
||||
|
||||
for (i, repo_path) in repos.iter().enumerate() {
|
||||
let repo = ThreadSafeRepository::open(repo_path).unwrap();
|
||||
|
||||
let mut repo_name = repo_path.file_name();
|
||||
if repo_path.ends_with(gix::discover::DOT_GIT_DIR) {
|
||||
if let Some(parent) = repo_path.parent() {
|
||||
repo_name = parent.file_name();
|
||||
};
|
||||
}
|
||||
|
||||
let repo_name = repo_name
|
||||
.iter()
|
||||
.filter_map(|r| r.to_str())
|
||||
.next()
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let branch_names = &*branches[i];
|
||||
let branches = get_repo_branches(&repo, branch_names).unwrap();
|
||||
|
||||
let mailmap = Mailmap::new(repo_path);
|
||||
|
||||
let branch_commits: Vec<_> = branches
|
||||
.par_iter()
|
||||
.filter_map(|branch| get_commit_ids(&repo, branch, start_date))
|
||||
.reduce(Vec::new, |mut c, n| {
|
||||
c.extend(n);
|
||||
c
|
||||
});
|
||||
|
||||
let repo = repo.to_thread_local();
|
||||
|
||||
let branch_commits = branch_commits
|
||||
.into_iter()
|
||||
.unique()
|
||||
.filter_map(|c| repo.find_commit(c).ok())
|
||||
.filter_map(|c| {
|
||||
let title = c
|
||||
.message()
|
||||
.ok()?
|
||||
.title
|
||||
.trim_ascii()
|
||||
.to_str()
|
||||
.ok()?
|
||||
.to_string();
|
||||
|
||||
if args.no_merges {
|
||||
let is_merge = c.parent_ids().count() > 1;
|
||||
if is_merge {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let author = c.author().ok()?;
|
||||
|
||||
// Ignores commits with different commit / author times
|
||||
// Usually due to rebases or cherry picking, however other edge cases may apply too
|
||||
if args.no_diff {
|
||||
let commit_info = c.committer().unwrap();
|
||||
if commit_info.time != author.time {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let email = author.email.to_string();
|
||||
let name = author.name.to_string();
|
||||
|
||||
let time = if args.use_author_time {
|
||||
author.time().ok()?
|
||||
}
|
||||
else {
|
||||
c.time().ok()?
|
||||
};
|
||||
let time =
|
||||
DateTime::from_timestamp_millis(time.seconds * 1000)?.with_timezone(&Local);
|
||||
if time < start_date || time > end_date {
|
||||
return None;
|
||||
}
|
||||
|
||||
let author = Author { name, email };
|
||||
let author = mailmap.resolve(author);
|
||||
|
||||
if !authors.is_empty() && !authors.contains(&author.name) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let commit = Commit {
|
||||
id: c.id,
|
||||
title,
|
||||
author,
|
||||
repo: repo_name.to_string(),
|
||||
time,
|
||||
};
|
||||
|
||||
if args.use_author_time {
|
||||
for other in &cached_commits {
|
||||
if other.author == commit.author
|
||||
&& other.title == commit.title
|
||||
&& other.time == commit.time
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
cached_commits.push(commit.clone());
|
||||
}
|
||||
|
||||
Some(commit)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
if !branch_commits.is_empty() {
|
||||
repos_count += 1;
|
||||
branches_count += branches.len();
|
||||
}
|
||||
|
||||
commits.extend(branch_commits);
|
||||
}
|
||||
|
||||
commits.par_sort_by_cached_key(|a| Reverse(a.time));
|
||||
|
||||
Ok((repos_count, branches_count, commits))
|
||||
}
|
||||
|
||||
fn get_repo_branches(repo: &ThreadSafeRepository, branch_names: &str) -> Option<Vec<String>> {
|
||||
if branch_names.is_empty() {
|
||||
let repo = repo.to_thread_local();
|
||||
let Ok(refs) = repo.references()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(prefix) = refs.prefixed("refs/heads")
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let branches = prefix
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|b| {
|
||||
b.inner
|
||||
.name
|
||||
.to_string()
|
||||
.strip_prefix("refs/heads/")
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(branches)
|
||||
}
|
||||
else {
|
||||
Some(branch_names.split(' ').map(|s| s.to_string()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_commit_ids(
|
||||
repo: &ThreadSafeRepository,
|
||||
branch: &str,
|
||||
start_date: DateTime<Local>,
|
||||
) -> Option<Vec<ObjectId>> {
|
||||
let repo = repo.to_thread_local();
|
||||
|
||||
// When passing the default @ (HEAD) branch this might actually not exist at all
|
||||
// locally so we're skipping it
|
||||
let rev = repo.rev_parse(branch).ok()?;
|
||||
|
||||
let branch_commits = rev
|
||||
.single()
|
||||
.unwrap()
|
||||
.ancestors()
|
||||
.sorting(Sorting::ByCommitTimeCutoff {
|
||||
order: CommitTimeOrder::NewestFirst,
|
||||
seconds: start_date.timestamp(),
|
||||
})
|
||||
.all()
|
||||
.ok()?;
|
||||
|
||||
let commits = branch_commits
|
||||
.filter_map(|c| c.ok())
|
||||
.map(|c| c.id)
|
||||
.collect();
|
||||
|
||||
Some(commits)
|
||||
}
|
||||
|
||||
fn find_git_repos(scan_path: &path::Path, repos: &mut Vec<PathBuf>, ignored_repos: &Vec<String>) {
|
||||
if let Some(path) = walk_dir(scan_path, ignored_repos) {
|
||||
repos.extend(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_dir(scan_path: &path::Path, ignored_repos: &Vec<String>) -> Option<Vec<PathBuf>> {
|
||||
let Ok(dirs) = scan_path.read_dir()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let dirs: Vec<PathBuf> = dirs
|
||||
.par_bridge()
|
||||
.filter_map(|d| d.ok())
|
||||
.filter(|d| {
|
||||
let dir_name = d.file_name().to_string_lossy().to_string();
|
||||
!ignored_repos.contains(&dir_name)
|
||||
})
|
||||
.filter(|d| d.file_type().is_ok_and(|t| t.is_dir()))
|
||||
.filter_map(|d| {
|
||||
let dir = d.path();
|
||||
let filename = dir.file_name().unwrap_or_default().to_string_lossy();
|
||||
|
||||
match filename.as_ref() {
|
||||
".git" => Some(vec![dir]),
|
||||
_ => walk_dir(&dir, ignored_repos),
|
||||
}
|
||||
})
|
||||
.reduce(Vec::new, |mut c, n| {
|
||||
c.extend(n);
|
||||
c
|
||||
});
|
||||
|
||||
Some(dirs)
|
||||
}
|
||||
|
||||
pub fn get_default_until(since: NaiveDate) -> String {
|
||||
let mut until = Local::now().date_naive();
|
||||
if since + Duration::days(365) < until {
|
||||
until = since + Duration::days(365);
|
||||
}
|
||||
until.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
fn get_color(val: i32, high: i32) -> usize {
|
||||
match COLOR_LOGIC.get() {
|
||||
Some(logic) => match logic {
|
||||
ColorLogic::ByAmount => match val {
|
||||
0 => 0,
|
||||
x if x < 2 => 1,
|
||||
x if x < 4 => 2,
|
||||
x if x < 6 => 3,
|
||||
x if x >= 6 => 4,
|
||||
_ => 0,
|
||||
},
|
||||
ColorLogic::ByWeight => {
|
||||
let color = val as f32 / high as f32;
|
||||
match color {
|
||||
0.0 => 0,
|
||||
x if x <= 0.2 => 1,
|
||||
x if x <= 0.4 => 2,
|
||||
x if x <= 0.8 => 3,
|
||||
x if x > 0.8 => 4,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_char() -> char {
|
||||
*CHAR.get_or_init(|| '▩')
|
||||
}
|
||||
|
||||
fn get_color_map() -> Vec<String> {
|
||||
COLOR_MAP
|
||||
.get_or_init(|| {
|
||||
GREEN_COLOR_MAP
|
||||
.into_iter()
|
||||
.map(|c| c.to_ansi())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.to_vec()
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ pub struct Mailmap {
|
|||
entries: Vec<MapEntry>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
struct MapEntry {
|
||||
new_name: Option<String>,
|
||||
|
|
|
|||
373
src/main.rs
373
src/main.rs
|
|
@ -1,346 +1,63 @@
|
|||
#![feature(let_chains)]
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::NaiveDate;
|
||||
use libgitheatmap::{RESET, heatmap::Heatmap, rgb::Rgb};
|
||||
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashSet,
|
||||
path::{self, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveTime, TimeZone};
|
||||
use clap::Parser;
|
||||
use gix::{bstr::ByteSlice, traverse::commit::simple::Sorting, ObjectId};
|
||||
use heatmap::{ColorLogic, HeatmapColors};
|
||||
use itertools::Itertools;
|
||||
use mailmap::Mailmap;
|
||||
use rgb::Rgb;
|
||||
|
||||
use crate::{cli::CliArgs, heatmap::Heatmap};
|
||||
|
||||
mod cli;
|
||||
mod heatmap;
|
||||
mod mailmap;
|
||||
mod rgb;
|
||||
|
||||
pub const ESCAPE: &str = "\x1B";
|
||||
pub const RESET: &str = "\x1B[0m";
|
||||
pub const DAYS: [&str; 7] = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||
pub static CHAR: OnceLock<char> = OnceLock::new();
|
||||
static COLOR_LOGIC: OnceLock<ColorLogic> = OnceLock::new();
|
||||
pub static COLOR_MAP: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
const GREEN_COLOR_MAP: [Rgb; 5] = [
|
||||
Rgb(0, 0, 0),
|
||||
Rgb(14, 68, 41),
|
||||
Rgb(0, 109, 50),
|
||||
Rgb(38, 166, 65),
|
||||
Rgb(25, 255, 64),
|
||||
];
|
||||
|
||||
const RED_COLOR_MAP: [Rgb; 5] = [
|
||||
Rgb(0, 0, 0),
|
||||
Rgb(208, 169, 35),
|
||||
Rgb(208, 128, 35),
|
||||
Rgb(208, 78, 35),
|
||||
Rgb(255, 0, 0),
|
||||
];
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
struct Commit {
|
||||
id: ObjectId,
|
||||
title: String,
|
||||
author: Author,
|
||||
time: DateTime<Local>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
struct Author {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
const REPO_COLOR: Rgb = Rgb(0, 255, 0);
|
||||
const AUTHOR_COLOR: Rgb = Rgb(200, 200, 0);
|
||||
const TIME_COLOR: Rgb = Rgb(0, 200, 200);
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = CliArgs::parse();
|
||||
|
||||
CHAR.set(args.char).unwrap();
|
||||
COLOR_LOGIC.set(args.counting.clone()).unwrap();
|
||||
let color_map = match args.color_scheme {
|
||||
HeatmapColors::Green => GREEN_COLOR_MAP,
|
||||
HeatmapColors::Red => RED_COLOR_MAP,
|
||||
};
|
||||
let color_map = color_map
|
||||
.into_iter()
|
||||
.map(|c| c.to_ansi())
|
||||
.collect::<Vec<_>>();
|
||||
COLOR_MAP.set(color_map).unwrap();
|
||||
let args = libgitheatmap::args();
|
||||
|
||||
let since = NaiveDate::parse_from_str(&args.since, "%Y-%m-%d").unwrap();
|
||||
|
||||
let until = args
|
||||
.until
|
||||
.clone()
|
||||
.unwrap_or_else(|| get_default_until(since));
|
||||
let until = NaiveDate::parse_from_str(&until, "%Y-%m-%d").unwrap();
|
||||
.unwrap_or_else(|| libgitheatmap::get_default_until(since));
|
||||
let until = match until.to_lowercase().as_str() {
|
||||
"today" => chrono::Local::now().date_naive(),
|
||||
_ => NaiveDate::parse_from_str(&until, "%Y-%m-%d").unwrap(),
|
||||
};
|
||||
|
||||
let split_months = args.split_months;
|
||||
let months_per_row = args.months_per_row;
|
||||
let format = args.format.clone();
|
||||
let format = args.format;
|
||||
let list_repos = args.list_repos;
|
||||
let list_days = args.list_days;
|
||||
let list_commits = args.list_commits;
|
||||
|
||||
let commits = get_commits(args, since, until).with_context(|| "Could not fetch commit list")?;
|
||||
let mut commits = libgitheatmap::get_commits(args, since, until)
|
||||
.with_context(|| "Could not fetch commit list")?;
|
||||
|
||||
let heatmap = Heatmap::new(
|
||||
since,
|
||||
until,
|
||||
commits.0,
|
||||
commits.1,
|
||||
commits.2,
|
||||
split_months,
|
||||
months_per_row,
|
||||
format,
|
||||
);
|
||||
if list_commits {
|
||||
// Newer entries at the bottom, older entries at top
|
||||
commits.2.reverse();
|
||||
for commit in commits.2 {
|
||||
let repo = format!("{}{}", REPO_COLOR.to_ansi(), commit.repo);
|
||||
let author = format!("{}{}", AUTHOR_COLOR.to_ansi(), commit.author);
|
||||
let time = format!("{}{}", TIME_COLOR.to_ansi(), commit.time);
|
||||
let message = commit.title;
|
||||
|
||||
println!("{heatmap}");
|
||||
println!("{repo} {author} {time}\n{RESET}{message}\n",);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let heatmap = Heatmap::new(
|
||||
since,
|
||||
until,
|
||||
commits.0,
|
||||
commits.1,
|
||||
commits.2,
|
||||
split_months,
|
||||
months_per_row,
|
||||
format,
|
||||
list_repos,
|
||||
list_days,
|
||||
);
|
||||
|
||||
println!("{heatmap}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_default_until(since: NaiveDate) -> String {
|
||||
let mut until = Local::now().date_naive();
|
||||
if since + Duration::days(365) < until {
|
||||
until = since + Duration::days(365);
|
||||
}
|
||||
until.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
fn get_color(val: i32, high: i32) -> usize {
|
||||
match COLOR_LOGIC.get() {
|
||||
Some(logic) => match logic {
|
||||
ColorLogic::ByAmount => match val {
|
||||
0 => 0,
|
||||
x if x < 2 => 1,
|
||||
x if x < 4 => 2,
|
||||
x if x < 6 => 3,
|
||||
x if x >= 6 => 4,
|
||||
_ => 0,
|
||||
},
|
||||
ColorLogic::ByWeight => {
|
||||
let color = val as f32 / high as f32;
|
||||
match color {
|
||||
0.0 => 0,
|
||||
x if x <= 0.2 => 1,
|
||||
x if x <= 0.4 => 2,
|
||||
x if x <= 0.8 => 3,
|
||||
x if x > 0.8 => 4,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_char() -> char {
|
||||
*CHAR.get_or_init(|| '▩')
|
||||
}
|
||||
|
||||
fn get_color_map() -> Vec<String> {
|
||||
COLOR_MAP
|
||||
.get_or_init(|| {
|
||||
GREEN_COLOR_MAP
|
||||
.into_iter()
|
||||
.map(|c| c.to_ansi())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
fn find_git_repos(
|
||||
scan_path: &path::Path,
|
||||
repos: &mut Vec<PathBuf>,
|
||||
ignored_repos: &Vec<String>,
|
||||
_args: &CliArgs,
|
||||
) {
|
||||
let Ok(dirs) = scan_path.read_dir()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dirs: Vec<_> = dirs
|
||||
.filter_map(|d| d.ok())
|
||||
.filter(|d| {
|
||||
let dir_name = d.file_name().to_string_lossy().to_string();
|
||||
!ignored_repos.contains(&dir_name)
|
||||
})
|
||||
.filter(|d| d.file_type().is_ok_and(|t| t.is_dir()))
|
||||
.collect_vec();
|
||||
|
||||
let dirs = dirs.iter().map(|d| d.path());
|
||||
|
||||
for dir in dirs {
|
||||
let filename = dir.file_name().unwrap_or_default().to_string_lossy();
|
||||
match filename.as_ref() {
|
||||
".git" => repos.push(dir),
|
||||
_ => find_git_repos(&dir, repos, ignored_repos, _args),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_commits(
|
||||
args: CliArgs,
|
||||
start_date: NaiveDate,
|
||||
end_date: NaiveDate,
|
||||
) -> Result<(usize, usize, Vec<Commit>)> {
|
||||
let mut commits: HashSet<Commit> = HashSet::new();
|
||||
|
||||
let ignored_repos = args.ignored_repos.as_ref().unwrap_or(&vec![]).to_owned();
|
||||
|
||||
let (repos, branches) = match &args.root_dir {
|
||||
Some(root) => {
|
||||
let mut repos: Vec<PathBuf> = vec![];
|
||||
find_git_repos(root, &mut repos, &ignored_repos, &args);
|
||||
let branches = vec!["".to_string(); repos.len()];
|
||||
(repos, branches)
|
||||
}
|
||||
None => {
|
||||
let repos = match args.repos {
|
||||
Some(r) => r,
|
||||
None => vec![PathBuf::from(".")],
|
||||
};
|
||||
|
||||
let branches = args.branches.unwrap_or_else(|| vec!["".to_string()]);
|
||||
|
||||
if repos.len() > 1 && repos.len() != branches.len() {
|
||||
return Err(anyhow!(
|
||||
"Number of repos ({}) needs to match the number of branch lists ({})!",
|
||||
repos.len(),
|
||||
branches.len()
|
||||
));
|
||||
}
|
||||
(repos, branches)
|
||||
}
|
||||
};
|
||||
|
||||
let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||
let start_date = start_date.and_time(midnight);
|
||||
let start_date = Local.from_local_datetime(&start_date).unwrap();
|
||||
|
||||
let current_time = Local::now().time();
|
||||
let end_date = end_date.and_time(current_time);
|
||||
let end_date = Local.from_local_datetime(&end_date).unwrap();
|
||||
|
||||
let authors = args.authors.unwrap_or_default();
|
||||
let mut repos_count = 0;
|
||||
let mut branches_count = 0;
|
||||
|
||||
for (i, repo_path) in repos.iter().enumerate() {
|
||||
let repo = gix::open(repo_path).unwrap();
|
||||
|
||||
let branch_names = &*branches[i];
|
||||
let mut branches = vec![];
|
||||
if branch_names.is_empty() {
|
||||
branches = repo
|
||||
.references()?
|
||||
.prefixed("refs/heads")?
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|b| {
|
||||
b.inner
|
||||
.name
|
||||
.to_string()
|
||||
.strip_prefix("refs/heads/")
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect_vec();
|
||||
}
|
||||
else {
|
||||
let branch_names = branch_names.split(' ').map(|s| s.to_string());
|
||||
branches.extend(branch_names);
|
||||
}
|
||||
|
||||
branches_count += branches.len();
|
||||
|
||||
let mailmap = Mailmap::new(repo_path);
|
||||
let mut has_commits = false;
|
||||
|
||||
for branch in branches {
|
||||
// When passing the default @ (HEAD) branch this might actually not exist at all
|
||||
// locally so we're skipping it
|
||||
let Ok(rev) = repo.rev_parse(&*branch)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let branch_commits = rev
|
||||
.single()
|
||||
.unwrap()
|
||||
.ancestors()
|
||||
.sorting(Sorting::ByCommitTimeNewestFirstCutoffOlderThan {
|
||||
seconds: start_date.timestamp(),
|
||||
})
|
||||
.all()?;
|
||||
|
||||
branch_commits
|
||||
.filter_map(|c| c.ok())
|
||||
.filter_map(|c| c.object().ok())
|
||||
.filter_map(|c| {
|
||||
let title = c
|
||||
.message()
|
||||
.ok()?
|
||||
.title
|
||||
.trim_ascii()
|
||||
.to_str()
|
||||
.ok()?
|
||||
.to_string();
|
||||
|
||||
if args.no_merges {
|
||||
let is_merge = c.parent_ids().count() > 1;
|
||||
if is_merge {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let author = c.author().ok()?;
|
||||
|
||||
let email = author.email.to_string();
|
||||
let name = author.name.to_string();
|
||||
|
||||
let author = Author { name, email };
|
||||
let author = mailmap.resolve(author);
|
||||
|
||||
if !authors.is_empty() && !authors.contains(&author.name) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let time = c.time().ok()?;
|
||||
let time =
|
||||
DateTime::from_timestamp_millis(time.seconds * 1000)?.with_timezone(&Local);
|
||||
if time < start_date || time > end_date {
|
||||
return None;
|
||||
}
|
||||
|
||||
has_commits = true;
|
||||
|
||||
Some(Commit {
|
||||
id: c.id,
|
||||
title,
|
||||
author,
|
||||
time,
|
||||
})
|
||||
})
|
||||
.for_each(|c| {
|
||||
commits.insert(c);
|
||||
});
|
||||
}
|
||||
|
||||
if has_commits {
|
||||
repos_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let commits = commits
|
||||
.into_iter()
|
||||
.sorted_by_cached_key(|a| Reverse(a.time))
|
||||
.collect_vec();
|
||||
|
||||
Ok((repos_count, branches_count, commits))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue