cli: Improve `rad inbox` output

* Add change authors
* Add object status
* Add colors
This commit is contained in:
cloudhead 2024-02-12 13:28:49 +01:00
parent b477864b83
commit 9767b485c2
No known key found for this signature in database
5 changed files with 155 additions and 73 deletions

View File

@ -18,27 +18,27 @@ $ git push rad -o patch.message="Copyright fixes" HEAD:refs/patches
``` ~alice ``` ~alice
$ rad inbox --sort-by id $ rad inbox --sort-by id
╭──────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────
│ heartwood │ │ heartwood
├──────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────
1 ● issue No license file [ .. ] opened now │ 001 ● 58fff44 No license file issue open bob now │
2 ● branch Change copyright bob/copy created now │ 002 ● bob/copy Change copyright branch created bob now │
╰──────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────
``` ```
``` ~alice ``` ~alice
$ rad inbox --all --sort-by id $ rad inbox --all --sort-by id
╭──────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────
│ radicle-git │ │ radicle-git
├──────────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────────────────
3 ● patch Copyright fixes [ ... ] opened now │ 003 ● 4dd5843 Copyright fixes patch open bob now │
╰──────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────
╭──────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────
│ heartwood │ │ heartwood
├──────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────
1 ● issue No license file [ .. ] opened now │ 001 ● 58fff44 No license file issue open bob now │
2 ● branch Change copyright bob/copy created now │ 002 ● bob/copy Change copyright branch created bob now │
╰──────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────
``` ```
``` ~alice ``` ~alice
@ -64,12 +64,12 @@ Date: Mon Jan 1 14:39:16 2018 +0000
``` ~alice ``` ~alice
$ rad inbox list --sort-by id $ rad inbox list --sort-by id
╭──────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────
│ heartwood │ │ heartwood
├──────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────────────────────
1 ● issue No license file [ ... ] opened now │ 001 ● 58fff44 No license file issue open bob now │
2 branch Change copyright bob/copy created now │ 002 bob/copy Change copyright branch created bob now │
╰──────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────
``` ```
``` ~alice ``` ~alice
@ -90,11 +90,11 @@ $ rad inbox clear
$ rad inbox $ rad inbox
Your inbox is empty. Your inbox is empty.
$ rad inbox --all $ rad inbox --all
╭──────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────
│ radicle-git │ │ radicle-git
├──────────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────────────────
3 ● patch Copyright fixes [ ... ] opened now │ 003 ● 4dd5843 Copyright fixes patch open bob now │
╰──────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────
``` ```
``` ~alice ``` ~alice

View File

@ -17,12 +17,12 @@ The contributor's changes are now visible to us.
``` ```
$ rad inbox --sort-by id $ rad inbox --sort-by id
╭──────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────────────
│ heartwood │ │ heartwood
├──────────────────────────────────────────────────────────────────────┤ ├────────────────────────────────────────────────────────────────────────────
1 ● issue flux capacitor underpowered d060989 opened now │ 001 ● d060989 flux capacitor underpowered issue open bob now │
2 ● patch Define power requirements a99d55e opened now │ 002 ● a99d55e Define power requirements patch open bob now │
╰──────────────────────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────────────────────────────
$ git branch -r $ git branch -r
bob/patches/a99d55e5958a8c52ff7efbc8ff000d9bbdac79c7 bob/patches/a99d55e5958a8c52ff7efbc8ff000d9bbdac79c7
rad/master rad/master

View File

