use colored::Colorize;
use crate::git;
pub fn run(story: bool) {
if story {
run_story();
} else {
run_normal();
}
}
fn run_normal() {
let entries = git::recent_log(15);
if entries.is_empty() {
println!();
println!(" {} No commits yet.", "⚠".yellow());
println!();
return;
}
println!();
println!(" {}:", "Your recent work".bold());
println!();
for entry in &entries {
let icon = if entry.is_merge {
"🔵"
} else {
"🟢"
};
println!(
" {} {:<40} {}",
icon,
entry.subject.bold(),
format!("({})", entry.relative_time).dimmed()
);
}
println!();
println!(
" {}",
"Tip: Run `g log --story` to see what changed in plain English".dimmed()
);
println!();
}
fn run_story() {
let entries = git::recent_log(20);
if entries.is_empty() {
println!();
println!(" {} No commits yet.", "⚠".yellow());
println!();
return;
}
let mut chronological: Vec<&git::LogEntry> = entries.iter().collect();
chronological.reverse();
println!();
println!(" {}:", "Project story".bold());
println!();
let mut current_group: Vec<&git::LogEntry> = vec![];
let mut groups: Vec<(bool, Vec<&git::LogEntry>)> = vec![];
for entry in &chronological {
if entry.is_merge && !current_group.is_empty() {
groups.push((false, current_group.clone()));
groups.push((true, vec![entry]));
current_group = vec![];
} else {
current_group.push(entry);
}
}
if !current_group.is_empty() {
groups.push((false, current_group));
}
for (is_merge_group, group) in &groups {
if *is_merge_group {
let subject = &group[0].subject;
println!(" Then, you {}.", humanize_merge(subject));
println!();
continue;
}
if group.len() > 1 {
if let Some(theme) = detect_theme(group) {
println!(" You worked on {}:", theme.bold());
} else {
println!(" You made some changes:");
}
println!();
for entry in group {
println!(" - {}", humanize_commit(&entry.subject));
}
println!();
} else {
println!(" - {}", humanize_commit(&group[0].subject));
}
}
let branch = git::current_branch();
let latest = &entries[0].subject;
println!(" {}:", "Current focus".bold());
println!(" Branch {} — last: \"{}\"", branch.cyan(), latest);
println!();
}
fn humanize_commit(subject: &str) -> String {
let s = subject.trim();
let mut chars = s.chars();
match chars.next() {
Some(c) => {
let first = c.to_uppercase().to_string();
format!("{}{}", first, chars.as_str())
}
None => s.to_string(),
}
}
fn humanize_merge(subject: &str) -> String {
let s = subject.trim().to_lowercase();
if s.starts_with("merge branch") {
let branch = s
.strip_prefix("merge branch ")
.unwrap_or(&s)
.trim_matches('\'')
.trim_matches('"');
format!("merged branch '{}' into your current branch", branch)
} else if s.starts_with("merge pull request") {
"merged a pull request".to_string()
} else {
format!("merged: {}", s)
}
}
fn detect_theme(group: &[&git::LogEntry]) -> Option<String> {
let subjects: Vec<String> = group.iter().map(|e| e.subject.to_lowercase()).collect();
let keywords = [
("auth", "authentication"),
("login", "login flow"),
("fix", "bug fixes"),
("test", "tests"),
("refactor", "refactoring"),
("ui", "UI improvements"),
("style", "styling"),
("docs", "documentation"),
("setup", "project setup"),
("init", "project initialization"),
("config", "configuration"),
("api", "API work"),
("db", "database changes"),
("deploy", "deployment"),
];
for (keyword, theme) in &keywords {
let matches = subjects.iter().filter(|s| s.contains(keyword)).count();
if matches > subjects.len() / 2 {
return Some(theme.to_string());
}
}
None
}