use crate::anchor::Anchor;
use crate::args::Args;
use crate::brief::clip;
use crate::failure::R;
use crate::glob;
use crate::model::{Node, Tree, Vivac};
use crate::output::outln;
use crate::render::print_json;
use serde_json::json;
const SHOWN: usize = 20;
struct Verdict<'a> {
file: String,
times: usize,
claimed_by: Vec<&'a Node>,
}
impl Verdict<'_> {
fn claimed_and_open(&self) -> bool {
self.claimed_by.iter().any(|n| n.state.is_open())
}
}
fn plural(n: usize, one: &str, many: &str) -> String {
if n == 1 {
format!("{n} {one}")
} else {
format!("{n} {many}")
}
}
fn reference<'a>(a: &'a Tree, args: &Args) -> Result<Option<&'a Vivac>, crate::failure::Failure> {
match args.opt("since") {
Some(s) => a
.vivac(s)
.map(Some)
.ok_or_else(|| crate::failure::Failure::usage(format!("No such vivac: {s}."))),
None => Ok(a.vivacs.last()),
}
}
pub fn reconcile(a: &Tree, anchor: &dyn Anchor, args: &Args) -> R {
let Some(since) = reference(a, args)? else {
outln!();
outln!(" No stop to measure from: this tree has no vivacs yet.");
outln!();
outln!(" vivac save \"<label>\"");
outln!();
return Ok(());
};
if since.anchor.is_empty_tree() {
outln!();
outln!(
" {} has no anchor, so there is no history to read.",
since.alias()
);
outln!(" Without version control the tree cannot be contradicted; that is");
outln!(" the floor of the product and not a failure.");
outln!();
return Ok(());
}
let changes: Vec<crate::anchor::Change> = anchor
.changed_since(&since.anchor)
.into_iter()
.filter(|c| !c.file_path.replace('\\', "/").starts_with(".vivac/"))
.collect();
let governing: Vec<&Node> = a
.nodes_iter()
.filter(|n| !n.governs(a).is_empty())
.collect();
let mut verdicts: Vec<Verdict> = changes
.iter()
.map(|c| {
let mut claimed_by: Vec<&Node> = governing
.iter()
.filter(|n| n.governs(a).iter().any(|g| glob::covers(g, &c.file_path)))
.copied()
.collect();
claimed_by.sort_by_key(|n| (!n.state.is_open(), n.num));
Verdict {
file: c.file_path.clone(),
times: c.times,
claimed_by,
}
})
.collect();
verdicts.sort_by(|x, y| y.times.cmp(&x.times).then_with(|| x.file.cmp(&y.file)));
let unclaimed: Vec<&Verdict> = verdicts
.iter()
.filter(|v| v.claimed_by.is_empty())
.collect();
let stale: Vec<&Verdict> = verdicts
.iter()
.filter(|v| !v.claimed_by.is_empty() && !v.claimed_and_open())
.collect();
let live: Vec<&Verdict> = verdicts.iter().filter(|v| v.claimed_and_open()).collect();
if args.has("json") {
let one = |v: &Verdict| {
json!({
"file": v.file,
"changes": v.times,
"claimed_by": v.claimed_by.iter().map(|n| json!({
"alias": n.alias(),
"title": n.title(a),
"state": n.state,
})).collect::<Vec<_>>(),
})
};
return print_json(json!({
"since": since.alias(),
"since_ts": since.ts,
"anchor": since.anchor.short(),
"governing_nodes": governing.len(),
"changed": verdicts.len(),
"unclaimed": unclaimed.iter().map(|v| one(v)).collect::<Vec<_>>(),
"claimed_by_closed_work": stale.iter().map(|v| one(v)).collect::<Vec<_>>(),
"claimed_and_open": live.iter().map(|v| one(v)).collect::<Vec<_>>(),
}));
}
outln!();
outln!(
" RECONCILE - since {} {}, {}",
since.alias(),
since.anchor.short(),
plural(verdicts.len(), "file changed", "files changed")
);
if verdicts.is_empty() {
outln!();
outln!(" Nothing changed. The tree and the work agree.");
outln!();
return Ok(());
}
if governing.is_empty() {
outln!();
outln!(" No node declares what it governs, so nothing here can be claimed.");
outln!(" Until some node says which files it owns, this command has nothing");
outln!(" to compare the work against.");
outln!();
outln!(" vivac push \"<title>\" --why \"<reason>\" --governs \"src/auth/**\"");
outln!();
return Ok(());
}
section(
"NOBODY CLAIMS THESE",
"push \"<title>\" --governs <path>",
&unclaimed,
|_| String::new(),
);
section(
"CLAIMED ONLY BY CLOSED WORK",
"focus <id> --reopen | block <id>",
&stale,
|v| {
v.claimed_by
.iter()
.map(|n| format!("{} [{}]", n.alias(), n.state.word(n.kind)))
.collect::<Vec<_>>()
.join(" ")
},
);
if args.has("all") {
section("CLAIMED, AND THE WORK IS OPEN", "", &live, |v| {
v.claimed_by
.iter()
.filter(|n| n.state.is_open())
.map(|n| n.alias())
.collect::<Vec<_>>()
.join(" ")
});
} else if !live.is_empty() {
outln!();
outln!(
" {} under work that is open, which is what is supposed to happen. --all",
plural(live.len(), "file", "files")
);
}
if unclaimed.is_empty() && stale.is_empty() {
outln!();
outln!(" Nothing to reconcile.");
}
outln!();
Ok(())
}
fn section(title: &str, action: &str, rows: &[&Verdict], note: impl Fn(&Verdict) -> String) {
if rows.is_empty() {
return;
}
outln!();
outln!(
"{}",
format!(
" {} ({}){}{}",
title,
rows.len(),
" ".repeat(38usize.saturating_sub(title.len() + 4)),
action
)
.trim_end()
);
for v in rows.iter().take(SHOWN) {
outln!(
"{}",
format!(" {:<44} {:>3} {}", clip(&v.file, 44), v.times, note(v)).trim_end()
);
}
if rows.len() > SHOWN {
outln!(" + {} more --json", rows.len() - SHOWN);
}
}