First impl, a heatmap with some clap configs
commit
f9f33e6b58
|
@ -0,0 +1,17 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
cargo-features = ["codegen-backend"]
|
||||
|
||||
[package]
|
||||
name = "gitstats"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = { level = "forbid" }
|
||||
|
||||
[dependencies]
|
||||
gix = { version = "0.64.0" }
|
||||
clap = { version = "4.5.13", features = ["derive"] }
|
||||
chrono = { version = "0.4.38" }
|
||||
itertools = { version = "0.13.0" }
|
||||
anyhow = { version = "1.0.86" }
|
||||
|
||||
[profile.dev]
|
||||
codegen-backend = "cranelift"
|
||||
lto = false
|
||||
incremental = true
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel= "nightly"
|
|
@ -0,0 +1,8 @@
|
|||
unstable_features = true
|
||||
reorder_imports = true
|
||||
hard_tabs = true
|
||||
control_brace_style = "ClosingNextLine"
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
edition = "2021"
|
||||
newline_style = "Unix"
|
|
@ -0,0 +1,23 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{arg, Parser, ValueHint};
|
||||
|
||||
use crate::heatmap::HeatmapColors;
|
||||
|
||||
#[derive(Clone, Debug, Parser, PartialEq, Eq)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct CliArgs {
|
||||
#[arg(default_value=".", value_hint = ValueHint::DirPath)]
|
||||
pub input: PathBuf,
|
||||
|
||||
#[arg(short, long)]
|
||||
pub author: String,
|
||||
|
||||
#[arg(short, long, default_value = "▩")]
|
||||
pub char: char,
|
||||
|
||||
#[arg(long("color"), value_enum, default_value_t = HeatmapColors::Green)]
|
||||
pub color_scheme: HeatmapColors,
|
||||
// #[arg(short, long, default_value = "24")]
|
||||
// pub split: u32,
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::{DateTime, Datelike, Duration, Utc};
|
||||
use clap::ValueEnum;
|
||||
|
||||
use crate::{get_char, get_color, get_color_map, Commit, DAYS, RESET};
|
||||
|
||||
pub struct Heatmap {
|
||||
data: [Vec<u32>; 7],
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
commits: Vec<Commit>,
|
||||
months: Vec<(usize, String)>,
|
||||
}
|
||||
|
||||
impl Heatmap {
|
||||
pub fn new(start_date: DateTime<Utc>, end_date: DateTime<Utc>, commits: Vec<Commit>) -> Self {
|
||||
let mut heatmap = Self {
|
||||
data: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
|
||||
start_date,
|
||||
end_date,
|
||||
commits,
|
||||
months: vec![],
|
||||
};
|
||||
|
||||
let mut grouped_commits = BTreeMap::new();
|
||||
|
||||
for commit in &heatmap.commits {
|
||||
let commit_day = commit.time.ordinal0();
|
||||
let record = grouped_commits.entry(commit_day).or_insert(0);
|
||||
*record += 1;
|
||||
}
|
||||
|
||||
let mut current_day = start_date;
|
||||
let mut day_of_week = (current_day.weekday().num_days_from_monday() + 1) % 7;
|
||||
|
||||
if day_of_week != 0 {
|
||||
for i in 0..day_of_week {
|
||||
heatmap.data[i as usize].push(0);
|
||||
}
|
||||
}
|
||||
|
||||
while current_day <= end_date {
|
||||
let month_name = current_day.format("%b").to_string();
|
||||
|
||||
if current_day == start_date {
|
||||
heatmap.months.push((0, month_name));
|
||||
}
|
||||
else if current_day.day0() == 0 {
|
||||
heatmap
|
||||
.months
|
||||
.push((heatmap.data[day_of_week as usize].len(), month_name));
|
||||
}
|
||||
|
||||
let value = grouped_commits.get(¤t_day.ordinal0());
|
||||
match value {
|
||||
Some(val) => heatmap.data[day_of_week as usize].push(*val),
|
||||
None => heatmap.data[day_of_week as usize].push(0),
|
||||
}
|
||||
current_day += Duration::days(1);
|
||||
day_of_week = (current_day.weekday().num_days_from_monday() + 1) % 7;
|
||||
// println!("{} - {current_day} | {value:?}", current_day.ordinal0());
|
||||
}
|
||||
|
||||
heatmap
|
||||
}
|
||||
|
||||
fn months_row(&self) -> String {
|
||||
let mut row = " ".to_string();
|
||||
|
||||
let mut last_index = 0;
|
||||
|
||||
for (index, month) in &self.months {
|
||||
let range_size = (index * 2).saturating_sub(last_index * 2).saturating_sub(3);
|
||||
for _i in 0..range_size {
|
||||
row.push(' ');
|
||||
}
|
||||
last_index = *index;
|
||||
row.push_str(month);
|
||||
}
|
||||
|
||||
row
|
||||
}
|
||||
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"{} - {}",
|
||||
self.start_date.format("%Y-%b-%d"),
|
||||
self.end_date.format("%Y-%b-%d")
|
||||
);
|
||||
println!("{} commits\n", &self.commits.len());
|
||||
println!("{}", self.months_row());
|
||||
for (day, row) in DAYS.iter().zip(&self.data) {
|
||||
print!("{day} ");
|
||||
for val in row {
|
||||
let color = &get_color_map()[get_color(*val)];
|
||||
print!("{color}{}{RESET} ", get_char());
|
||||
}
|
||||
print!("\n");
|
||||
}
|
||||
print!("\nLess ");
|
||||
for color in get_color_map() {
|
||||
print!("{color}{}{RESET} ", get_char())
|
||||
}
|
||||
print!(" More\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum HeatmapColors {
|
||||
Green,
|
||||
Red,
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
#![feature(byte_slice_trim_ascii)]
|
||||
#![feature(let_chains)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{cmp::Reverse, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Datelike, Duration, Utc};
|
||||
use clap::Parser;
|
||||
use gix::{bstr::ByteSlice, ObjectId, Repository};
|
||||
use heatmap::HeatmapColors;
|
||||
use rgb::Rgb;
|
||||
|
||||
use crate::{cli::CliArgs, heatmap::Heatmap};
|
||||
|
||||
mod cli;
|
||||
mod heatmap;
|
||||
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_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(57, 211, 83),
|
||||
];
|
||||
|
||||
const RED_COLOR_MAP: [Rgb; 5] = [
|
||||
Rgb(0, 0, 0),
|
||||
Rgb(208, 169, 35),
|
||||
Rgb(208, 128, 35),
|
||||
Rgb(208, 78, 35),
|
||||
Rgb(208, 35, 64),
|
||||
];
|
||||
|
||||
struct Commit {
|
||||
id: ObjectId,
|
||||
title: String,
|
||||
author: String,
|
||||
time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
clear_screen();
|
||||
|
||||
let args = CliArgs::parse();
|
||||
|
||||
CHAR.set(args.char).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 repo = gix::open(&args.input).unwrap();
|
||||
|
||||
let end_date = Utc::now() - Duration::days(1);
|
||||
let start_date = end_date - Duration::days(365);
|
||||
|
||||
let commits =
|
||||
get_commits(repo, args, start_date).with_context(|| "Could not fetch commit list")?;
|
||||
|
||||
let heatmap = Heatmap::new(start_date, end_date, commits);
|
||||
heatmap.print();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_color(val: u32) -> usize {
|
||||
match val {
|
||||
0 => 0,
|
||||
x if x < 2 => 1,
|
||||
x if x < 4 => 2,
|
||||
x if x < 6 => 3,
|
||||
x if x > 8 => 4,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_screen() {
|
||||
print!("\x1b[2J\x1b[1;1H");
|
||||
}
|
||||
|
||||
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 get_commits(repo: Repository, args: CliArgs, start_date: DateTime<Utc>) -> Result<Vec<Commit>> {
|
||||
let mut commits: Vec<Commit> = repo
|
||||
.head()?
|
||||
.into_peeled_id()?
|
||||
.ancestors()
|
||||
.all()?
|
||||
.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();
|
||||
|
||||
let author = c.author().ok()?.name.to_string();
|
||||
if author != args.author {
|
||||
return None;
|
||||
}
|
||||
|
||||
let time = c.time().ok()?;
|
||||
let time = DateTime::from_timestamp_millis(time.seconds * 1000)?;
|
||||
if time <= start_date + Duration::days(1) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Commit {
|
||||
id: c.id,
|
||||
title,
|
||||
author,
|
||||
time,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
commits.sort_by_cached_key(|a| Reverse(a.time));
|
||||
|
||||
Ok(commits)
|
||||
}
|
||||
|
||||
fn print_streak(commits: Vec<Commit>, args: CliArgs) {
|
||||
let mut commits_pushed = 0;
|
||||
let mut days_streak = 0;
|
||||
let mut last_date: Option<DateTime<Utc>> = None;
|
||||
let mut start_date: Option<DateTime<Utc>> = None;
|
||||
let mut end_date: Option<DateTime<Utc>> = None;
|
||||
|
||||
for commit in commits {
|
||||
match last_date {
|
||||
Some(date) => {
|
||||
let day = date.ordinal0();
|
||||
let commit_day = commit.time.ordinal0();
|
||||
let time_diff = date - commit.time;
|
||||
|
||||
if commit_day != day {
|
||||
days_streak += 1;
|
||||
}
|
||||
|
||||
// if time_diff.num_hours() >= args.split.into() {
|
||||
// println!(
|
||||
// "Failing Commit\n{} - {} {} hours difference",
|
||||
// commit.id,
|
||||
// commit.time.to_rfc3339(),
|
||||
// time_diff.num_hours()
|
||||
// );
|
||||
// break;
|
||||
// }
|
||||
|
||||
commits_pushed += 1;
|
||||
last_date = Some(commit.time);
|
||||
end_date = Some(commit.time);
|
||||
|
||||
println!(
|
||||
"{} - {} {} hours difference",
|
||||
commit.id,
|
||||
commit.time.to_rfc3339(),
|
||||
time_diff.num_hours()
|
||||
);
|
||||
}
|
||||
None => {
|
||||
last_date = Some(commit.time);
|
||||
start_date = Some(commit.time);
|
||||
println!("First Commit\n{} - {}", commit.id, commit.time.to_rfc3339());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{commits_pushed} commits pushed during a {days_streak} days streak");
|
||||
if let Some(start_date) = start_date
|
||||
&& let Some(end_date) = end_date
|
||||
{
|
||||
let start_date = start_date.format("%d-%b-%Y").to_string();
|
||||
let end_date = end_date.format("%d-%b-%Y").to_string();
|
||||
|
||||
println!("Between: {start_date} {end_date}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use crate::ESCAPE;
|
||||
|
||||
pub struct Rgb(pub u8, pub u8, pub u8);
|
||||
|
||||
impl Rgb {
|
||||
pub fn to_ansi(&self) -> String {
|
||||
format!("{ESCAPE}[38;2;{};{};{}m", self.0, self.1, self.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Rgb {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("rgb({},{},{})", self.0, self.1, self.2))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue