rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af
{
"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:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "6d1d8c74468f98a693823446a75e7a4f66c43527",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"title": "term: Table to JSON",
"state": {
"status": "draft",
"conflicts": []
},
"before": "e130b4dc06ca02a035519e1ea86ffeafc788866f",
"after": "83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af",
"commits": [
"83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af",
"6d18836edaf624bd56f3ca4b7eacbdf09e31be95"
],
"target": "3419821106e274c96f56948a05d2f51f28f5a6c7",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "6d1d8c74468f98a693823446a75e7a4f66c43527",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "e130b4dc06ca02a035519e1ea86ffeafc788866f",
"oid": "83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af",
"timestamp": 1727206951
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "330a2a43-b597-4d8d-b993-ac64b7d2d1ac"
},
"info_url": "https://cci.rad.levitte.org//330a2a43-b597-4d8d-b993-ac64b7d2d1ac.html"
}
Started at: 2026-09-08 13:52:23.494916+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ 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/330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 39 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 6d1d8c74468f98a693823446a75e7a4f66c43527
✓ Switched to branch patch/6d1d8c7 at revision 6d1d8c7
✓ Branch patch/6d1d8c7 setup to track rad/patches/6d1d8c74468f98a693823446a75e7a4f66c43527
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af
HEAD is now at 83bc28ed term: Table to JSON
Exit code: 0
$ rad patch show 6d1d8c74468f98a693823446a75e7a4f66c43527 -p
╭────────────────────────────────────────────────────────────────────────────╮
│ Title term: Table to JSON │
│ Patch 6d1d8c74468f98a693823446a75e7a4f66c43527 │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af │
│ Base e130b4dc06ca02a035519e1ea86ffeafc788866f │
│ Target master │
│ Branches patch/6d1d8c7 │
│ Commits ahead 2, behind 1191 │
│ Status draft │
├────────────────────────────────────────────────────────────────────────────┤
│ 83bc28e term: Table to JSON │
│ 6d18836 cli: Use `term::Table::header` where possible │
├────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 6d1d8c7 @ e130b4d..83bc28e by lorenz z6MkkPv…WX5sTEz 1 year ago │
╰────────────────────────────────────────────────────────────────────────────╯
commit 83bc28ed0a0a00f5a4ea603cec3cf8b21096e3af
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Tue Sep 24 21:42:17 2024 +0200
term: Table to JSON
diff --git a/Cargo.lock b/Cargo.lock
index 9feb012a8..ac8d27a52 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2164,6 +2164,8 @@ dependencies = [
"once_cell",
"pretty_assertions",
"radicle-signals",
+ "serde",
+ "serde_json",
"shlex",
"tempfile",
"termion 3.0.0",
diff --git a/radicle-cli/src/commands/follow.rs b/radicle-cli/src/commands/follow.rs
index f8bbc9289..a21c3dadc 100644
--- a/radicle-cli/src/commands/follow.rs
+++ b/radicle-cli/src/commands/follow.rs
@@ -44,10 +44,17 @@ pub enum OperationName {
List,
}
+#[derive(Debug)]
+pub enum OutputFormat {
+ Text,
+ Json,
+}
+
#[derive(Debug)]
pub struct Options {
pub op: Operation,
pub verbose: bool,
+ pub output_format: OutputFormat,
}
impl Args for Options {
@@ -58,6 +65,7 @@ impl Args for Options {
let mut verbose = false;
let mut nid: Option<NodeId> = None;
let mut alias: Option<Alias> = None;
+ let mut output_format = OutputFormat::Text;
while let Some(arg) = parser.next()? {
match &arg {
@@ -76,6 +84,7 @@ impl Args for Options {
alias = Some(name.to_owned());
}
+ Long("json") => output_format = OutputFormat::Json,
Long("verbose") | Short('v') => verbose = true,
Long("help") | Short('h') => {
return Err(Error::Help.into());
@@ -90,7 +99,7 @@ impl Args for Options {
Some(nid) => Operation::Follow { nid, alias },
None => Operation::List { alias },
};
- Ok((Options { op, verbose }, vec![]))
+ Ok((Options { op, verbose, output_format }, vec![]))
}
}
@@ -100,7 +109,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
match options.op {
Operation::Follow { nid, alias } => follow(nid, alias, &mut node, &profile)?,
- Operation::List { alias } => following(&profile, alias)?,
+ Operation::List { alias } => following(&profile, alias, options.output_format)?,
}
Ok(())
@@ -137,7 +146,7 @@ pub fn follow(
Ok(())
}
-pub fn following(profile: &Profile, alias: Option<Alias>) -> anyhow::Result<()> {
+pub fn following(profile: &Profile, alias: Option<Alias>, output_format: OutputFormat) -> anyhow::Result<()> {
let store = profile.policies()?;
let aliases = profile.aliases();
let mut t = term::Table::new(term::table::TableOptions::bordered());
@@ -158,7 +167,14 @@ pub fn following(profile: &Profile, alias: Option<Alias>) -> anyhow::Result<()>
.filter(|p| p.alias.as_ref().is_some_and(|alias_| *alias_ == alias)),
),
};
- t.print();
+
+ match output_format {
+ OutputFormat::Text => t.print(),
+ OutputFormat::Json => {
+ Paint::disable();
+ serde_json::to_writer_pretty(std::io::stdout(), &t.to_json())?
+ }
+ }
Ok(())
}
diff --git a/radicle-term/Cargo.toml b/radicle-term/Cargo.toml
index 204790350..1ca264619 100644
--- a/radicle-term/Cargo.toml
+++ b/radicle-term/Cargo.toml
@@ -18,6 +18,8 @@ crossbeam-channel = { version = "0.5.6" }
inquire = { version = "0.7.4", default-features = false, features = ["termion", "editor"] }
libc = { version = "0.2" }
once_cell = { version = "1.13" }
+serde = { version = "1" }
+serde_json = { version = "1" }
shlex = { version = "1.1" }
termion = { version = "3" }
thiserror = { version = "1" }
diff --git a/radicle-term/src/table.rs b/radicle-term/src/table.rs
index 8cc8ff34a..66476f814 100644
--- a/radicle-term/src/table.rs
+++ b/radicle-term/src/table.rs
@@ -223,6 +223,31 @@ impl<const W: usize, T: Cell> Table<W, T> {
}
Size::new(cols, rows).constrain(c)
}
+
+ pub fn to_json(&self) -> serde_json::Value {
+ assert!(self.rows.len() > 1);
+
+ let header = {
+ match &self.rows[0] {
+ Row::Header(header) => header,
+ _ => panic!("Cannot convert table to JSON. Expecting header in first row, but encountered row of different variant."),
+ }
+ };
+
+ serde_json::Value::Array(self.rows[1..].iter().enumerate().filter_map(|(index, row)| {
+ match row {
+ Row::Data(cells) => {
+ let mut obj = serde_json::Map::new();
+ header.iter().zip(cells.iter()).for_each(|(key, value)| {
+ obj.insert(key.to_string(), serde_json::Value::String(value.to_string()));
+ });
+ Some(serde_json::Value::Object(obj))
+ },
+ Row::Divider => None,
+ Row::Header(_) => panic!("Cannot convert table to JSON. Encountered unexpected header in row {}.", index + 1),
+ }
+ }).collect())
+ }
}
#[cfg(test)]
commit 6d18836edaf624bd56f3ca4b7eacbdf09e31be95
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Thu Sep 12 20:42:10 2024 +0200
cli: Use `term::Table::header` where possible
diff --git a/radicle-cli/src/commands/follow.rs b/radicle-cli/src/commands/follow.rs
index 4713cb553..f8bbc9289 100644
--- a/radicle-cli/src/commands/follow.rs
+++ b/radicle-cli/src/commands/follow.rs
@@ -141,7 +141,7 @@ pub fn following(profile: &Profile, alias: Option<Alias>) -> anyhow::Result<()>
let store = profile.policies()?;
let aliases = profile.aliases();
let mut t = term::Table::new(term::table::TableOptions::bordered());
- t.push([
+ t.header([
term::format::default(String::from("DID")),
term::format::default(String::from("Alias")),
term::format::default(String::from("Policy")),
diff --git a/radicle-cli/src/commands/id.rs b/radicle-cli/src/commands/id.rs
index ba8dcbd1d..c18307945 100644
--- a/radicle-cli/src/commands/id.rs
+++ b/radicle-cli/src/commands/id.rs
@@ -504,7 +504,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let mut revisions =
term::Table::<7, term::Label>::new(term::table::TableOptions::bordered());
- revisions.push([
+ revisions.header([
term::format::dim(String::from("●")).into(),
term::format::bold(String::from("ID")).into(),
term::format::bold(String::from("Title")).into(),
diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs
index 08220e897..e0f084775 100644
--- a/radicle-cli/src/commands/issue.rs
+++ b/radicle-cli/src/commands/issue.rs
@@ -664,7 +664,7 @@ where
});
let mut table = term::Table::new(term::table::TableOptions::bordered());
- table.push([
+ table.header([
term::format::dim(String::from("●")).into(),
term::format::bold(String::from("ID")).into(),
term::format::bold(String::from("Title")).into(),
diff --git a/radicle-cli/src/commands/ls.rs b/radicle-cli/src/commands/ls.rs
index c26cd8400..a0527b7ef 100644
--- a/radicle-cli/src/commands/ls.rs
+++ b/radicle-cli/src/commands/ls.rs
@@ -148,7 +148,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if rows.is_empty() {
term::print(term::format::italic("Nothing to show."));
} else {
- table.push([
+ table.header([
"Name".into(),
"RID".into(),
"Visibility".into(),
diff --git a/radicle-cli/src/commands/node/control.rs b/radicle-cli/src/commands/node/control.rs
index 5e86a957b..3d8d53ebc 100644
--- a/radicle-cli/src/commands/node/control.rs
+++ b/radicle-cli/src/commands/node/control.rs
@@ -246,7 +246,7 @@ pub fn sessions(node: &Node) -> Result<Option<term::Table<4, term::Label>>, node
let mut table = term::Table::new(term::table::TableOptions::bordered());
let now = LocalTime::now();
- table.push([
+ table.header([
term::format::bold("Peer").into(),
term::format::bold("Address").into(),
term::format::bold("State").into(),
diff --git a/radicle-cli/src/commands/patch/list.rs b/radicle-cli/src/commands/patch/list.rs
index 3edf5c91a..14fb6a8b5 100644
--- a/radicle-cli/src/commands/patch/list.rs
+++ b/radicle-cli/src/commands/patch/list.rs
@@ -56,7 +56,7 @@ pub fn run(
..TableOptions::default()
});
- table.push([
+ table.header([
term::format::dim(String::from("●")).into(),
term::format::bold(String::from("ID")).into(),
term::format::bold(String::from("Title")).into(),
diff --git a/radicle-cli/src/commands/sync.rs b/radicle-cli/src/commands/sync.rs
index 87279c764..1b85efc66 100644
--- a/radicle-cli/src/commands/sync.rs
+++ b/radicle-cli/src/commands/sync.rs
@@ -320,7 +320,7 @@ fn sync_status(
let local_nid = node.nid()?;
let aliases = profile.aliases();
- table.push([
+ table.header([
term::format::dim(String::from("●")).into(),
term::format::bold(String::from("Node")).into(),
term::Label::blank(),
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 330a2a43-b597-4d8d-b993-ac64b7d2d1ac -v /opt/radcis/ci.rad.levitte.org/cci/state/330a2a43-b597-4d8d-b993-ac64b7d2d1ac/s:/330a2a43-b597-4d8d-b993-ac64b7d2d1ac/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w:/330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w -w /330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /330a2a43-b597-4d8d-b993-ac64b7d2d1ac/s/script.sh
+ cargo --version
info: syncing channel updates for '1.80-x86_64-unknown-linux-gnu'
info: latest update on 2024-08-08, rust version 1.80.1 (3f5fd8dd4 2024-08-06)
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.80.1 (376290515 2024-07-16)
+ rustc --version
rustc 1.80.1 (3f5fd8dd4 2024-08-06)
+ cargo fmt --check
Diff in /330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w/radicle-cli/src/commands/follow.rs at line 99:
Some(nid) => Operation::Follow { nid, alias },
None => Operation::List { alias },
};
- Ok((Options { op, verbose, output_format }, vec![]))
+ Ok((
+ Options {
+ op,
+ verbose,
+ output_format,
+ },
+ vec![],
+ ))
}
}
Diff in /330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w/radicle-cli/src/commands/follow.rs at line 146:
Ok(())
}
-pub fn following(profile: &Profile, alias: Option<Alias>, output_format: OutputFormat) -> anyhow::Result<()> {
+pub fn following(
+ profile: &Profile,
+ alias: Option<Alias>,
+ output_format: OutputFormat,
+) -> anyhow::Result<()> {
let store = profile.policies()?;
let aliases = profile.aliases();
let mut t = term::Table::new(term::table::TableOptions::bordered());
Diff in /330a2a43-b597-4d8d-b993-ac64b7d2d1ac/w/radicle-term/src/table.rs at line 234:
}
};
- serde_json::Value::Array(self.rows[1..].iter().enumerate().filter_map(|(index, row)| {
- match row {
- Row::Data(cells) => {
- let mut obj = serde_json::Map::new();
- header.iter().zip(cells.iter()).for_each(|(key, value)| {
- obj.insert(key.to_string(), serde_json::Value::String(value.to_string()));
- });
- Some(serde_json::Value::Object(obj))
- },
- Row::Divider => None,
- Row::Header(_) => panic!("Cannot convert table to JSON. Encountered unexpected header in row {}.", index + 1),
- }
- }).collect())
+ serde_json::Value::Array(
+ self.rows[1..]
+ .iter()
+ .enumerate()
+ .filter_map(|(index, row)| match row {
+ Row::Data(cells) => {
+ let mut obj = serde_json::Map::new();
+ header.iter().zip(cells.iter()).for_each(|(key, value)| {
+ obj.insert(
+ key.to_string(),
+ serde_json::Value::String(value.to_string()),
+ );
+ });
+ Some(serde_json::Value::Object(obj))
+ }
+ Row::Divider => None,
+ Row::Header(_) => panic!(
+ "Cannot convert table to JSON. Encountered unexpected header in row {}.",
+ index + 1
+ ),
+ })
+ .collect(),
+ )
}
}
Exit code: 1
{
"response": "finished",
"result": "failure"
}