Something went wrong. Try again.
A human-friendly DSL for ATProto Lexicons forked from stavola.xyz/mlf
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687//! `mlf diff [NSID]` — print a unified diff between the local Lexicon//! JSON and the currently-published record for one NSID (or every//! changed / new / removed NSID in the package).
use crate::remote_state::{Classification, RemoteState, RemoteStateError, classify};use similar::{ChangeTag, TextDiff};
pub async fn run_diff(nsid: Option<String>) -> Result<(), RemoteStateError> { let state = RemoteState::load().await?; let classifications = classify(&state);
let targets: Vec<String> = match nsid { Some(ref n) => vec![n.clone()], None => classifications .iter() .filter(|(_, c)| !matches!(c, Classification::Unchanged)) .map(|(n, _)| n.clone()) .collect(), };
if targets.is_empty() { println!("No differences."); return Ok(()); }
for (i, nsid) in targets.iter().enumerate() { if i > 0 { println!(); } let cls = classifications.get(nsid); print_diff(&state, nsid, cls); }
Ok(())}
fn print_diff(state: &RemoteState, nsid: &str, cls: Option<&Classification>) { let local_json = state .local .get(nsid) .map(|l| pretty_json(&l.record_json)) .unwrap_or_default(); let remote_json = state .remote .get(nsid) .map(|r| pretty_json(&r.record_json)) .unwrap_or_default();
let header = match cls { Some(Classification::Unchanged) => format!("= {nsid} (unchanged)"), Some(Classification::New) => format!("+ {nsid} (new, not yet on PDS)"), Some(Classification::Removed) => format!("- {nsid} (removed locally, still on PDS)"), Some(Classification::Changed { local_cid, remote_cid, }) => format!("~ {nsid} {remote_cid} → {local_cid}"), None => format!("? {nsid} (not found locally or remotely)"), }; println!("{header}"); println!("{}", "-".repeat(header.chars().count()));
let diff = TextDiff::from_lines(&remote_json, &local_json); for change in diff.iter_all_changes() { let (sign, text) = match change.tag() { ChangeTag::Delete => ("-", change.value()), ChangeTag::Insert => ("+", change.value()), ChangeTag::Equal => (" ", change.value()), }; // Trim trailing newline — `value()` returns `"line\n"` from the // source; we supply our own newline via println. let text = text.strip_suffix('\n').unwrap_or(text); println!("{sign} {text}"); }}
fn pretty_json(value: &serde_json::Value) -> String { // `to_string_pretty` preserves insertion order; serde_json uses // `preserve_order` via the `IndexMap` feature by default in this // workspace? If not, key order is lexicographic. Either is fine for // a human-facing diff; the CID comparison happens elsewhere. let mut s = serde_json::to_string_pretty(value).unwrap_or_default(); if !s.ends_with('\n') { s.push('\n'); } s}