Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli/rad: issue list --output json
✗ CI failure WillForan committed 2 months ago
commit e659621fb8d5332bedfad8ea48ea7e13fe4e907e
parent 9ff67562cbbc8abba3fc3b97831c575db289b8e7
1 failed (1 total) View logs
3 files changed +108 -47
modified crates/radicle-cli/examples/rad-issue.md
@@ -114,3 +114,24 @@ $ rad issue comment d87dcfe --edit 880fdcd -m "Even more power!"
│ Even more power!        │
╰─────────────────────────╯
```
+

+
For programmatic use or downstream customization, it may be useful to get issue lists in json format.
+
```
+
$ rad issue list --output json
+
[
+
  {
+
    "id": "d87dcfe8c2b3200e78b128d9b959cfdf7063fefe",
+
    "state": {
+
      "status": "open"
+
    },
+
    "title": "flux capacitor underpowered",
+
    "author": "alice",
+
    "did": "(you)",
+
    "labels": [
+
      "good-first-issue"
+
    ],
+
    "assignees": [],
+
    "opened": 1671125284000
+
  }
+
]
+
```
modified crates/radicle-cli/src/commands/issue.rs
@@ -3,11 +3,13 @@ mod cache;
mod comment;

use anyhow::Context as _;
+
use serde_json as json;

use radicle::cob::common::Label;
use radicle::cob::issue::{CloseReason, State};
use radicle::cob::{issue, Title};

+
use radicle::cob::Timestamp;
use radicle::crypto;
use radicle::issue::cache::Issues as _;
use radicle::node::device::Device;
@@ -18,6 +20,7 @@ use radicle::storage;
use radicle::storage::{WriteRepository, WriteStorage};
use radicle::Profile;
use radicle::{cob, Node};
+
use radicle_cob::ObjectId;

pub use args::Args;
use args::{Assigned, Command, CommentAction, StateArg};
@@ -30,6 +33,8 @@ use crate::terminal::format::Author;
use crate::terminal::issue::Format;
use crate::terminal::Element;

+
use crate::commands::issue::args::OutputFormat;
+

const ABOUT: &str = "Manage issues";

pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
@@ -200,6 +205,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                issues,
                &list_args.assigned,
                &((&list_args.state).into()),
+
                &list_args.output,
                &profile,
                args.verbose,
            )?;
@@ -239,10 +245,23 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    Ok(())
}

+
#[derive(serde::Serialize)]
+
pub struct IssueSummary {
+
    id: ObjectId,
+
    state: State,
+
    title: String,
+
    author: String,
+
    did: String,
+
    labels: Vec<String>,
+
    assignees: Vec<String>,
+
    opened: Timestamp,
+
}
+

fn list<C>(
    cache: C,
    assigned: &Option<Assigned>,
    state: &Option<State>,
+
    output: &OutputFormat,
    profile: &profile::Profile,
    verbose: bool,
) -> anyhow::Result<()>
@@ -283,85 +302,94 @@ where
                    return None;
                }
            }
+
            Some((id, issue))}).
+
        // pull out aliases for author and assignees, sort labels
+
        map(|(id, issue)| {
+
            let assignees: Vec<String> = issue
+
                .assignees()
+
                .map(|did| {
+
                    let (alias, _) = Author::new(did.as_key(), profile, false).labels();
+

+
                    alias.content().to_owned()
+
                })
+
                .collect::<Vec<_>>();

-
            Some((id, issue))
+
            let mut labels = issue.labels().map(|t| t.to_string()).collect::<Vec<_>>();
+
            labels.sort();
+

+
            let author = issue.author().id;
+
            let (alias, did) = Author::new(&author, profile, false).labels();
+

+
            IssueSummary {
+
                id: id,
+
                state: issue.state().to_owned(),
+
                title: issue.title().to_string(),
+
                author: alias.to_string(),
+
                did: did.to_string(),
+
                labels: labels.to_owned(),
+
                assignees: assignees,
+
                opened: issue.timestamp()}
        })
        .collect::<Vec<_>>();

-
    all.sort_by(|(id1, i1), (id2, i2)| {
-
        let by_timestamp = i2.timestamp().cmp(&i1.timestamp());
-
        let by_id = id1.cmp(id2);
+
    all.sort_by(|i1, i2| {
+
        let by_timestamp = i2.opened.cmp(&i1.opened);
+
        let by_id = i1.id.cmp(&i2.id);

        by_timestamp.then(by_id)
    });

+
    match output {
+
        OutputFormat::Table => {
+
            print_table(all);
+
        }
+
        OutputFormat::Json => {
+
            println!("{}", json::to_string_pretty(&all)?);
+
        }
+
    }
+

+
    Ok(())
+
}
+

+
fn print_table(issues: Vec<IssueSummary>) {
    let mut table = term::Table::new(term::table::TableOptions::bordered());
    table.header([
        term::format::dim(String::from("●")).into(),
        term::format::bold(String::from("ID")).into(),
        term::format::bold(String::from("Title")).into(),
        term::format::bold(String::from("Author")).into(),
-
        term::Line::blank(),
+
        term::Line::blank(), // DID
        term::format::bold(String::from("Labels")).into(),
        term::format::bold(String::from("Assignees")).into(),
        term::format::bold(String::from("Opened")).into(),
    ]);
    table.divider();

-
    table.extend(all.into_iter().map(|(id, issue)| {
-
        let assigned: String = issue
-
            .assignees()
-
            .map(|did| {
-
                let (alias, _) = Author::new(did.as_key(), profile, verbose).labels();
-

-
                alias.content().to_owned()
-
            })
-
            .collect::<Vec<_>>()
-
            .join(", ");
-

-
        let mut labels = issue.labels().map(|t| t.to_string()).collect::<Vec<_>>();
-
        labels.sort();
-

-
        let author = issue.author().id;
-
        let (alias, did) = Author::new(&author, profile, verbose).labels();
-

-
        mk_issue_row(id, issue, assigned, labels, alias, did)
-
    }));
-

+
    table.extend(issues.into_iter().map(|issue| mk_issue_row(issue)));
    table.print();
-

-
    Ok(())
}

-
fn mk_issue_row(
-
    id: cob::ObjectId,
-
    issue: issue::Issue,
-
    assigned: String,
-
    labels: Vec<String>,
-
    alias: radicle_term::Label,
-
    did: radicle_term::Label,
-
) -> [radicle_term::Line; 8] {
+
fn mk_issue_row(issue: IssueSummary) -> [radicle_term::Line; 8] {
    [
-
        match issue.state() {
+
        match issue.state {
            State::Open => term::format::positive("●").into(),
            State::Closed { .. } => term::format::negative("●").into(),
        },
-
        term::format::tertiary(term::format::cob(&id))
+
        term::format::tertiary(term::format::cob(&issue.id))
            .to_owned()
            .into(),
-
        term::format::default(issue.title().to_owned()).into(),
-
        alias.into(),
-
        did.into(),
-
        term::format::secondary(labels.join(", ")).into(),
-
        if assigned.is_empty() {
+
        term::format::default(issue.title).into(),
+
        issue.author.into(),
+
        issue.did.into(),
+
        term::format::secondary(issue.labels.join(", ")).into(),
+
        if issue.assignees.is_empty() {
            term::format::dim(String::default()).into()
        } else {
-
            term::format::primary(assigned.to_string()).dim().into()
+
            term::format::primary(issue.assignees.join(", "))
+
                .dim()
+
                .into()
        },
-
        term::format::timestamp(issue.timestamp())
-
            .dim()
-
            .italic()
-
            .into(),
+
        term::format::timestamp(issue.opened).dim().italic().into(),
    ]
}

modified crates/radicle-cli/src/commands/issue/args.rs
@@ -17,6 +17,13 @@ pub enum Assigned {
    Peer(Did),
}

+
#[derive(Default, Debug, Clone, PartialEq, Eq, clap::ValueEnum)]
+
pub enum OutputFormat {
+
    #[default]
+
    Table,
+
    Json,
+
}
+

#[derive(Parser, Debug)]
#[command(about = super::ABOUT, disable_version_flag = true)]
pub struct Args {
@@ -240,6 +247,10 @@ pub(crate) struct ListArgs {
    #[arg(num_args = 0..=1)]
    pub(crate) assigned: Option<Assigned>,

+
    /// How to display results
+
    #[arg(long, name = "OUTPUT", default_value = "table")]
+
    pub(crate) output: OutputFormat,
+

    #[clap(flatten)]
    pub(crate) state: ListStateArgs,
}
@@ -296,6 +307,7 @@ impl From<EmptyArgs> for ListArgs {
        Self {
            assigned: args.assigned,
            state: ListStateArgs::from(args.state),
+
            output: OutputFormat::Table,
        }
    }
}