#![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 = OnceLock::new(); pub static COLOR_MAP: OnceLock> = 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, } 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::>(); 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 { COLOR_MAP .get_or_init(|| { GREEN_COLOR_MAP .into_iter() .map(|c| c.to_ansi()) .collect::>() }) .to_vec() } fn get_commits(repo: Repository, args: CliArgs, start_date: DateTime) -> Result> { let mut commits: Vec = 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, args: CliArgs) { let mut commits_pushed = 0; let mut days_streak = 0; let mut last_date: Option> = None; let mut start_date: Option> = None; let mut end_date: Option> = 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}"); } }