Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: Improve `rad inbox` output
Merged did:key:z6MksFqX...wzpT opened 2 years ago
  • Add change authors
  • Add object status
  • Add colors
5 files changed +162 -77 9576a649 9767b485
modified 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
-
╭──────────────────────────────────────────────────────────────╮
-
│ heartwood                                                    │
-
├──────────────────────────────────────────────────────────────┤
-
│ 1   ●   issue    No license file    [  ..  ]   opened    now │
-
│ 2   ●   branch   Change copyright   bob/copy   created   now │
-
╰──────────────────────────────────────────────────────────────╯
-
╭──────────────────────────────────────────────────────────╮
-
│ radicle-git                                              │
-
├──────────────────────────────────────────────────────────┤
-
│ 3   ●   patch   Copyright fixes   [ ... ]   opened   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
modified 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
modified 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, &notifs.read_only(), storage),
+
        Operation::List => list(mode, sort_by, &notifs.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: &notifications::StoreReader,
    storage: &Storage,
+
    profile: &Profile,
) -> anyhow::Result<()> {
    let repos: Vec<term::VStack<'_>> = 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,17 @@ fn list_all<'a>(
    sort_by: SortBy,
    notifs: &notifications::StoreReader,
    storage: &Storage,
+
    profile: &Profile,
) -> anyhow::Result<Vec<term::VStack<'a>>> {
-
    let mut repos = Vec::new();
-
    for repo in storage.repositories()? {
-
        let repo = list_repo(repo.rid, sort_by, notifs, storage)?;
-
        repos.extend(repo.into_iter());
+
    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, profile)?;
+
        vstacks.extend(vstack.into_iter());
    }
-
    Ok(repos)
+
    Ok(vstacks)
}

fn list_repo<'a, R: ReadStorage>(
@@ -214,6 +218,7 @@ fn list_repo<'a, R: ReadStorage>(
    sort_by: SortBy,
    notifs: &notifications::StoreReader,
    storage: &R,
+
    profile: &Profile,
) -> anyhow::Result<Option<term::VStack<'a>>>
where
    <R as ReadStorage>::Repository: cob::Store,
@@ -223,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)?;
@@ -242,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::<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 } => {
-
                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())
-
                };
-
                let status = match n.update {
-
                    RefUpdate::Updated { .. } => "updated",
-
                    RefUpdate::Created { .. } => "opened",
-
                    RefUpdate::Deleted { .. } => "deleted",
-
                    RefUpdate::Skipped { .. } => "skipped",
+
                    (
+
                        type_name.to_string(),
+
                        "".to_owned(),
+
                        term::format::default(String::new()),
+
                    )
                };
-
                (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(),
        ]);
    }

@@ -293,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),
        ))
modified 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())),
        }
    }
}
modified 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<D: fmt::Display>(cmd: D) -> Paint<String> {
}

/// Format a COB id.
-
pub fn cob(id: &ObjectId) -> String {
-
    format!("{:.7}", id.to_string())
+
pub fn cob(id: &ObjectId) -> Paint<String> {
+
    Paint::new(format!("{:.7}", id.to_string()))
}

/// Format a DID.
@@ -72,6 +73,16 @@ pub fn timestamp(time: impl Into<LocalTime>) -> Paint<String> {
    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
/// `<node-id> (<username>)` 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<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)]
mod test {
    use super::*;