use std::collections::{BTreeMap, BTreeSet};
use radicle::cob::patch;
use radicle::cob::patch::{Patch, PatchId};
use radicle::patch::cache::Patches as _;
use radicle::prelude::*;
use radicle::profile::Profile;
use radicle::storage::git::Repository;
use term::format::Author;
use term::table::{Table, TableOptions};
use term::Element as _;
use crate::terminal as term;
use crate::terminal::patch as common;
use itertools::Itertools as _;
pub fn run(
filter: Option<&patch::Status>,
authors: BTreeSet<Did>,
repository: &Repository,
profile: &Profile,
) -> anyhow::Result<()> {
let patches = term::cob::patches(profile, repository)?;
let mut all = Vec::new();
let iter = match filter {
Some(status) => patches.list_by_status(status)?,
None => patches.list()?,
};
for patch in iter {
let (id, patch) = match patch {
Ok((id, patch)) => (id, patch),
Err(e) => {
log::error!(target: "cli", "Patch load error: {e}");
continue;
}
};
if !authors.is_empty() {
if !authors.contains(patch.author().id()) {
continue;
}
}
all.push((id, patch));
}
if all.is_empty() {
term::print(term::format::italic("Nothing to show."));
return Ok(());
}
let mut table = Table::<10, term::Line>::new(TableOptions {
spacing: 2,
border: Some(term::colors::FAINT),
..TableOptions::default()
});
table.header([
term::format::dim(String::from("●")).into(),
term::format::bold(String::from("ID")).into(),
term::format::bold(String::from("Title")).into(),
term::format::bold(String::from("Author")).into(),
term::Line::blank(),
term::format::bold(String::from("Reviews")).into(),
term::format::bold(String::from("Head")).into(),
term::format::bold(String::from("+")).into(),
term::format::bold(String::from("-")).into(),
term::format::bold(String::from("Updated")).into(),
]);
table.divider();
let me = *profile.id();
all.sort_by(|(id1, p1), (id2, p2)| {
let is_me = (p2.author().id().as_key() == &me).cmp(&(p1.author().id().as_key() == &me));
let by_id = id1.cmp(id2);
let by_rev_time = p2.updated_at().cmp(&p1.updated_at());
is_me.then(by_rev_time).then(by_id)
});
let (rows, errors): (Vec<_>, Vec<_>) = all
.iter()
.map(|(id, patch)| {
row(id, patch, repository, profile).map_err(|e| (patch.title(), id, e.to_string()))
})
.partition_result();
table.extend(rows);
table.print();
if !errors.is_empty() {
for (title, id, error) in errors {
term::error(format!("patch {title:?} ({id}) failed to load: {error}",));
}
}
Ok(())
}
pub fn row(
id: &PatchId,
patch: &Patch,
repository: &Repository,
profile: &Profile,
) -> anyhow::Result<[term::Line; 10]> {
let state = patch.state();
let (_, revision) = patch.latest();
let (from, to) = revision.range();
let stats = common::diff_stats(repository.raw(), &from, &to)?;
let author = patch.author().id;
let (alias, did) = Author::new(&author, profile, false).labels();
let mut delegates = repository
.delegates()?
.into_iter()
.map(|delegate| (*delegate, term::format::patch::verdict(None)))
.collect::<BTreeMap<_, _>>();
for (key, review) in revision.reviews() {
delegates
.entry(*key)
.and_modify(|verdict| *verdict = term::format::patch::verdict(review.verdict()));
}
let n = delegates.len();
let reviews = delegates
.values()
.cloned()
.map(term::Label::from)
.enumerate()
.flat_map(|(i, label)| {
if i == n - 1 {
vec![label].into_iter()
} else {
vec![label, term::Label::space()].into_iter()
}
})
.collect::<Vec<_>>();
Ok([
match state {
patch::State::Open { .. } => term::format::positive("●").into(),
patch::State::Archived => term::format::yellow("●").into(),
patch::State::Draft => term::format::dim("●").into(),
patch::State::Merged { .. } => term::PREFIX_SUCCESS.into(),
},
term::format::tertiary(term::format::cob(id)).into(),
term::format::default(patch.title().to_owned()).into(),
alias.into(),
did.into(),
reviews.into(),
term::format::secondary(term::format::oid(revision.head())).into(),
term::format::positive(format!("+{}", stats.insertions())).into(),
term::format::negative(format!("-{}", stats.deletions())).into(),
term::format::timestamp(patch.updated_at())
.dim()
.italic()
.into(),
])
}