@ -9,7 +9,6 @@ use radicle::node::notifications;
use radicle::node::notifications::*; use radicle::node::notifications::*;
use radicle::patch::Patches; use radicle::patch::Patches;
use radicle::prelude::{Profile, RepoId}; use radicle::prelude::{Profile, RepoId};
use radicle::storage::RefUpdate;
use radicle::storage::{ReadRepository, ReadStorage}; use radicle::storage::{ReadRepository, ReadStorage};
use radicle::{cob, Storage}; 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; let Options { op, mode, sort_by } = options;
match op { match op {
Operation::List => list(mode, sort_by, &notifs.read_only(), storage), Operation::List => list(mode, sort_by, &notifs.read_only(), storage, &profile),
Operation::Clear => clear(mode, &mut notifs), Operation::Clear => clear(mode, &mut notifs),
Operation::Show => show(mode, &mut notifs, storage, &profile), Operation::Show => show(mode, &mut notifs, storage, &profile),
} }
@ -168,21 +167,22 @@ fn list(
sort_by: SortBy, sort_by: SortBy,
notifs: &notifications::StoreReader, notifs: &notifications::StoreReader,
storage: &Storage, storage: &Storage,
profile: &Profile,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let repos: Vec<term::VStack<'_>> = match mode { let repos: Vec<term::VStack<'_>> = match mode {
Mode::Contextual => { Mode::Contextual => {
if let Ok((_, rid)) = radicle::rad::cwd() { if let Ok((_, rid)) = radicle::rad::cwd() {
list_repo(rid, sort_by, notifs, storage)? list_repo(rid, sort_by, notifs, storage, profile)?
.into_iter() .into_iter()
.collect() .collect()
} else { } 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() .into_iter()
.collect(), .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"), Mode::ById(_) => anyhow::bail!("the `list` command does not take IDs"),
}; };
@ -200,13 +200,14 @@ fn list_all<'a>(
sort_by: SortBy, sort_by: SortBy,
notifs: &notifications::StoreReader, notifs: &notifications::StoreReader,
storage: &Storage, storage: &Storage,
profile: &Profile,
) -> anyhow::Result<Vec<term::VStack<'a>>> { ) -> anyhow::Result<Vec<term::VStack<'a>>> {
let mut repos = storage.repositories()?; let mut repos = storage.repositories()?;
repos.sort_by_key(|r| r.rid); repos.sort_by_key(|r| r.rid);
let mut vstacks = Vec::new(); let mut vstacks = Vec::new();
for repo in repos { 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()); vstacks.extend(vstack.into_iter());
} }
Ok(vstacks) Ok(vstacks)
@ -217,6 +218,7 @@ fn list_repo<'a, R: ReadStorage>(
sort_by: SortBy, sort_by: SortBy,
notifs: &notifications::StoreReader, notifs: &notifications::StoreReader,
storage: &R, storage: &R,
profile: &Profile,
) -> anyhow::Result<Option<term::VStack<'a>>> ) -> anyhow::Result<Option<term::VStack<'a>>>
where where
<R as ReadStorage>::Repository: cob::Store, <R as ReadStorage>::Repository: cob::Store,
@ -226,6 +228,7 @@ where
..term::TableOptions::default() ..term::TableOptions::default()
}); });
let repo = storage.repository(rid)?; let repo = storage.repository(rid)?;
let (_, head) = repo.head()?;
let doc = repo.identity_doc()?; let doc = repo.identity_doc()?;
let proj = doc.project()?; let proj = doc.project()?;
let issues = Issues::open(&repo)?; let issues = Issues::open(&repo)?;
@ -245,48 +248,82 @@ where
} else { } else {
term::format::tertiary(String::from("")).into() 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 } => { NotificationKind::Branch { name } => {
let commit = if let Some(head) = n.update.new() { let commit = if let Some(head) = n.update.new() {
repo.commit(head)?.summary().unwrap_or_default().to_owned() repo.commit(head)?.summary().unwrap_or_default().to_owned()
} else { } else {
String::new() String::new()
}; };
let status = match n.update {
RefUpdate::Updated { .. } => "updated", let state = match n
RefUpdate::Created { .. } => "created", .update
RefUpdate::Deleted { .. } => "deleted", .new()
RefUpdate::Skipped { .. } => "skipped", .map(|oid| repo.is_ancestor_of(oid, head))
}; .transpose()
("branch".to_string(), commit, status, name.to_string()) {
Ok(Some(true)) => {
term::Paint::<String>::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 } => { NotificationKind::Cob { type_name, id } => {
let (category, summary) = if type_name == *cob::issue::TYPENAME { let (category, summary, state) = if type_name == *cob::issue::TYPENAME {
let issue = issues.get(&id)?.ok_or(anyhow!("missing"))?; let Some(issue) = issues.get(&id)? else {
(String::from("issue"), issue.title().to_owned()) // 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 { } else if type_name == *cob::patch::TYPENAME {
let patch = patches.get(&id)?.ok_or(anyhow!("missing"))?; let Some(patch) = patches.get(&id)? else {
(String::from("patch"), patch.title().to_owned()) // Patch could have been deleted after notification was created.
continue;
};
(
String::from("patch"),
patch.title().to_owned(),
term::format::patch::state(patch.state()),
)
} else { } else {
(type_name.to_string(), "".to_owned()) (
type_name.to_string(),
"".to_owned(),
term::format::default(String::new()),
)
}; };
let status = match n.update { (category, summary, state, term::format::cob(&id))
RefUpdate::Updated { .. } => "updated",
RefUpdate::Created { .. } => "opened",
RefUpdate::Deleted { .. } => "deleted",
RefUpdate::Skipped { .. } => "skipped",
};
(category, summary, status, 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([ table.push([
n.id.to_string().into(), term::format::dim(format!("{:-03}", n.id)).into(),
seen, seen,
category.into(), term::format::tertiary(name).into(),
summary.into(), summary.into(),
name.into(), term::format::dim(category).into(),
status.into(), state.into(),
term::format::timestamp(n.timestamp).into(), author,
term::format::italic(term::format::timestamp(n.timestamp)).into(),
]); ]);
} }
@ -296,7 +333,7 @@ where
Ok(Some( Ok(Some(
term::VStack::default() term::VStack::default()
.border(Some(term::colors::FAINT)) .border(Some(term::colors::FAINT))
.child(term::label(proj.name())) .child(term::label(term::format::bold(proj.name())))
.divider() .divider()
.child(table), .child(table),
)) ))

View File

@ -22,9 +22,8 @@ impl Options {
Some(refname) => Ok(Qualified::from_refstr(refname) Some(refname) => Ok(Qualified::from_refstr(refname)
.map_or_else(|| refname.clone(), |q| q.to_ref_string())), .map_or_else(|| refname.clone(), |q| q.to_ref_string())),
// SAFETY: Patch IDs are valid refstrings. // SAFETY: Patch IDs are valid refstrings.
None => Ok( None => Ok(git::refname!("patch")
git::refname!("patch").join(RefString::try_from(term::format::cob(id)).unwrap()) .join(RefString::try_from(term::format::cob(id).item).unwrap())),
),
} }
} }
} }

