From 9767b485c2aad1e23097d2b5165287ba84cfa452 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Mon, 12 Feb 2024 13:28:49 +0100 Subject: [PATCH] cli: Improve `rad inbox` output * Add change authors * Add object status * Add colors --- radicle-cli/examples/rad-inbox.md | 56 +++++----- .../workflow/5-patching-maintainer.md | 12 +- radicle-cli/src/commands/inbox.rs | 105 ++++++++++++------ radicle-cli/src/commands/patch/checkout.rs | 5 +- radicle-cli/src/terminal/format.rs | 50 ++++++++- 5 files changed, 155 insertions(+), 73 deletions(-) diff --git a/radicle-cli/examples/rad-inbox.md b/radicle-cli/examples/rad-inbox.md index 17c74bf2..fb75d4f3 100644 --- a/radicle-cli/examples/rad-inbox.md +++ b/radicle-cli/examples/rad-inbox.md @@ -18,27 +18,27 @@ $ git push rad -o patch.message="Copyright fixes" HEAD:refs/patches ``` ~alice $ rad inbox --sort-by id -╭──────────────────────────────────────────────────────────────╮ -│ heartwood │ -├──────────────────────────────────────────────────────────────┤ -│ 1 ● issue No license file [ .. ] opened now │ -│ 2 ● branch Change copyright bob/copy created now │ -╰──────────────────────────────────────────────────────────────╯ +╭──────────────────────────────────────────────────────────────────────╮ +│ heartwood │ +├──────────────────────────────────────────────────────────────────────┤ +│ 001 ● 58fff44 No license file issue open bob now │ +│ 002 ● bob/copy Change copyright branch created bob now │ +╰──────────────────────────────────────────────────────────────────────╯ ``` ``` ~alice $ rad inbox --all --sort-by id -╭──────────────────────────────────────────────────────────╮ -│ radicle-git │ -├──────────────────────────────────────────────────────────┤ -│ 3 ● patch Copyright fixes [ ... ] opened now │ -╰──────────────────────────────────────────────────────────╯ -╭──────────────────────────────────────────────────────────────╮ -│ heartwood │ -├──────────────────────────────────────────────────────────────┤ -│ 1 ● issue No license file [ .. ] opened now │ -│ 2 ● branch Change copyright bob/copy created now │ -╰──────────────────────────────────────────────────────────────╯ +╭────────────────────────────────────────────────────────────────╮ +│ radicle-git │ +├────────────────────────────────────────────────────────────────┤ +│ 003 ● 4dd5843 Copyright fixes patch open bob now │ +╰────────────────────────────────────────────────────────────────╯ +╭──────────────────────────────────────────────────────────────────────╮ +│ heartwood │ +├──────────────────────────────────────────────────────────────────────┤ +│ 001 ● 58fff44 No license file issue open bob now │ +│ 002 ● bob/copy Change copyright branch created bob now │ +╰──────────────────────────────────────────────────────────────────────╯ ``` ``` ~alice @@ -64,12 +64,12 @@ Date: Mon Jan 1 14:39:16 2018 +0000 ``` ~alice $ rad inbox list --sort-by id -╭──────────────────────────────────────────────────────────────╮ -│ heartwood │ -├──────────────────────────────────────────────────────────────┤ -│ 1 ● issue No license file [ ... ] opened now │ -│ 2 branch Change copyright bob/copy created now │ -╰──────────────────────────────────────────────────────────────╯ +╭──────────────────────────────────────────────────────────────────────╮ +│ heartwood │ +├──────────────────────────────────────────────────────────────────────┤ +│ 001 ● 58fff44 No license file issue open bob now │ +│ 002 bob/copy Change copyright branch created bob now │ +╰──────────────────────────────────────────────────────────────────────╯ ``` ``` ~alice @@ -90,11 +90,11 @@ $ rad inbox clear $ rad inbox Your inbox is empty. $ rad inbox --all -╭──────────────────────────────────────────────────────────╮ -│ radicle-git │ -├──────────────────────────────────────────────────────────┤ -│ 3 ● patch Copyright fixes [ ... ] opened now │ -╰──────────────────────────────────────────────────────────╯ +╭────────────────────────────────────────────────────────────────╮ +│ radicle-git │ +├────────────────────────────────────────────────────────────────┤ +│ 003 ● 4dd5843 Copyright fixes patch open bob now │ +╰────────────────────────────────────────────────────────────────╯ ``` ``` ~alice diff --git a/radicle-cli/examples/workflow/5-patching-maintainer.md b/radicle-cli/examples/workflow/5-patching-maintainer.md index 45b52bdc..120c50c6 100644 --- a/radicle-cli/examples/workflow/5-patching-maintainer.md +++ b/radicle-cli/examples/workflow/5-patching-maintainer.md @@ -17,12 +17,12 @@ The contributor's changes are now visible to us. ``` $ rad inbox --sort-by id -╭──────────────────────────────────────────────────────────────────────╮ -│ heartwood │ -├──────────────────────────────────────────────────────────────────────┤ -│ 1 ● issue flux capacitor underpowered d060989 opened now │ -│ 2 ● patch Define power requirements a99d55e opened now │ -╰──────────────────────────────────────────────────────────────────────╯ +╭────────────────────────────────────────────────────────────────────────────╮ +│ heartwood │ +├────────────────────────────────────────────────────────────────────────────┤ +│ 001 ● d060989 flux capacitor underpowered issue open bob now │ +│ 002 ● a99d55e Define power requirements patch open bob now │ +╰────────────────────────────────────────────────────────────────────────────╯ $ git branch -r bob/patches/a99d55e5958a8c52ff7efbc8ff000d9bbdac79c7 rad/master diff --git a/radicle-cli/src/commands/inbox.rs b/radicle-cli/src/commands/inbox.rs index e1a2fc6a..702676c5 100644 --- a/radicle-cli/src/commands/inbox.rs +++ b/radicle-cli/src/commands/inbox.rs @@ -9,7 +9,6 @@ use radicle::node::notifications; use radicle::node::notifications::*; use radicle::patch::Patches; use radicle::prelude::{Profile, RepoId}; -use radicle::storage::RefUpdate; use radicle::storage::{ReadRepository, ReadStorage}; use radicle::{cob, Storage}; @@ -157,7 +156,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let Options { op, mode, sort_by } = options; match op { - Operation::List => list(mode, sort_by, ¬ifs.read_only(), storage), + Operation::List => list(mode, sort_by, ¬ifs.read_only(), storage, &profile), Operation::Clear => clear(mode, &mut notifs), Operation::Show => show(mode, &mut notifs, storage, &profile), } @@ -168,21 +167,22 @@ fn list( sort_by: SortBy, notifs: ¬ifications::StoreReader, storage: &Storage, + profile: &Profile, ) -> anyhow::Result<()> { let repos: Vec> = match mode { Mode::Contextual => { if let Ok((_, rid)) = radicle::rad::cwd() { - list_repo(rid, sort_by, notifs, storage)? + list_repo(rid, sort_by, notifs, storage, profile)? .into_iter() .collect() } else { - list_all(sort_by, notifs, storage)? + list_all(sort_by, notifs, storage, profile)? } } - Mode::ByRepo(rid) => list_repo(rid, sort_by, notifs, storage)? + Mode::ByRepo(rid) => list_repo(rid, sort_by, notifs, storage, profile)? .into_iter() .collect(), - Mode::All => list_all(sort_by, notifs, storage)?, + Mode::All => list_all(sort_by, notifs, storage, profile)?, Mode::ById(_) => anyhow::bail!("the `list` command does not take IDs"), }; @@ -200,13 +200,14 @@ fn list_all<'a>( sort_by: SortBy, notifs: ¬ifications::StoreReader, storage: &Storage, + profile: &Profile, ) -> anyhow::Result>> { let mut repos = storage.repositories()?; repos.sort_by_key(|r| r.rid); let mut vstacks = Vec::new(); for repo in repos { - let vstack = list_repo(repo.rid, sort_by, notifs, storage)?; + let vstack = list_repo(repo.rid, sort_by, notifs, storage, profile)?; vstacks.extend(vstack.into_iter()); } Ok(vstacks) @@ -217,6 +218,7 @@ fn list_repo<'a, R: ReadStorage>( sort_by: SortBy, notifs: ¬ifications::StoreReader, storage: &R, + profile: &Profile, ) -> anyhow::Result>> where ::Repository: cob::Store, @@ -226,6 +228,7 @@ where ..term::TableOptions::default() }); let repo = storage.repository(rid)?; + let (_, head) = repo.head()?; let doc = repo.identity_doc()?; let proj = doc.project()?; let issues = Issues::open(&repo)?; @@ -245,48 +248,82 @@ where } else { term::format::tertiary(String::from("●")).into() }; - let (category, summary, status, name) = match n.kind { + let (category, summary, state, name) = match n.kind { NotificationKind::Branch { name } => { let commit = if let Some(head) = n.update.new() { repo.commit(head)?.summary().unwrap_or_default().to_owned() } else { String::new() }; - let status = match n.update { - RefUpdate::Updated { .. } => "updated", - RefUpdate::Created { .. } => "created", - RefUpdate::Deleted { .. } => "deleted", - RefUpdate::Skipped { .. } => "skipped", - }; - ("branch".to_string(), commit, status, name.to_string()) + + let state = match n + .update + .new() + .map(|oid| repo.is_ancestor_of(oid, head)) + .transpose() + { + Ok(Some(true)) => { + term::Paint::::from(term::format::secondary("merged")) + } + Ok(Some(false)) | Ok(None) => term::format::ref_update(n.update).into(), + Err(e) => return Err(e.into()), + } + .to_owned(); + + ( + "branch".to_string(), + commit, + state, + term::format::default(name.to_string()), + ) } NotificationKind::Cob { type_name, id } => { - let (category, summary) = if type_name == *cob::issue::TYPENAME { - let issue = issues.get(&id)?.ok_or(anyhow!("missing"))?; - (String::from("issue"), issue.title().to_owned()) + let (category, summary, state) = if type_name == *cob::issue::TYPENAME { + let Some(issue) = issues.get(&id)? else { + // Issue could have been deleted after notification was created. + continue; + }; + ( + String::from("issue"), + issue.title().to_owned(), + term::format::issue::state(issue.state()), + ) } else if type_name == *cob::patch::TYPENAME { - let patch = patches.get(&id)?.ok_or(anyhow!("missing"))?; - (String::from("patch"), patch.title().to_owned()) + let Some(patch) = patches.get(&id)? else { + // Patch could have been deleted after notification was created. + continue; + }; + ( + String::from("patch"), + patch.title().to_owned(), + term::format::patch::state(patch.state()), + ) } else { - (type_name.to_string(), "".to_owned()) + ( + type_name.to_string(), + "".to_owned(), + term::format::default(String::new()), + ) }; - let status = match n.update { - RefUpdate::Updated { .. } => "updated", - RefUpdate::Created { .. } => "opened", - RefUpdate::Deleted { .. } => "deleted", - RefUpdate::Skipped { .. } => "skipped", - }; - (category, summary, status, term::format::cob(&id)) + (category, summary, state, term::format::cob(&id)) } }; + let author = n + .remote + .map(|r| { + let (alias, _) = term::format::Author::new(&r, profile).labels(); + alias + }) + .unwrap_or_default(); table.push([ - n.id.to_string().into(), + term::format::dim(format!("{:-03}", n.id)).into(), seen, - category.into(), + term::format::tertiary(name).into(), summary.into(), - name.into(), - status.into(), - term::format::timestamp(n.timestamp).into(), + term::format::dim(category).into(), + state.into(), + author, + term::format::italic(term::format::timestamp(n.timestamp)).into(), ]); } @@ -296,7 +333,7 @@ where Ok(Some( term::VStack::default() .border(Some(term::colors::FAINT)) - .child(term::label(proj.name())) + .child(term::label(term::format::bold(proj.name()))) .divider() .child(table), )) diff --git a/radicle-cli/src/commands/patch/checkout.rs b/radicle-cli/src/commands/patch/checkout.rs index 4b35fcd0..1546840e 100644 --- a/radicle-cli/src/commands/patch/checkout.rs +++ b/radicle-cli/src/commands/patch/checkout.rs @@ -22,9 +22,8 @@ impl Options { Some(refname) => Ok(Qualified::from_refstr(refname) .map_or_else(|| refname.clone(), |q| q.to_ref_string())), // SAFETY: Patch IDs are valid refstrings. - None => Ok( - git::refname!("patch").join(RefString::try_from(term::format::cob(id)).unwrap()) - ), + None => Ok(git::refname!("patch") + .join(RefString::try_from(term::format::cob(id).item).unwrap())), } } } diff --git a/radicle-cli/src/terminal/format.rs b/radicle-cli/src/terminal/format.rs index bf80bbac..dcd1191a 100644 --- a/radicle-cli/src/terminal/format.rs +++ b/radicle-cli/src/terminal/format.rs @@ -10,6 +10,7 @@ use radicle::identity::Visibility; use radicle::node::{Alias, AliasStore, NodeId}; use radicle::prelude::Did; use radicle::profile::Profile; +use radicle::storage::RefUpdate; use radicle_term::element::Line; use crate::terminal as term; @@ -44,8 +45,8 @@ pub fn command(cmd: D) -> Paint { } /// Format a COB id. -pub fn cob(id: &ObjectId) -> String { - format!("{:.7}", id.to_string()) +pub fn cob(id: &ObjectId) -> Paint { + Paint::new(format!("{:.7}", id.to_string())) } /// Format a DID. @@ -72,6 +73,16 @@ pub fn timestamp(time: impl Into) -> Paint { Paint::new(fmt.convert(duration.into())) } +/// Format a ref update. +pub fn ref_update(update: RefUpdate) -> Paint<&'static str> { + match update { + RefUpdate::Updated { .. } => term::format::tertiary("updated"), + RefUpdate::Created { .. } => term::format::positive("created"), + RefUpdate::Deleted { .. } => term::format::negative("deleted"), + RefUpdate::Skipped { .. } => term::format::dim("skipped"), + } +} + /// Identity formatter that takes a profile and displays it as /// ` ()` depending on the configuration. pub struct Identity<'a> { @@ -222,6 +233,41 @@ pub mod html { } } +/// Issue formatting +pub mod issue { + use super::*; + use radicle::issue::{CloseReason, State}; + + /// Format issue state. + pub fn state(s: &State) -> term::Paint { + match s { + State::Open => term::format::positive(s.to_string()), + State::Closed { + reason: CloseReason::Other, + } => term::format::negative(s.to_string()), + State::Closed { + reason: CloseReason::Solved, + } => term::format::secondary(s.to_string()), + } + } +} + +/// Patch formatting +pub mod patch { + use super::*; + use radicle::patch::State; + + /// Format patch state. + pub fn state(s: &State) -> term::Paint { + match s { + State::Draft { .. } => term::format::dim(s.to_string()), + State::Open { .. } => term::format::positive(s.to_string()), + State::Archived => term::format::yellow(s.to_string()), + State::Merged { .. } => term::format::secondary(s.to_string()), + } + } +} + #[cfg(test)] mod test { use super::*;