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
This commit is contained in:
parent
5fbd1e95cd
commit
5a35e6b04c
3 changed files with 187 additions and 0 deletions
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -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"] }
|
39
src/main.rs
Normal file
39
src/main.rs
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
135
src/tui.rs
Normal file
135
src/tui.rs
Normal file
|
@ -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<Oid, (DateTime<Utc>, Commit)>,
|
||||||
|
total: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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<String> = commits.values().map(|(_t, c)| crate::author_display(c, false)).collect();
|
||||||
|
let mut authors_data : HashMap<String,Vec<(f64, f64)>> = 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(())
|
||||||
|
}
|
Loading…
Reference in a new issue