//! Shared commit formats and short reference decorations for log and show. use crate::{Result, error}; use gix::bstr::ByteSlice; pub(crate) fn decorations(commit: &gix::Commit<'_>) -> Result { let repo = commit.repo; let head = crate::head_info(repo)?; let mut labels = Vec::new(); for reference in repo .references() .map_err(|e| error("refs", e))? .all() .map_err(|e| error("refs", e))? { let mut reference = reference.map_err(|e| error("refs", e))?; let name = reference.name().as_bstr().to_str_lossy().into_owned(); if reference .peel_to_id() .is_ok_and(|id| id.detach() == commit.id) { let short = reference.name().shorten().to_string(); labels.push(if head.branch.as_deref() == Some(name.as_ref()) { format!("HEAD -> {short}") } else if name.starts_with("refs/tags/") { format!("tag: {short}") } else { short }); } } if head.branch.is_none() && repo .rev_parse_single("HEAD") .is_ok_and(|id| id.detach() == commit.id) { labels.push("HEAD".into()); } labels.sort_by(|a, b| { b.starts_with("HEAD") .cmp(&a.starts_with("HEAD")) .then_with(|| b.starts_with("tag: ").cmp(&a.starts_with("tag: "))) .then_with(|| b.cmp(a)) }); Ok(if labels.is_empty() { String::new() } else { format!(" ({})", labels.join(", ")) }) } fn relative(seconds: i64) -> String { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let mut diff = now - seconds; if diff < 0 { return "in the future".into(); } let (count, unit) = if diff < 90 { (diff, "second") } else { diff = (diff + 30) / 60; if diff < 90 { (diff, "minute") } else { diff = (diff + 30) / 60; if diff < 36 { (diff, "hour") } else { diff = (diff + 12) / 24; if diff < 14 { (diff, "day") } else if diff < 70 { ((diff + 3) / 7, "week") } else if diff < 365 { ((diff + 15) / 30, "month") } else if diff < 1825 { let total_months = (diff * 12 + 183) / 365; let years = total_months / 12; let months = total_months % 12; if months != 0 { return format!( "{years} year{}, {months} month{} ago", if years == 1 { "" } else { "s" }, if months == 1 { "" } else { "s" } ); } (years, "year") } else { ((diff + 183) / 365, "year") } } } }; format!("{count} {unit}{} ago", if count == 1 { "" } else { "s" }) } pub(crate) fn format(commit: &gix::Commit<'_>, template: &str) -> Result { let decoded = commit.decode().map_err(|e| error("format", e))?; let message = decoded.message.to_str_lossy(); let author = commit.author().map_err(|e| error("author", e))?; let committer = commit.committer().map_err(|e| error("committer", e))?; let at = author.time().map_err(|e| error("date", e))?; let ct = committer.time().map_err(|e| error("date", e))?; let parents = decoded .parents .iter() .map(ToString::to_string) .collect::>(); let tree = decoded.tree.to_string(); let id = commit.id.to_string(); let fields = [ ("%H", id.clone()), ("%h", id[..7].into()), ("%P", parents.join(" ")), ( "%p", parents .iter() .map(|p| &p[..7]) .collect::>() .join(" "), ), ("%T", tree.clone()), ("%t", tree[..7].into()), ("%an", author.name.to_string()), ("%ae", author.email.to_string()), ("%ad", at.format_or_unix(gix::date::time::format::DEFAULT)), ("%ar", relative(at.seconds)), ("%cn", committer.name.to_string()), ("%ce", committer.email.to_string()), ("%cd", ct.format_or_unix(gix::date::time::format::DEFAULT)), ("%cr", relative(ct.seconds)), ("%s", message.lines().next().unwrap_or("").into()), ( "%b", message.split_once("\n\n").map_or("", |(_, b)| b).into(), ), ("%B", message.to_string()), ("%d", decorations(commit)?), ("%n", "\n".into()), ("%%", "%".into()), ]; let expanded = crate::plumbing::format(template, &message); let mut rest = expanded.as_str(); let mut text = String::new(); while !rest.is_empty() { if let Some((key, value)) = fields.iter().find(|(key, _)| rest.starts_with(key)) { text.push_str(value); rest = &rest[key.len()..]; } else { let ch = rest.chars().next().unwrap(); text.push(ch); rest = &rest[ch.len_utf8()..]; } } Ok(text) }