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 8 months ago
commit 8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a
parent f00d1d67432882bef11fc940601f071efe55c88d
3 failed (3 total) View logs
1 file changed +100 -37
modified crates/radicle-cli/src/commands/issue.rs
@@ -6,6 +6,7 @@ use std::ffi::OsString;
use std::str::FromStr;

use anyhow::{anyhow, Context as _};
+
use serde_json as json;

use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue::{CloseReason, State};
@@ -21,6 +22,8 @@ use radicle::storage;
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
use radicle::Profile;
use radicle::{cob, Node};
+
use radicle_cob::ObjectId;
+
use radicle::cob::Timestamp;

use crate::git::Rev;
use crate::node;
@@ -41,7 +44,7 @@ Usage
    rad issue [<option>...]
    rad issue delete <issue-id> [<option>...]
    rad issue edit <issue-id> [--title <title>] [--description <text>] [<option>...]
-
    rad issue list [--assigned <did>] [--all | --closed | --open | --solved] [<option>...]
+
    rad issue list [--assigned <did>] [--all | --closed | --open | --solved] [--output {table,json}] [<option>...]
    rad issue open [--title <title>] [--description <text>] [--label <label>] [<option>...]
    rad issue react <issue-id> [--emoji <char>] [--to <comment>] [<option>...]
    rad issue assign <issue-id> [--add <did>] [--delete <did>] [<option>...]
@@ -65,6 +68,12 @@ Label options

    Note: --add takes precedence over --delete

+
List options
+

+
    --output              'table' or 'json'. Use 'json' with e.g. 'jq' to customize output
+
                             rad issue list --output json |
+
                             jq -r '.[]|[(.author+" "+.did), (.opened/1000|todate), .title]|@tsv'
+

Show options

    -v, --verbose          Show additional information about the issue
@@ -95,6 +104,14 @@ pub enum OperationName {
    Cache,
}

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

+

/// Command line Peer argument.
#[derive(Default, Debug, PartialEq, Eq)]
pub enum Assigned {
@@ -154,6 +171,7 @@ pub enum Operation {
    List {
        assigned: Option<Assigned>,
        state: Option<State>,
+
        output: Option<OutputFormat>,
    },
    Cache {
        id: Option<Rev>,
@@ -197,6 +215,7 @@ impl Args for Options {
        let mut labels = Vec::new();
        let mut assignees = Vec::new();
        let mut format = Format::default();
+
        let mut output = Some(OutputFormat::default());
        let mut message = Message::default();
        let mut reply_to = None;
        let mut edit_comment = None;
@@ -231,6 +250,16 @@ impl Args for Options {
                        reason: CloseReason::Solved,
                    });
                }
+
                Long("output") if op == Some(OperationName::List) => {
+
                    let val = parser.value()?;
+
                    let val = term::args::string(&val);
+

+
                    match val.as_str() {
+
                        "table" => output = Some(OutputFormat::Table),
+
                        "json" => output = Some(OutputFormat::Json),
+
                        _ => anyhow::bail!("unknown output '{val}' not 'table' or 'json'."),
+
                    }
+
                }

                // Open/Edit options.
                Long("title")
@@ -296,6 +325,7 @@ impl Args for Options {
                        _ => anyhow::bail!("unknown format '{val}'"),
                    }
                }
+

                Long("verbose") | Short('v') if op == Some(OperationName::Show) => {
                    verbose = true;
                }
@@ -453,7 +483,9 @@ impl Args for Options {
                id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
                opts: label_opts,
            },
-
            OperationName::List => Operation::List { assigned, state },
+
            OperationName::List => Operation::List {
+
                assigned, state, output
+
            },
            OperationName::Cache => Operation::Cache {
                id,
                storage: cache_storage,
@@ -677,8 +709,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                .collect::<Vec<_>>();
            issue.label(labels, &signer)?;
        }
-
        Operation::List { assigned, state } => {
-
            list(issues, &assigned, &state, &profile)?;
+
        Operation::List { assigned, state, output } => {
+
            list(issues, &assigned, &state, &output, &profile)?;
        }
        Operation::Delete { id } => {
            let signer = term::signer(&profile)?;
@@ -715,10 +747,23 @@ pub fn run(options: Options, 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: &Option<OutputFormat>,
    profile: &profile::Profile,
) -> anyhow::Result<()>
where
@@ -758,74 +803,92 @@ 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 {
+
        Some(OutputFormat::Table) => {print_table(all); }
+
        Some(OutputFormat::Json) => {println!("{}",json::to_string_pretty(&all)?);},
+
        &None => {println!("Unknown ouptput format!");}
+
    }
+

+
    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();

-
    for (id, issue) in all {
-
        let assigned: String = issue
-
            .assignees()
-
            .map(|did| {
-
                let (alias, _) = Author::new(did.as_key(), profile, false).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, false).labels();
-

+
    for issue in issues {
        table.push([
-
            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())
+
            term::format::timestamp(issue.opened)
                .dim()
                .italic()
                .into(),
        ]);
    }
    table.print();
-

-
    Ok(())
}

fn open<R, G>(