From 5a35e6b04c8c0fc06fdba97c5f994616c59969ec Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 21 Nov 2023 03:17:15 +0100 Subject: [PATCH] feat: first bad implementation it just reads path from argv[1] and plots commits per week for all users and quits at first keypress or mouse event --- Cargo.toml | 13 +++++ src/main.rs | 39 +++++++++++++++ src/tui.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/tui.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d7b7730 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gitts" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.31" +crossterm = "0.27.0" +git2 = "0.18.1" +rand = "0.8.5" +ratatui = { version = "0.24.0", features = ["all-widgets"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..653b712 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +use git2::{Repository, Commit}; +use chrono::DateTime; + +mod tui; + +fn main() { + let repo = Repository::open(std::env::args().nth(1).unwrap()).unwrap(); + + let mut rev = repo.revwalk().unwrap(); + rev.set_sorting(git2::Sort::TOPOLOGICAL).unwrap(); + rev.push_head().unwrap(); + + let mut store = HashMap::new(); + + for commit in rev { + match commit { + Err(e) => eprintln!("could not revwalk: {}", e), + Ok(oid) => { + let commit = repo.find_commit(oid).unwrap(); + let id_txt = commit.id().to_string()[0..8].to_string(); + let time = DateTime::from_timestamp(commit.time().seconds(), 0).unwrap(); + println!(" * {}: {}\t -- {}", id_txt, commit.summary().unwrap_or(""), author_display(&commit, true)); + store.insert(oid, (time, commit)); + }, + } + } + + tui::display(store, false).unwrap(); +} + +pub fn author_display(commit: &Commit, full: bool) -> String { + if full { + format!("{} <{}>", commit.author().name().unwrap_or(""), commit.author().email().unwrap_or("")) + } else { + commit.author().name().unwrap_or("").to_string() + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..a233f56 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,135 @@ +use std::collections::{HashMap, HashSet}; + +use rand::seq::SliceRandom; +use chrono::{Utc, Duration, DateTime}; +use crossterm::{terminal::{enable_raw_mode, EnterAlternateScreen, disable_raw_mode, LeaveAlternateScreen}, execute, event}; +use git2::{Commit, Oid}; +use ratatui::{prelude::*, widgets::{Dataset, GraphType, Chart, Block, Axis}}; + +const COLORS : [Color; 10] = [ + Color::Blue, + Color::LightBlue, + Color::Cyan, + Color::LightCyan, + Color::Green, + Color::LightGreen, + Color::Yellow, + Color::LightYellow, + Color::Magenta, + Color::LightMagenta, +]; + +pub fn display( + commits: HashMap, Commit)>, + total: bool, +) -> Result<(), Box> { + let now = Utc::now(); + let min = *commits.values().map(|(t, _c)| t).min().unwrap(); + let bins = (now - min).num_weeks() as usize; + + + let mut data = vec![(0f64, 0f64); bins + 1]; + let authors : HashSet = commits.values().map(|(_t, c)| crate::author_display(c, false)).collect(); + let mut authors_data : HashMap> = HashMap::new(); + for author in authors { + authors_data.insert(author, vec![(0f64, 0f64); bins + 1]); + } + + let mut y_max = 0.; + for (t, c) in commits.values() { + let bin = (*t - min).num_weeks() as usize; + data[bin].1 += 1.; + if data[bin].1 > y_max { + y_max = data[bin].1; + } + if let Some(x) = authors_data.get_mut(&crate::author_display(c, false)) { + x[bin].1 += 1.; + } + } + + let x_min = min.timestamp() as f64; + let x_max = Utc::now().timestamp() as f64; + + for (_name, author_d) in authors_data.iter_mut() { + let mut min_mut = min; + for (x, _) in author_d.iter_mut() { + *x = min_mut.timestamp() as f64; + min_mut += Duration::weeks(1); + } + } + + let mut min_mut = min; + for (x, _) in data.iter_mut() { + *x = min_mut.timestamp() as f64; + min_mut += Duration::weeks(1); + } + + // prepare chart widget + let mut datasets = Vec::new(); + + for (name, author_d) in authors_data.iter() { + datasets.push( + Dataset::default() + .name(name.clone()) + .marker(symbols::Marker::Braille) + .graph_type(GraphType::Line) + .style(Style::default().fg(COLORS.choose(&mut rand::thread_rng()).cloned().unwrap())) + .data(author_d) + ); + } + if total { + datasets.push( + Dataset::default() + .name("[total]") + .marker(symbols::Marker::Braille) + .graph_type(GraphType::Line) + .style(Style::default().fg(Color::Red)) + .data(&data) + ); + } + + let x_labels = [ + min.format("%Y/%m/%d").to_string(), + Utc::now().date_naive().format("%Y/%m/%d").to_string(), + ]; + let y_labels = [ + "0".into(), format!("{}", y_max) + ]; + + let chart = Chart::new(datasets) + .block(Block::default().title("Commits")) + .x_axis(Axis::default() + .title(Span::styled("time", Style::default().fg(Color::Cyan))) + .style(Style::default().fg(Color::White)) + .bounds([x_min - 1., x_max + 1.]) + .labels(x_labels.iter().map(Span::from).collect()) + ) + .y_axis(Axis::default() + .title(Span::styled("Y Axis", Style::default().fg(Color::Cyan))) + .style(Style::default().fg(Color::White)) + .bounds([-0.1, y_max + 0.1]) + .labels(y_labels.iter().map(Span::from).collect()) + ); + + // setup terminal + enable_raw_mode()?; + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; + + // draw chart and keep it for 3s + terminal.draw(|f| f.render_widget(chart, f.size()))?; + event::read().unwrap(); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + )?; + terminal.show_cursor()?; + + Ok(()) +}