use std::io::{self, Write};
use serde::Serialize;
use super::*;
#[derive(Serialize)]
struct LogRecord<'a> {
cid: String,
time: u64,
timestamp: String,
author: &'a str,
description: &'a str,
parents: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
task_id: Option<&'a str>,
}
#[derive(clap::ValueEnum, Clone, Debug)]
pub(crate) enum Format {
Human,
Json,
}
#[derive(clap::Args, Debug)]
#[command(after_long_help = "\
Examples:
mnem log # last 20 ops (default, human form)
mnem log -n 5 # last 5 ops
mnem log --oneline # short-cid + message per line
mnem log --format=json | jq . # JSON Lines, pipe through jq
mnem log --format=json -n 100 # agent-facing op stream
")]
pub(crate) struct Args {
#[arg(long, short = 'n', default_value_t = 20)]
pub limit: usize,
#[arg(long)]
pub oneline: bool,
#[arg(long, value_enum, default_value = "human")]
pub format: Format,
}
pub(crate) fn run(override_path: Option<&Path>, args: Args) -> Result<()> {
let (_dir, r, bs, _ohs) = repo::open_all(override_path)?;
let stdout = io::stdout();
let mut w = stdout.lock();
let mut cur = r.op_id().clone();
for i in 0..args.limit {
let bytes = bs
.get(&cur)?
.ok_or_else(|| anyhow!("op {cur} missing from store"))?;
let op: Operation = from_canonical_bytes(&bytes)?;
if args.oneline {
let full = cur.to_string();
let short = short_cid(&full);
writeln!(w, "{short} {}", op.description)?;
} else {
match args.format {
Format::Json => write_json_record(&mut w, &cur, &op)?,
Format::Human => write_human_record(&mut w, &cur, &op)?,
}
}
match op.parents.first() {
Some(p) => cur = p.clone(),
None => {
let _ = i;
break;
}
}
}
Ok(())
}
fn write_human_record(w: &mut impl Write, cid: &mnem_core::id::Cid, op: &Operation) -> Result<()> {
writeln!(w, "op {cid}")?;
writeln!(w, " time {}us", op.time)?;
if !op.author.is_empty() {
writeln!(w, " author {}", op.author)?;
}
if let Some(agent) = &op.agent_id {
writeln!(w, " agent {agent}")?;
}
if let Some(task) = &op.task_id {
writeln!(w, " task {task}")?;
}
writeln!(w, " message {}", op.description)?;
writeln!(w)?;
Ok(())
}
fn write_json_record(w: &mut impl Write, cid: &mnem_core::id::Cid, op: &Operation) -> Result<()> {
let record = LogRecord {
cid: cid.to_string(),
time: op.time,
timestamp: micros_to_rfc3339(op.time),
author: &op.author,
description: &op.description,
parents: op.parents.iter().map(ToString::to_string).collect(),
agent_id: op.agent_id.as_deref(),
task_id: op.task_id.as_deref(),
};
let line = serde_json::to_string(&record).context("serialising log record")?;
writeln!(w, "{line}")?;
Ok(())
}
fn micros_to_rfc3339(micros: u64) -> String {
use std::time::{Duration, UNIX_EPOCH};
let secs = micros / 1_000_000;
let nanos = ((micros % 1_000_000) * 1_000) as u32;
match UNIX_EPOCH.checked_add(Duration::new(secs, nanos)) {
Some(_t) => {
let s = secs % 60;
let m = (secs / 60) % 60;
let h = (secs / 3600) % 24;
let days = secs / 86400;
let (year, month, day) = days_to_ymd(days);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
year,
month,
day,
h,
m,
s,
micros % 1_000_000,
)
}
None => micros.to_string(),
}
}
fn days_to_ymd(days: u64) -> (u64, u8, u8) {
let z = days as i64 + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u64, m as u8, d as u8)
}
fn short_cid(full: &str) -> String {
if full.len() <= 10 {
full.to_string()
} else {
full.chars().skip(2).take(8).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn check(days: u64, expected_year: u64, expected_month: u8, expected_day: u8) {
let (y, m, d) = days_to_ymd(days);
assert_eq!(
(y, m, d),
(expected_year, expected_month, expected_day),
"days_to_ymd({days}) = ({y},{m},{d}), want ({expected_year},{expected_month},{expected_day})"
);
}
#[test]
fn epoch() {
check(0, 1970, 1, 1);
}
#[test]
fn epoch_plus_one() {
check(1, 1970, 1, 2);
}
#[test]
fn start_of_february_1970() {
check(31, 1970, 2, 1);
}
#[test]
fn start_of_march_1970_non_leap() {
check(59, 1970, 3, 1);
}
#[test]
fn second_year() {
check(365, 1971, 1, 1);
}
#[test]
fn year_2000_leap_day() {
check(11016, 2000, 2, 29);
}
#[test]
fn year_2000_day_before_leap() {
check(11015, 2000, 2, 28);
}
#[test]
fn year_2000_day_after_leap() {
check(11017, 2000, 3, 1);
}
#[test]
fn year_2024_leap_day() {
check(19782, 2024, 2, 29);
}
#[test]
fn year_1972_leap_day() {
check(789, 1972, 2, 29);
}
#[test]
fn year_2024_day_before_leap() {
check(19781, 2024, 2, 28);
}
#[test]
fn year_2024_day_after_leap() {
check(19783, 2024, 3, 1);
}
#[test]
fn december_year_end_1970() {
check(364, 1970, 12, 31);
}
#[test]
fn year_2100_is_not_leap_feb_28() {
check(47540, 2100, 2, 28);
}
#[test]
fn year_2100_is_not_leap_next_day_is_march() {
check(47541, 2100, 3, 1);
}
#[test]
fn year_2100_start() {
check(47482, 2100, 1, 1);
}
}