View File

@ -10,6 +10,7 @@ use radicle::identity::Visibility;
use radicle::node::{Alias, AliasStore, NodeId}; use radicle::node::{Alias, AliasStore, NodeId};
use radicle::prelude::Did; use radicle::prelude::Did;
use radicle::profile::Profile; use radicle::profile::Profile;
use radicle::storage::RefUpdate;
use radicle_term::element::Line; use radicle_term::element::Line;
use crate::terminal as term; use crate::terminal as term;
@ -44,8 +45,8 @@ pub fn command<D: fmt::Display>(cmd: D) -> Paint<String> {
} }
/// Format a COB id. /// Format a COB id.
pub fn cob(id: &ObjectId) -> String { pub fn cob(id: &ObjectId) -> Paint<String> {
format!("{:.7}", id.to_string()) Paint::new(format!("{:.7}", id.to_string()))
} }
/// Format a DID. /// Format a DID.
@ -72,6 +73,16 @@ pub fn timestamp(time: impl Into<LocalTime>) -> Paint<String> {
Paint::new(fmt.convert(duration.into())) 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 /// Identity formatter that takes a profile and displays it as
/// `<node-id> (<username>)` depending on the configuration. /// `<node-id> (<username>)` depending on the configuration.
pub struct Identity<'a> { 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<String> {
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<String> {
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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;