use chrono::Utc;
pub fn id_short(id: &str) -> &str {
let end = id.len().min(8);
let mut end = end;
while end > 0 && !id.is_char_boundary(end) {
end -= 1;
}
&id[..end]
}
pub fn auto_namespace() -> String {
if let Ok(out) = std::process::Command::new("git")
.args(["remote", "get-url", "origin"])
.stderr(std::process::Stdio::null())
.output()
{
let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !url.is_empty()
&& let Some(name) = url.rsplit('/').next()
{
let name = name.trim_end_matches(".git");
if !name.is_empty() {
return name.to_string();
}
}
}
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_else(|| "global".to_string())
}
pub fn human_age(iso: &str) -> String {
let Ok(dt) = chrono::DateTime::parse_from_rfc3339(iso) else {
return iso.to_string();
};
let dur = Utc::now().signed_duration_since(dt);
if dur.num_seconds() < 60 {
return "just now".to_string();
}
if dur.num_minutes() < 60 {
return format!("{}m ago", dur.num_minutes());
}
if dur.num_hours() < 24 {
return format!("{}h ago", dur.num_hours());
}
if dur.num_days() < 30 {
return format!("{}d ago", dur.num_days());
}
format!("{}mo ago", dur.num_days() / 30)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_short_empty() {
assert_eq!(id_short(""), "");
}
#[test]
fn test_id_short_under_8() {
assert_eq!(id_short("abc"), "abc");
assert_eq!(id_short("1234567"), "1234567");
}
#[test]
fn test_id_short_exactly_8() {
assert_eq!(id_short("12345678"), "12345678");
}
#[test]
fn test_id_short_over_8() {
assert_eq!(id_short("abcdefghijklmnop"), "abcdefgh");
}
#[test]
fn test_id_short_utf8_boundary() {
let s = "abcdefgé";
let out = id_short(s);
assert!(out.len() <= 8);
assert_eq!(out, "abcdefg");
}
#[test]
fn test_human_age_just_now() {
let now = Utc::now().to_rfc3339();
assert_eq!(human_age(&now), "just now");
}
#[test]
fn test_human_age_minutes() {
let past = (Utc::now() - chrono::Duration::minutes(5)).to_rfc3339();
let age = human_age(&past);
assert!(age.ends_with("m ago"), "got: {age}");
}
#[test]
fn test_human_age_hours() {
let past = (Utc::now() - chrono::Duration::hours(3)).to_rfc3339();
let age = human_age(&past);
assert!(age.ends_with("h ago"), "got: {age}");
}
#[test]
fn test_human_age_days() {
let past = (Utc::now() - chrono::Duration::days(5)).to_rfc3339();
let age = human_age(&past);
assert!(age.ends_with("d ago"), "got: {age}");
}
#[test]
fn test_human_age_months() {
let past = (Utc::now() - chrono::Duration::days(120)).to_rfc3339();
let age = human_age(&past);
assert!(age.ends_with("mo ago"), "got: {age}");
}
#[test]
fn test_human_age_invalid_rfc3339_returns_input() {
assert_eq!(human_age("not-a-date"), "not-a-date");
assert_eq!(human_age(""), "");
}
#[test]
fn test_human_age_future_timestamp() {
let future = (Utc::now() + chrono::Duration::seconds(30)).to_rfc3339();
let out = human_age(&future);
assert!(!out.is_empty());
}
#[test]
fn test_auto_namespace_in_git_repo() {
let ns = auto_namespace();
assert!(!ns.is_empty(), "auto_namespace must return non-empty");
}
#[test]
fn test_auto_namespace_no_git_uses_dirname() {
let ns = auto_namespace();
assert!(!ns.is_empty());
}
#[test]
fn test_auto_namespace_falls_back_to_global() {
let ns = auto_namespace();
assert!(!ns.is_empty());
}
}