use clap::Args as ClapArgs;
use std::path::Path;
use crate::annotation::Kind;
use crate::cli::output;
use crate::cli::span_context;
use crate::compact::filter_superseded;
use crate::qual_file::{self, find_project_root};
#[derive(ClapArgs)]
pub struct Args {
pub artifact: String,
#[arg(long, default_value = "human")]
pub format: String,
#[arg(long)]
pub no_ignore: bool,
#[arg(long)]
pub pretty: bool,
#[arg(long)]
pub all: bool,
#[arg(long = "type", value_name = "TYPE")]
pub record_type: Option<String>,
}
pub fn run(args: Args) -> crate::Result<()> {
let root = find_project_root(Path::new("."));
let discover_root = root.as_deref().unwrap_or(Path::new("."));
let all_qual_files = qual_file::discover(discover_root, !args.no_ignore)?;
let records = qual_file::find_records_for(&args.artifact, &all_qual_files);
if records.is_empty() {
return Err(crate::Error::Validation(format!(
"No records found for '{}'",
args.artifact
)));
}
let owned_records: Vec<crate::annotation::Record> =
records.iter().map(|r| (*r).clone()).collect();
let mut display_records: Vec<crate::annotation::Record> = if args.all {
owned_records.clone()
} else {
let active = filter_superseded(&owned_records);
active
.into_iter()
.filter(|r| r.kind() != Some(&Kind::Resolve))
.cloned()
.collect()
};
if let Some(ref type_filter) = args.record_type {
display_records.retain(|r| r.record_type() == type_filter);
}
if args.format == "json" {
if args.pretty {
let mut value: serde_json::Value =
serde_json::from_str(&output::show_json(&args.artifact, &display_records))?;
if let Some(records_arr) = value["records"].as_array_mut() {
for rec_val in records_arr.iter_mut() {
if let Some(span_val) = rec_val.get("body").and_then(|b| b.get("span"))
&& !span_val.is_null()
&& let Some(record) = display_records
.iter()
.find(|r| rec_val.get("id").and_then(|v| v.as_str()) == Some(r.id()))
&& let Some(att) = record.as_annotation()
&& let Some(ref span) = att.body.span
{
let ctx = span_context::read_span_context(
Path::new(&args.artifact),
span,
span_context::DEFAULT_CONTEXT_LINES,
);
rec_val["context"] = span_context::to_json(&ctx);
}
}
}
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
println!("{}", output::show_json(&args.artifact, &display_records));
}
return Ok(());
}
println!();
println!(" {}", args.artifact);
let display_ids: std::collections::HashSet<&str> =
display_records.iter().map(|r| r.id()).collect();
let mut children: std::collections::HashMap<&str, Vec<&crate::annotation::Record>> =
std::collections::HashMap::new();
let mut roots: Vec<&crate::annotation::Record> = Vec::new();
for record in &display_records {
let parent_id = record
.as_annotation()
.and_then(|a| a.body.references.as_deref());
if let Some(pid) = parent_id
&& display_ids.contains(pid)
{
children.entry(pid).or_default().push(record);
} else {
roots.push(record);
}
}
println!();
println!(" Records ({}):", display_records.len());
for (i, record) in roots.iter().enumerate() {
if i > 0 {
println!();
}
print_record(record, " ", " ", &args, &children);
}
println!();
Ok(())
}
fn print_record(
record: &crate::annotation::Record,
line_prefix: &str,
cont_prefix: &str,
args: &Args,
children: &std::collections::HashMap<&str, Vec<&crate::annotation::Record>>,
) {
if let Some(att) = record.as_annotation() {
let date = att.created_at.format("%Y-%m-%d");
let issuer_short = att
.issuer
.strip_prefix("mailto:")
.and_then(|e| e.split('@').next())
.unwrap_or(&att.issuer);
let id_short = &att.id[..8.min(att.id.len())];
println!(
"{line_prefix}{} {:?} {} {} {}",
att.body.kind, att.body.summary, issuer_short, date, id_short,
);
if args.pretty
&& let Some(ref span) = att.body.span
{
let ctx = span_context::read_span_context(
Path::new(&args.artifact),
span,
span_context::DEFAULT_CONTEXT_LINES,
);
if let Some(ref warning) = ctx.warning {
println!("{cont_prefix} note: {warning}");
}
let formatted = span_context::format_human(&ctx);
if !formatted.is_empty() {
for line in formatted.lines() {
println!("{cont_prefix}{line}");
}
}
}
} else if let Some(epoch) = record.as_epoch() {
let date = epoch.created_at.format("%Y-%m-%d");
let id_short = &epoch.id[..8.min(epoch.id.len())];
println!(
"{line_prefix}epoch {:?} {} {} {}",
epoch.body.summary, epoch.issuer, date, id_short,
);
} else if matches!(record, crate::annotation::Record::Dependency(_)) {
return;
} else {
let id = record.id();
let id_short = &id[..8.min(id.len())];
let type_str = record.record_type();
let type_display = if type_str.is_empty() {
"<unknown>"
} else {
type_str
};
println!("{line_prefix}{type_display} {id_short}");
}
if let Some(replies) = children.get(record.id()) {
for (i, reply) in replies.iter().enumerate() {
let is_last = i == replies.len() - 1;
let branch = if is_last {
"\u{2514}\u{2500} "
} else {
"\u{251c}\u{2500} "
};
let continuation = if is_last { " " } else { "\u{2502} " };
let child_line = format!("{cont_prefix}{branch}");
let child_cont = format!("{cont_prefix}{continuation}");
print_record(reply, &child_line, &child_cont, args, children);
}
}
}