Files
sausage/userland/git/src/pretty.rs
T

158 lines
5.3 KiB
Rust

//! 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<String> {
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<String> {
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::<Vec<_>>();
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::<Vec<_>>()
.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)
}