cli: Use new widgets for `patch list`

This commit is contained in:
Alexis Sellier 2023-03-20 15:45:23 +01:00
parent ab74e54949
commit db8ec5735f
No known key found for this signature in database
14 changed files with 224 additions and 149 deletions

View File

@ -46,17 +46,13 @@ It will now be listed as one of the project's open patches.
``` ```
$ rad patch $ rad patch
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
YOU PROPOSED │ Define power requirements 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
Define power requirements 191a14e520f R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) 3 months ago │
└─ * opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..] ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
└─ * patch id 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 ```
```
OTHERS PROPOSED
Nothing to show.
$ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 $ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │
@ -118,3 +114,22 @@ Now, let's checkout the patch that we just created:
$ rad patch checkout 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 $ rad patch checkout 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
✓ Switched to branch patch/191a14e520f ✓ Switched to branch patch/191a14e520f
``` ```
We can also add a review verdict as such:
```
$ rad review 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 --accept --no-confirm --no-message --no-sync
✓ Patch 191a14e520f accepted
```
Showing the patch list now will reveal the favorable verdict:
```
$ rad patch
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Define power requirements 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 R1 27857ec (flux-capacitor-power, patch/191a14e520f) ahead 2, behind 0 │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) 3 months ago │
│ ✓ accepted by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) 3 months ago │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

View File

@ -46,17 +46,11 @@ It will now be listed as one of the project's open patches.
``` ```
$ rad patch $ rad patch
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
YOU PROPOSED │ Define power requirements a07ef7743a32a2e902672ea3526d1db6ee08108a R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
Define power requirements a07ef7743a3 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) 3 months ago │
└─ * opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) [..] ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
└─ * patch id a07ef7743a32a2e902672ea3526d1db6ee08108a
OTHERS PROPOSED
Nothing to show.
$ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a $ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a
╭────────────────────────────────────────────────────────────────────╮ ╭────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │ │ Title Define power requirements │

View File

@ -27,17 +27,11 @@ $ git branch -r
bob/master bob/master
rad/master rad/master
$ rad patch $ rad patch
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
YOU PROPOSED │ Define power requirements a07ef7743a32a2e902672ea3526d1db6ee08108a R1 27857ec ahead 2, behind 0 │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
Nothing to show. │ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk 3 months ago │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
OTHERS PROPOSED
Define power requirements a07ef7743a3 R1 27857ec ahead 2, behind 0
└─ * opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [..]
└─ * patch id a07ef7743a32a2e902672ea3526d1db6ee08108a
``` ```
Wait! There's a mistake. The REQUIREMENTS should be a markdown file. Let's Wait! There's a mistake. The REQUIREMENTS should be a markdown file. Let's

View File

@ -80,16 +80,20 @@ pub fn pretty_sync_status(
repo: &git::raw::Repository, repo: &git::raw::Repository,
revision_oid: Oid, revision_oid: Oid,
head_oid: Oid, head_oid: Oid,
) -> anyhow::Result<String> { ) -> anyhow::Result<term::Line> {
let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?; let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
if a == 0 && b == 0 { if a == 0 && b == 0 {
return Ok(term::format::dim("up to date").to_string()); return Ok(term::Line::new(term::format::dim("up to date")));
} }
let ahead = term::format::positive(a); let ahead = term::format::positive(a);
let behind = term::format::negative(b); let behind = term::format::negative(b);
Ok(format!("ahead {ahead}, behind {behind}")) Ok(term::Line::default()
.item("ahead ")
.item(ahead)
.item(", behind ")
.item(behind))
} }
/// Make a human friendly string for commit version information. /// Make a human friendly string for commit version information.
@ -98,8 +102,9 @@ pub fn pretty_sync_status(
pub fn pretty_commit_version( pub fn pretty_commit_version(
revision_oid: &Oid, revision_oid: &Oid,
repo: &Option<git::raw::Repository>, repo: &Option<git::raw::Repository>,
) -> anyhow::Result<String> { ) -> anyhow::Result<term::Line> {
let mut oid = term::format::secondary(term::format::oid(*revision_oid)).to_string(); let oid = term::format::secondary(term::format::oid(*revision_oid));
let mut line = term::Line::new(oid);
let mut branches: Vec<String> = vec![]; let mut branches: Vec<String> = vec![];
if let Some(repo) = repo { if let Some(repo) = repo {
@ -115,14 +120,11 @@ pub fn pretty_commit_version(
} }
}; };
if !branches.is_empty() { if !branches.is_empty() {
oid = format!( line.push(term::Label::space());
"{} {}", line.push(term::format::yellow(format!("({})", branches.join(", "))));
oid,
term::format::yellow(format!("({})", branches.join(", "))),
);
} }
Ok(oid) Ok(line)
} }
#[inline] #[inline]

View File

@ -7,7 +7,7 @@ use radicle::profile::Profile;
use radicle::storage::git::Repository; use radicle::storage::git::Repository;
use crate::terminal as term; use crate::terminal as term;
use term::cell::Cell as _; use term::Element as _;
use super::common; use super::common;
@ -33,33 +33,18 @@ pub fn run(
other.push((id, patch)); other.push((id, patch));
} }
} }
term::blank();
term::print(term::format::badge_secondary("YOU PROPOSED"));
if own.is_empty() { if own.is_empty() && other.is_empty() {
term::blank();
term::print(term::format::italic("Nothing to show.")); term::print(term::format::italic("Nothing to show."));
} else { return Ok(());
for (id, patch) in &mut own {
term::blank();
print(&me, id, patch, &workdir, repository)?;
}
} }
term::blank();
term::print(term::format::badge_secondary("OTHERS PROPOSED"));
if other.is_empty() { for (id, patch) in &mut own {
term::blank(); print(&me, id, patch, &workdir, repository)?;
term::print(term::format::italic("Nothing to show.")); }
} else { for (id, patch) in &mut other {
for (id, patch) in &mut other { print(profile.id(), id, patch, &workdir, repository)?;
term::blank();
print(profile.id(), id, patch, &workdir, repository)?;
}
} }
term::blank();
Ok(()) Ok(())
} }
@ -75,31 +60,43 @@ fn print(
let target_head = common::patch_merge_target_oid(patch.target(), repository)?; let target_head = common::patch_merge_target_oid(patch.target(), repository)?;
let you = patch.author().id().as_key() == whoami; let you = patch.author().id().as_key() == whoami;
let prefix = "└─ "; let mut author_info = term::Line::spaced([
let mut author_info = vec![format!( term::format::positive("").into(),
"{}* opened by {}", term::format::default("opened by").into(),
prefix, term::format::tertiary(patch.author().id()).into(),
term::format::tertiary(patch.author().id()), ]);
)];
if you { if you {
author_info.push(term::format::secondary("(you)").to_string()); author_info.push(term::Label::space());
author_info.push(term::format::primary("(you)"));
} }
author_info.push(term::format::dim(term::format::timestamp(&patch.timestamp())).to_string()); author_info.push(term::Label::space());
author_info.push(term::format::dim(term::format::timestamp(
&patch.timestamp(),
)));
let (_, revision) = patch let (_, revision) = patch
.latest() .latest()
.ok_or_else(|| anyhow!("patch is malformed: no revisions found"))?; .ok_or_else(|| anyhow!("patch is malformed: no revisions found"))?;
term::info!( let mut widget = term::VStack::default()
"{} {} {} {} {}", .child(
term::format::bold(patch.title()), term::Line::spaced([
term::format::highlight(term::format::cob(patch_id)), term::format::bold(patch.title()).into(),
term::format::dim(format!("R{}", patch.version())), term::format::highlight(patch_id).into(),
common::pretty_commit_version(&revision.oid, workdir)?, term::format::dim(format!("R{}", patch.version())).into(),
common::pretty_sync_status(repository.raw(), *revision.oid, target_head)?, ])
); .space()
term::info!("{}", author_info.join(" ")); .extend(common::pretty_commit_version(&revision.oid, workdir)?)
term::info!("{prefix}* patch id {}", term::format::highlight(patch_id)); .space()
.extend(common::pretty_sync_status(
repository.raw(),
*revision.oid,
target_head,
)?),
)
.divider()
.child(author_info)
.border(Some(term::colors::FAINT));
let mut timeline = Vec::new(); let mut timeline = Vec::new();
for merge in revision.merges.iter() { for merge in revision.merges.iter() {
@ -107,20 +104,22 @@ fn print(
let mut badges = Vec::new(); let mut badges = Vec::new();
if peer.delegate { if peer.delegate {
badges.push(term::format::secondary("(delegate)").to_string()); badges.push(term::format::secondary("(delegate)").into());
} }
if peer.id == *whoami { if peer.id == *whoami {
badges.push(term::format::secondary("(you)").to_string()); badges.push(term::format::primary("(you)").into());
} }
timeline.push(( timeline.push((
merge.timestamp, merge.timestamp,
format!( term::Line::spaced(
"{}{} by {} {}", [
" ".repeat(prefix.width()), term::format::secondary(term::format::dim("✓ merged")).into(),
term::format::secondary(term::format::dim("✓ merged")), term::format::default("by").into(),
term::format::tertiary(peer.id), term::format::tertiary(peer.id).into(),
badges.join(" "), ]
.into_iter()
.chain(badges),
), ),
)); ));
} }
@ -134,32 +133,33 @@ fn print(
let mut badges = Vec::new(); let mut badges = Vec::new();
if peer.delegate { if peer.delegate {
badges.push(term::format::secondary("(delegate)").to_string()); badges.push(term::format::secondary("(delegate)").into());
} }
if peer.id == *whoami { if peer.id == *whoami {
badges.push(term::format::secondary("(you)").to_string()); badges.push(term::format::primary("(you)").into());
} }
timeline.push(( timeline.push((
review.timestamp(), review.timestamp(),
format!( term::Line::spaced(
"{}{} by {} {}", [
" ".repeat(prefix.width()), verdict.into(),
verdict, term::format::default("by").into(),
term::format::tertiary(reviewer), term::format::tertiary(reviewer).into(),
badges.join(" "), ]
.into_iter()
.chain(badges),
), ),
)); ));
} }
timeline.sort_by_key(|(t, _)| *t); timeline.sort_by_key(|(t, _)| *t);
for (time, event) in timeline.iter().rev() { for (time, mut line) in timeline.into_iter().rev() {
term::info!( line.push(term::Label::space());
"{} {}", line.push(term::format::dim(term::format::timestamp(&time)));
event, widget.push(line);
term::format::dim(term::format::timestamp(time))
);
} }
widget.print();
Ok(()) Ok(())
} }

View File

@ -68,7 +68,7 @@ pub fn run(
let description = patch.description().trim(); let description = patch.description().trim();
let meta = VStack::default() let meta = VStack::default()
.border(Some(term::ansi::Color::Blue)) .border(Some(term::colors::FAINT))
.child(attrs) .child(attrs)
.blank() .blank()
.children(if !description.is_empty() { .children(if !description.is_empty() {

View File

@ -28,6 +28,7 @@ Options
-r, --revision <number> Revision number to review, defaults to the latest -r, --revision <number> Revision number to review, defaults to the latest
--[no-]sync Sync review to seed (default: sync) --[no-]sync Sync review to seed (default: sync)
-m, --message [<string>] Provide a comment with the review (default: prompt) -m, --message [<string>] Provide a comment with the review (default: prompt)
--no-confirm Don't ask for confirmation
--no-message Don't provide a comment with the review --no-message Don't provide a comment with the review
--help Print help --help Print help
"#, "#,
@ -50,6 +51,7 @@ pub struct Options {
pub message: Message, pub message: Message,
pub sync: bool, pub sync: bool,
pub verbose: bool, pub verbose: bool,
pub confirm: bool,
pub verdict: Option<Verdict>, pub verdict: Option<Verdict>,
} }
@ -63,6 +65,7 @@ impl Args for Options {
let mut message = Message::default(); let mut message = Message::default();
let mut sync = true; let mut sync = true;
let mut verbose = false; let mut verbose = false;
let mut confirm = true;
let mut verdict = None; let mut verdict = None;
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
@ -90,6 +93,9 @@ impl Args for Options {
message.append(&txt); message.append(&txt);
} }
} }
Long("no-confirm") => {
confirm = false;
}
Long("no-message") => { Long("no-message") => {
message = Message::Blank; message = Message::Blank;
} }
@ -121,6 +127,7 @@ impl Args for Options {
id: id.ok_or_else(|| anyhow!("a patch id to review must be provided"))?, id: id.ok_or_else(|| anyhow!("a patch id to review must be provided"))?,
message, message,
sync, sync,
confirm,
revision, revision,
verbose, verbose,
verdict, verdict,
@ -158,13 +165,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Some(Verdict::Reject) => term::format::negative("Reject"), Some(Verdict::Reject) => term::format::negative("Reject"),
None => term::format::dim("Review"), None => term::format::dim("Review"),
}; };
if !term::confirm(format!( if options.confirm
"{} {} {} by {}?", && !term::confirm(format!(
verdict_pretty, "{} {} {} by {}?",
patch_id_pretty, verdict_pretty,
term::format::dim(format!("R{revision_ix}")), patch_id_pretty,
term::format::tertiary(patch.author().id()) term::format::dim(format!("R{revision_ix}")),
)) { term::format::tertiary(patch.author().id())
))
{
anyhow::bail!("Patch review aborted"); anyhow::bail!("Patch review aborted");
} }

View File

@ -0,0 +1,4 @@
use crate::ansi::Color;
/// The faintest color; useful for borders and such.
pub const FAINT: Color = Color::Fixed(236);

View File

@ -71,6 +71,16 @@ impl Line {
} }
} }
pub fn spaced(items: impl IntoIterator<Item = Label>) -> Self {
let mut line = Self::default();
for item in items.into_iter() {
line.push(item);
line.push(Label::space());
}
line.items.pop();
line
}
/// Add an item to this line. /// Add an item to this line.
pub fn item(mut self, item: impl Into<Label>) -> Self { pub fn item(mut self, item: impl Into<Label>) -> Self {
self.push(item); self.push(item);
@ -90,10 +100,12 @@ impl Line {
/// Pad this line to occupy the given width. /// Pad this line to occupy the given width.
pub fn pad(&mut self, width: usize) { pub fn pad(&mut self, width: usize) {
let w = Element::columns(self); let w = self.columns();
let pad = width - w;
self.items.push(Label::new(" ".repeat(pad).as_str())); if width > w {
let pad = width - w;
self.items.push(Label::new(" ".repeat(pad).as_str()));
}
} }
/// Truncate this line to the given width. /// Truncate this line to the given width.
@ -108,6 +120,11 @@ impl Line {
} }
} }
} }
pub fn space(mut self) -> Self {
self.items.push(Label::space());
self
}
} }
impl IntoIterator for Line { impl IntoIterator for Line {

View File

@ -16,6 +16,10 @@ pub fn positive<D: std::fmt::Display>(msg: D) -> Paint<D> {
Paint::green(msg).bold() Paint::green(msg).bold()
} }
pub fn primary<D: std::fmt::Display>(msg: D) -> Paint<D> {
Paint::magenta(msg)
}
pub fn secondary<D: std::fmt::Display>(msg: D) -> Paint<D> { pub fn secondary<D: std::fmt::Display>(msg: D) -> Paint<D> {
Paint::blue(msg).bold() Paint::blue(msg).bold()
} }

View File

@ -11,10 +11,14 @@ pub struct HStack<'a> {
impl<'a> HStack<'a> { impl<'a> HStack<'a> {
/// Add an element to the stack. /// Add an element to the stack.
pub fn child(mut self, child: impl Element + 'a) -> Self { pub fn child(mut self, child: impl Element + 'a) -> Self {
self.width += child.columns(); self.push(child);
self
}
pub fn push(&mut self, child: impl Element + 'a) {
self.width += child.columns() + 1;
self.height = self.height.max(child.rows()); self.height = self.height.max(child.rows());
self.elems.push(Box::new(child)); self.elems.push(Box::new(child));
self
} }
} }

View File

@ -16,6 +16,11 @@ impl Label {
pub fn blank() -> Self { pub fn blank() -> Self {
Self(Paint::default()) Self(Paint::default())
} }
/// Create a single space label
pub fn space() -> Self {
Self(Paint::new(" ".to_owned()))
}
} }
impl Element for Label { impl Element for Label {
@ -51,19 +56,10 @@ impl Cell for Label {
} }
} }
impl From<Paint<String>> for Label { impl<D: fmt::Display> From<Paint<D>> for Label {
fn from(paint: Paint<String>) -> Self { fn from(paint: Paint<D>) -> Self {
Self(Paint { Self(Paint {
item: paint.item.replace('\n', " "), item: paint.item.to_string().replace('\n', " "),
style: paint.style,
})
}
}
impl From<Paint<&str>> for Label {
fn from(paint: Paint<&str>) -> Self {
Self(Paint {
item: paint.item.replace('\n', " "),
style: paint.style, style: paint.style,
}) })
} }

View File

@ -1,5 +1,6 @@
pub mod ansi; pub mod ansi;
pub mod cell; pub mod cell;
pub mod colors;
pub mod command; pub mod command;
pub mod editor; pub mod editor;
pub mod element; pub mod element;

View File

@ -6,21 +6,26 @@ pub struct VStackOptions {
border: Option<Color>, border: Option<Color>,
} }
/// A vertical stack row.
#[derive(Default, Debug)]
enum Row<'a> {
Element(Box<dyn Element + 'a>),
#[default]
Dividier,
}
/// Vertical stack of [`Element`] objects that implements [`Element`]. /// Vertical stack of [`Element`] objects that implements [`Element`].
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct VStack<'a> { pub struct VStack<'a> {
elems: Vec<Box<dyn Element + 'a>>, rows: Vec<Row<'a>>,
opts: VStackOptions, opts: VStackOptions,
width: usize, width: usize,
height: usize, height: usize,
} }
impl<'a> VStack<'a> { impl<'a> VStack<'a> {
/// Add an element to the stack. /// Add an element to the stack and return the stack.
pub fn child(mut self, child: impl Element + 'a) -> Self { pub fn child(mut self, child: impl Element + 'a) -> Self {
self.width = self.width.max(child.columns()); self.push(child);
self.height += child.rows();
self.elems.push(Box::new(child));
self self
} }
@ -29,6 +34,13 @@ impl<'a> VStack<'a> {
self.child(Label::blank()) self.child(Label::blank())
} }
/// Add a horizontal divider.
pub fn divider(mut self) -> Self {
self.rows.push(Row::Dividier);
self.height += 1;
self
}
/// Add multiple elements to the stack. /// Add multiple elements to the stack.
pub fn children<E: Element + 'a>(self, children: impl IntoIterator<Item = E>) -> Self { pub fn children<E: Element + 'a>(self, children: impl IntoIterator<Item = E>) -> Self {
let mut vstack = self; let mut vstack = self;
@ -44,6 +56,13 @@ impl<'a> VStack<'a> {
self.opts.border = color; self.opts.border = color;
self self
} }
/// Add an element to the stack.
pub fn push(&mut self, child: impl Element + 'a) {
self.width = self.width.max(child.columns());
self.height += child.rows();
self.rows.push(Row::Element(Box::new(child)));
}
} }
impl<'a> Element for VStack<'a> { impl<'a> Element for VStack<'a> {
@ -67,18 +86,34 @@ impl<'a> Element for VStack<'a> {
); );
} }
for elem in &self.elems { for row in &self.rows {
for mut line in elem.render() { match row {
if let Some(color) = self.opts.border { Row::Element(elem) => {
line.pad(self.width); for mut line in elem.render() {
lines.push( if let Some(color) = self.opts.border {
Line::default() line.pad(self.width);
.item(Paint::new("").fg(color)) lines.push(
.extend(line) Line::default()
.item(Paint::new("").fg(color)), .item(Paint::new("").fg(color))
); .extend(line)
} else { .item(Paint::new("").fg(color)),
lines.push(line); );
} else {
lines.push(line);
}
}
}
Row::Dividier => {
if let Some(color) = self.opts.border {
lines.push(
Line::default()
.item(Paint::new("").fg(color))
.item(Paint::new("".repeat(self.width + 2)).fg(color))
.item(Paint::new("").fg(color)),
);
} else {
lines.push(Line::default());
}
} }
} }
} }