rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "6e63c4b82080cbab1d644b2744b0443e0087fdcc",
"author": {
"id": "did:key:z6MkuCCEhu83W87Nnu2EEdBT4brhzR2fmiGGJRUYKhCESEyW",
"alias": "will"
},
"title": "cli/rad: issue list --output json",
"state": {
"status": "open",
"conflicts": []
},
"before": "f00d1d67432882bef11fc940601f071efe55c88d",
"after": "8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a",
"commits": [
"8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a"
],
"target": "f00d1d67432882bef11fc940601f071efe55c88d",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "6e63c4b82080cbab1d644b2744b0443e0087fdcc",
"author": {
"id": "did:key:z6MkuCCEhu83W87Nnu2EEdBT4brhzR2fmiGGJRUYKhCESEyW",
"alias": "will"
},
"description": "json output added to put customized output into the users' control.\n\nIssue: 2e51b37\nTopic: https://radicle.zulipchat.com/#narrow/channel/369873-Support/topic/.60rad.20issue.20list.60.20as.20tsv.20or.20json.3F/with/538037563\n\nRefactored `list` command to collect new `IssueSummary` struct via `map`.\n\nMove table printing into dedicated function.\nDispatched on new `output` format option (`OutputFormat`).\nCurrently, options are 'table' or new 'json' via `serde_json`.\n\nAn alternative to this code might be using `cache.db` with sqlite3.\nBut getting author and assignee aliases from DID isn't easy (?)\n\n```\nsqlite3 $(rad self --home)/cobs/cache.db \"\n select json_group_array(json_insert(json_extract(issue,'$'),'$.id',id))\n from issues\n where repo = '$(rad .)'\"|\n jq '.[] |\n [.id, .state.status,\n ([(.thread.comments[]|[(.edits[0].timestamp/1000|todate),.author])]|sort[0]),\n .title]|\n flatten(1) | @tsv' -r\n ```",
"base": "f00d1d67432882bef11fc940601f071efe55c88d",
"oid": "8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a",
"timestamp": 1757213064
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "c5fe4485-74d7-497b-a33e-a94a44d3fd69"
},
"info_url": "https://cci.rad.levitte.org//c5fe4485-74d7-497b-a33e-a94a44d3fd69.html"
}
Started at: 2025-09-07 04:44:32.356668+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 116 issues · 15 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 6e63c4b82080cbab1d644b2744b0443e0087fdcc
✓ Switched to branch patch/6e63c4b at revision 6e63c4b
✓ Branch patch/6e63c4b setup to track rad/patches/6e63c4b82080cbab1d644b2744b0443e0087fdcc
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a
HEAD is now at 8e969002 cli/rad: issue list --output json
Exit code: 0
$ git show 8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a
commit 8e96900267cb3d5c1e0d63a6f2f669afcccaeb8a
Author: WillForan <willforan@gmail.com>
Date: Sat Sep 6 21:46:43 2025 -0400
cli/rad: issue list --output json
json output added to put customized output into the users' control.
Issue: 2e51b37
Topic: https://radicle.zulipchat.com/#narrow/channel/369873-Support/topic/.60rad.20issue.20list.60.20as.20tsv.20or.20json.3F/with/538037563
Refactored `list` command to collect new `IssueSummary` struct via `map`.
Move table printing into dedicated function.
Dispatched on new `output` format option (`OutputFormat`).
Currently, options are 'table' or new 'json' via `serde_json`.
An alternative to this code might be using `cache.db` with sqlite3.
But getting author and assignee aliases from DID isn't easy (?)
```
sqlite3 $(rad self --home)/cobs/cache.db "
select json_group_array(json_insert(json_extract(issue,'$'),'$.id',id))
from issues
where repo = '$(rad .)'"|
jq '.[] |
[.id, .state.status,
([(.thread.comments[]|[(.edits[0].timestamp/1000|todate),.author])]|sort[0]),
.title]|
flatten(1) | @tsv' -r
```
diff --git a/crates/radicle-cli/src/commands/issue.rs b/crates/radicle-cli/src/commands/issue.rs
index 8465cbae..69645cf6 100644
--- a/crates/radicle-cli/src/commands/issue.rs
+++ b/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>(
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name c5fe4485-74d7-497b-a33e-a94a44d3fd69 -v /opt/radcis/ci.rad.levitte.org/cci/state/c5fe4485-74d7-497b-a33e-a94a44d3fd69/s:/c5fe4485-74d7-497b-a33e-a94a44d3fd69/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/c5fe4485-74d7-497b-a33e-a94a44d3fd69/w:/c5fe4485-74d7-497b-a33e-a94a44d3fd69/w -w /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /c5fe4485-74d7-497b-a33e-a94a44d3fd69/s/script.sh
time="2025-09-07T04:44:34+02:00" level=error msg="User-selected graph driver \"overlay\" overwritten by graph driver \"vfs\" from database - delete libpod local files (\"/opt/radcis/ci.rad.levitte.org/.local/share/containers/storage\") to resolve. May prevent use of images created by other tools"
time="2025-09-07T04:44:34+02:00" level=error msg="User-selected graph driver \"overlay\" overwritten by graph driver \"vfs\" from database - delete libpod local files (\"/opt/radcis/ci.rad.levitte.org/.local/share/containers/storage\") to resolve. May prevent use of images created by other tools"
+ cargo --version
info: syncing channel updates for '1.88-x86_64-unknown-linux-gnu'
info: latest update on 2025-06-26, rust version 1.88.0 (6b00bc388 2025-06-23)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.88.0 (873a06493 2025-05-10)
+ rustc --version
rustc 1.88.0 (6b00bc388 2025-06-23)
+ cargo fmt --check
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:10:
use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue::{CloseReason, State};
+use radicle::cob::Timestamp;
use radicle::cob::{issue, thread, Title};
use radicle::crypto;
use radicle::git::Oid;
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:23:
use radicle::Profile;
use radicle::{cob, Node};
use radicle_cob::ObjectId;
-use radicle::cob::Timestamp;
use crate::git::Rev;
use crate::node;
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:111:
Json,
}
-
/// Command line Peer argument.
#[derive(Default, Debug, PartialEq, Eq)]
pub enum Assigned {
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:484:
opts: label_opts,
},
OperationName::List => Operation::List {
- assigned, state, output
+ assigned,
+ state,
+ output,
},
OperationName::Cache => Operation::Cache {
id,
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:709:
.collect::<Vec<_>>();
issue.label(labels, &signer)?;
}
- Operation::List { assigned, state, output } => {
+ Operation::List {
+ assigned,
+ state,
+ output,
+ } => {
list(issues, &assigned, &state, &output, &profile)?;
}
Operation::Delete { id } => {
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:841:
});
match output {
- Some(OutputFormat::Table) => {print_table(all); }
- Some(OutputFormat::Json) => {println!("{}",json::to_string_pretty(&all)?);},
- &None => {println!("Unknown ouptput format!");}
+ Some(OutputFormat::Table) => {
+ print_table(all);
+ }
+ Some(OutputFormat::Json) => {
+ println!("{}", json::to_string_pretty(&all)?);
+ }
+ &None => {
+ println!("Unknown ouptput format!");
+ }
}
Ok(())
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:850:
}
-
-fn print_table(issues: Vec<IssueSummary>){
+fn print_table(issues: Vec<IssueSummary>) {
let mut table = term::Table::new(term::table::TableOptions::bordered());
table.header([
term::format::dim(String::from("●")).into(),
Diff in /c5fe4485-74d7-497b-a33e-a94a44d3fd69/w/crates/radicle-cli/src/commands/issue.rs:880:
if issue.assignees.is_empty() {
term::format::dim(String::default()).into()
} else {
- term::format::primary(issue.assignees.join(", ")).dim().into()
+ term::format::primary(issue.assignees.join(", "))
+ .dim()
+ .into()
},
- term::format::timestamp(issue.opened)
- .dim()
- .italic()
- .into(),
+ term::format::timestamp(issue.opened).dim().italic().into(),
]);
}
table.print();
Exit code: 1
{
"response": "finished",
"result": "failure"
}