use std::io::Write;
use anyhow::{Context, Result};
use gix::bstr::ByteSlice;
use gix::prelude::ObjectIdExt;
use crate::context::CommandContext;
use crate::pager::Pager;
use git_meta_lib::types::{Target, TargetType};
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
const YELLOW: &str = "\x1b[33m";
const GREEN: &str = "\x1b[32m";
const CYAN: &str = "\x1b[36m";
const BLUE: &str = "\x1b[34m";
pub fn run(
start_ref: Option<&str>, count: usize, metadata_only: bool, ) -> Result<()> {
let ctx = CommandContext::open(None)?;
let repo = ctx.session.repo();
let mut out = Pager::start(Some(repo));
write_log(&mut out, &ctx, start_ref, count, metadata_only)
}
fn write_log<W: Write>(
out: &mut W,
ctx: &CommandContext,
start_ref: Option<&str>,
count: usize,
metadata_only: bool,
) -> Result<()> {
let repo = ctx.session.repo();
let start_oid = resolve_start(repo, start_ref)?;
let walk = repo.rev_walk(Some(start_oid));
let iter = walk.all()?;
let mut printed = 0usize;
for info_result in iter {
if printed >= count {
break;
}
let info = info_result.context("error walking commits")?;
let oid = info.id;
let commit_obj = oid.attach(repo).object()?.into_commit();
let sha = oid.to_string();
let entries = ctx
.session
.store()
.get_all(
&Target::from_parts(TargetType::Commit, Some(sha.clone())),
None,
)
.unwrap_or_default();
let meta: Vec<(String, String)> = entries
.into_iter()
.map(|entry| {
let raw = serde_json::from_str::<String>(&entry.value).unwrap_or(entry.value);
(entry.key, raw)
})
.collect();
if metadata_only && meta.is_empty() {
continue;
}
if printed > 0 {
writeln!(out)?;
}
printed += 1;
let short_sha = &sha[..10];
let decoded = commit_obj.decode()?;
let author_name = decoded
.author()
.map_err(|e| anyhow::anyhow!("{e}"))?
.name
.to_str_lossy();
let author_email = decoded
.author()
.map_err(|e| anyhow::anyhow!("{e}"))?
.email
.to_str_lossy();
writeln!(
out,
"{YELLOW}commit {short_sha}{RESET} {DIM}---{RESET} \
{GREEN}{author_name}{RESET} {DIM}<{author_email}>{RESET}"
)?;
let message = decoded.message.to_str_lossy();
let message = message.trim().to_string();
let nonempty_lines: Vec<&str> = message.lines().filter(|l| !l.trim().is_empty()).collect();
let shown = nonempty_lines.len().min(4);
for line in &nonempty_lines[..shown] {
writeln!(out, " {line}")?;
}
if nonempty_lines.len() > 4 {
let extra = nonempty_lines.len() - 4;
writeln!(out, " {DIM}... ({extra} more lines){RESET}")?;
}
if !meta.is_empty() {
writeln!(out, " {CYAN}--- metadata ---{RESET}")?;
for (key, value) in &meta {
let preview = format_value_preview(value);
writeln!(out, " {BLUE}|{RESET} {BOLD}{key}{RESET} {preview}")?;
}
writeln!(out, " {BLUE}.{RESET}")?;
}
}
if printed == 0 {
if metadata_only {
writeln!(out, "No commits with metadata found.")?;
} else {
writeln!(out, "No commits found.")?;
}
}
Ok(())
}
fn resolve_start(repo: &gix::Repository, start_ref: Option<&str>) -> Result<gix::ObjectId> {
let spec = start_ref.unwrap_or("HEAD");
let obj = repo
.rev_parse_single(spec)
.with_context(|| format!("could not resolve ref '{spec}'"))?;
let commit = obj.object()?.peel_tags_to_end()?.into_commit();
Ok(commit.id().detach())
}
fn format_value_preview(value: &str) -> String {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(value) {
if let Some(arr) = json.as_array() {
return format!("{CYAN}[list: {} items]{RESET}", arr.len());
}
if let Some(obj) = json.as_object() {
return format!("{CYAN}{{object: {} keys}}{RESET}", obj.len());
}
}
let first_line = value.lines().next().unwrap_or("");
let has_more_lines = value.contains('\n') && value.trim_end_matches('\n') != first_line;
let mut preview = if first_line.len() > 50 {
format!("{}...", &first_line[..50])
} else {
first_line.to_string()
};
if has_more_lines && first_line.len() <= 50 {
preview.push_str(" ...");
}
format!("{DIM}{preview}{RESET}")
}