Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Use new widgets for `patch list`
Alexis Sellier committed 3 years ago
commit db8ec5735fd64b027344da5e66306cc5c8f1685f
parent ab74e54949cc23485d64354c5da3a825bd8bd6ee
14 files changed +224 -149
modified radicle-cli/examples/rad-patch.md
@@ -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                                                         │
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+
```
modified radicle-cli/examples/workflow/4-patching-contributor.md
@@ -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                                  │
modified radicle-cli/examples/workflow/5-patching-maintainer.md
@@ -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
modified radicle-cli/src/commands/patch/common.rs
@@ -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]
modified radicle-cli/src/commands/patch/list.rs
@@ -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(())
}
modified radicle-cli/src/commands/patch/show.rs
@@ -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() {
modified radicle-cli/src/commands/review.rs
@@ -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");
    }

added radicle-term/src/colors.rs
@@ -0,0 +1,4 @@
+
use crate::ansi::Color;
+

+
/// The faintest color; useful for borders and such.
+
pub const FAINT: Color = Color::Fixed(236);
modified radicle-term/src/element.rs
@@ -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 {
modified radicle-term/src/format.rs
@@ -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()
}
modified radicle-term/src/hstack.rs
@@ -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
    }
}

modified radicle-term/src/label.rs
@@ -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 {
-
        Self(Paint {
-
            item: paint.item.replace('\n', " "),
-
            style: paint.style,
-
        })
-
    }
-
}
-

-
impl From<Paint<&str>> for Label {
-
    fn from(paint: Paint<&str>) -> Self {
+
impl<D: fmt::Display> From<Paint<D>> for Label {
+
    fn from(paint: Paint<D>) -> Self {
        Self(Paint {
-
            item: paint.item.replace('\n', " "),
+
            item: paint.item.to_string().replace('\n', " "),
            style: paint.style,
        })
    }
modified radicle-term/src/lib.rs
@@ -1,5 +1,6 @@
pub mod ansi;
pub mod cell;
+
pub mod colors;
pub mod command;
pub mod editor;
pub mod element;
modified radicle-term/src/vstack.rs
@@ -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());
+
                    }
                }
            }
        }