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
YOU PROPOSED
Define power requirements 191a14e520f R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0
└─ * opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [..]
└─ * patch id 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
OTHERS PROPOSED
Nothing to show.
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Define power requirements 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) 3 months ago │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
```
$ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
╭────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │
@ -118,3 +114,22 @@ Now, let's checkout the patch that we just created:
$ rad patch checkout 191a14e520f2eeff7c0e3ee0a5523c5217eecb89
✓ 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
YOU PROPOSED
Define power requirements a07ef7743a3 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0
└─ * opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) [..]
└─ * patch id a07ef7743a32a2e902672ea3526d1db6ee08108a
OTHERS PROPOSED
Nothing to show.
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Define power requirements a07ef7743a32a2e902672ea3526d1db6ee08108a R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) 3 months ago │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
$ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a
╭────────────────────────────────────────────────────────────────────╮
│ Title Define power requirements │

View File

@ -27,17 +27,11 @@ $ git branch -r
bob/master
rad/master
$ rad patch
YOU PROPOSED
Nothing to show.
OTHERS PROPOSED
Define power requirements a07ef7743a3 R1 27857ec ahead 2, behind 0
└─ * opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [..]
└─ * patch id a07ef7743a32a2e902672ea3526d1db6ee08108a
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Define power requirements a07ef7743a32a2e902672ea3526d1db6ee08108a R1 27857ec ahead 2, behind 0 │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk 3 months ago │
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
```
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,
revision_oid: Oid,
head_oid: Oid,
) -> anyhow::Result<String> {
) -> anyhow::Result<term::Line> {
let (a, b) = repo.graph_ahead_behind(revision_oid, head_oid)?;
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 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.
@ -98,8 +102,9 @@ pub fn pretty_sync_status(
pub fn pretty_commit_version(
revision_oid: &Oid,
repo: &Option<git::raw::Repository>,
) -> anyhow::Result<String> {
let mut oid = term::format::secondary(term::format::oid(*revision_oid)).to_string();
) -> anyhow::Result<term::Line> {
let oid = term::format::secondary(term::format::oid(*revision_oid));
let mut line = term::Line::new(oid);
let mut branches: Vec<String> = vec![];
if let Some(repo) = repo {
@ -115,14 +120,11 @@ pub fn pretty_commit_version(
}
};
if !branches.is_empty() {
oid = format!(
"{} {}",
oid,
term::format::yellow(format!("({})", branches.join(", "))),
);
line.push(term::Label::space());
line.push(term::format::yellow(format!("({})", branches.join(", "))));
}
Ok(oid)
Ok(line)
}
#[inline]

View File

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

View File

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

View File

@ -28,6 +28,7 @@ Options
-r, --revision <number> Revision number to review, defaults to the latest
--[no-]sync Sync review to seed (default: sync)
-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
--help Print help
"#,
@ -50,6 +51,7 @@ pub struct Options {
pub message: Message,
pub sync: bool,
pub verbose: bool,
pub confirm: bool,
pub verdict: Option<Verdict>,
}
@ -63,6 +65,7 @@ impl Args for Options {
let mut message = Message::default();
let mut sync = true;
let mut verbose = false;
let mut confirm = true;
let mut verdict = None;
while let Some(arg) = parser.next()? {
@ -90,6 +93,9 @@ impl Args for Options {
message.append(&txt);
}
}
Long("no-confirm") => {
confirm = false;
}
Long("no-message") => {
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"))?,
message,
sync,
confirm,
revision,
verbose,
verdict,
@ -158,13 +165,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Some(Verdict::Reject) => term::format::negative("Reject"),
None => term::format::dim("Review"),
};
if !term::confirm(format!(
"{} {} {} by {}?",
verdict_pretty,
patch_id_pretty,
term::format::dim(format!("R{revision_ix}")),
term::format::tertiary(patch.author().id())
)) {
if options.confirm
&& !term::confirm(format!(
"{} {} {} by {}?",
verdict_pretty,
patch_id_pretty,
term::format::dim(format!("R{revision_ix}")),
term::format::tertiary(patch.author().id())
))
{
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.
pub fn item(mut self, item: impl Into<Label>) -> Self {
self.push(item);
@ -90,10 +100,12 @@ impl Line {
/// Pad this line to occupy the given width.
pub fn pad(&mut self, width: usize) {
let w = Element::columns(self);
let pad = width - w;
let w = self.columns();
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.
@ -108,6 +120,11 @@ impl Line {
}
}
}
pub fn space(mut self) -> Self {
self.items.push(Label::space());
self
}
}
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()
}
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> {
Paint::blue(msg).bold()
}

View File

@ -11,10 +11,14 @@ pub struct HStack<'a> {
impl<'a> HStack<'a> {
/// Add an element to the stack.
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.elems.push(Box::new(child));
self
}
}

View File

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

View File

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

View File

@ -6,21 +6,26 @@ pub struct VStackOptions {
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`].
#[derive(Default, Debug)]
pub struct VStack<'a> {
elems: Vec<Box<dyn Element + 'a>>,
rows: Vec<Row<'a>>,
opts: VStackOptions,
width: usize,
height: usize,
}
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 {
self.width = self.width.max(child.columns());
self.height += child.rows();
self.elems.push(Box::new(child));
self.push(child);
self
}
@ -29,6 +34,13 @@ impl<'a> VStack<'a> {
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.
pub fn children<E: Element + 'a>(self, children: impl IntoIterator<Item = E>) -> Self {
let mut vstack = self;
@ -44,6 +56,13 @@ impl<'a> VStack<'a> {
self.opts.border = color;
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> {
@ -67,18 +86,34 @@ impl<'a> Element for VStack<'a> {
);
}
for elem in &self.elems {
for mut line in elem.render() {
if let Some(color) = self.opts.border {
line.pad(self.width);
lines.push(
Line::default()
.item(Paint::new("").fg(color))
.extend(line)
.item(Paint::new("").fg(color)),
);
} else {
lines.push(line);
for row in &self.rows {
match row {
Row::Element(elem) => {
for mut line in elem.render() {
if let Some(color) = self.opts.border {
line.pad(self.width);
lines.push(
Line::default()
.item(Paint::new("").fg(color))
.extend(line)
.item(Paint::new("").fg(color)),
);
} 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());
}
}
}
}