use crate::git::load::{DECORATE, RefKind, RefLabel, fit_refs, parse_refs};
use crate::git::{SEP, git_capture};
use crate::tui::widgets::MIN_SUBJECT;
use colored::Colorize;
const NAME_MAX: usize = 18;
#[derive(clap::Args)]
pub struct Args {
#[arg(short = 'n', long, default_value_t = 30)]
pub number: usize,
#[arg(short, long)]
pub all: bool,
#[arg(short, long)]
pub graph: bool,
}
pub fn run(args: Args) {
if args.graph {
graph_log(args.all, args.number);
} else {
pretty_log(args.all, args.number);
}
}
const REC: char = '\u{1e}';
struct Row {
hash: String,
date: String,
rel: String,
committer: String,
refs: Vec<RefLabel>,
subject: String,
}
fn pretty_log(all: bool, limit: usize) {
let n = limit.to_string();
let fmt = format!("--pretty=format:%h{SEP}%ad{SEP}%ar{SEP}%cn{SEP}%D{SEP}%s");
let mut args = vec![
"log",
"-n",
&n,
"--date=format:%Y-%m-%d %H:%M",
DECORATE,
&fmt,
];
if all {
args.push("--all");
}
let out = match git_capture(".", &args) {
Some(o) if !o.is_empty() => o,
_ => {
eprintln!("{}", "no commits (or not a git repo)".dimmed());
return;
}
};
let rows: Vec<Row> = out.lines().filter_map(parse_row).collect();
let rel_w = rel_width(rows.iter());
let width = term_width();
for r in &rows {
print_row("", r, rel_w, width);
}
}
fn graph_log(all: bool, limit: usize) {
let n = limit.to_string();
let fmt = format!("--pretty=format:{REC}%h{SEP}%ad{SEP}%ar{SEP}%cn{SEP}%D{SEP}%s");
let mut args = vec![
"log",
"--graph",
"--color=never",
"--date=format:%Y-%m-%d %H:%M",
DECORATE,
"-n",
&n,
&fmt,
];
if all {
args.push("--all");
}
let out = match git_capture(".", &args) {
Some(o) if !o.is_empty() => o,
_ => {
eprintln!("{}", "no commits (or not a git repo)".dimmed());
return;
}
};
let parsed: Vec<(String, Option<Row>)> = out
.lines()
.map(|line| match line.find(REC) {
Some(i) => (
line[..i].to_string(),
parse_row(&line[i + REC.len_utf8()..]),
),
None => (line.to_string(), None),
})
.collect();
let rel_w = rel_width(parsed.iter().filter_map(|(_, r)| r.as_ref()));
let width = term_width();
for (rail, row) in &parsed {
match row {
Some(r) => print_row(rail, r, rel_w, width),
None => println!("{rail}"),
}
}
}
fn parse_row(line: &str) -> Option<Row> {
let mut f = line.split(SEP);
Some(Row {
hash: f.next()?.to_string(),
date: f.next()?.to_string(),
rel: f.next()?.to_string(),
committer: f.next()?.to_string(),
refs: parse_refs(f.next()?),
subject: f.next().unwrap_or("").to_string(),
})
}
fn rel_width<'a>(rows: impl Iterator<Item = &'a Row>) -> usize {
rows.map(|r| r.rel.chars().count() + 2).max().unwrap_or(0)
}
fn print_row(rail: &str, r: &Row, rel_w: usize, width: usize) {
let who = truncate(&r.committer, NAME_MAX);
let hash = format!("{:<7}", r.hash);
let left = rail.chars().count()
+ hash.chars().count()
+ 2
+ 16
+ 2
+ rel_w
+ 2
+ who.chars().count()
+ 3;
let (refs, refs_w) = ref_labels(&r.refs, width.saturating_sub(left + MIN_SUBJECT));
let subject = truncate(&r.subject, width.saturating_sub(left + refs_w).max(10));
let rel_token = format!("({})", r.rel);
let rel = format!("{rel_token:>rel_w$}");
let who = format!("<{who}>");
println!(
"{rail}{} {} {} {} {refs}{}",
hash.yellow(),
r.date.green(),
rel.magenta(),
who.blue(),
subject
);
}
fn ref_labels(refs: &[RefLabel], room: usize) -> (String, usize) {
if refs.is_empty() {
return (String::new(), 0);
}
let (kept, hidden) = fit_refs(refs, room);
let mut width = 3 + 2 * kept.len().saturating_sub(1);
let mut painted: Vec<String> = kept
.into_iter()
.map(|(kind, text)| {
width += text.chars().count();
match kind {
RefKind::Head | RefKind::Detached => text.cyan(),
RefKind::Branch => text.green(),
RefKind::Remote => text.red(),
RefKind::Tag => text.yellow(),
}
.bold()
.to_string()
})
.collect();
if hidden > 0 {
let more = format!("+{hidden}");
width += 2 + more.len();
painted.push(more.dimmed().to_string());
}
(format!("({}) ", painted.join(", ")), width)
}
fn term_width() -> usize {
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(100)
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let kept: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{kept}…")
}