rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoode36b2ae4c32901844f8c9c912d0db6d9376a32c5
{
"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": "4bce378ccf31f83d0bb6e38d01a29f901f6aa8e5",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"title": "cli: `rad cob {show \u2192 log}`, and new `rad cob show`",
"state": {
"status": "merged",
"conflicts": []
},
"before": "749e8239852aa4591277d946e585607dc4f464aa",
"after": "e36b2ae4c32901844f8c9c912d0db6d9376a32c5",
"commits": [
"e36b2ae4c32901844f8c9c912d0db6d9376a32c5",
"2fc2f2cf4179880ef10c6a3bfd84bfab11953450",
"45526fbd1bf0dcd1973f7ed1047a6fa1cea6899f"
],
"target": "3419821106e274c96f56948a05d2f51f28f5a6c7",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "4bce378ccf31f83d0bb6e38d01a29f901f6aa8e5",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Makes `rad cob show` print the COB as materialized object. The previous implementation of `rad cob show`, which showed operations on the COB, stays available as `rad cob log`.\n\nThis is a breaking change to `rad cob`.\n\nRelated discussion: <https://radicle.zulipchat.com/#narrow/stream/411091-dogfood/topic/Plumbing.20Command.20for.20COBs>",
"base": "6ab3bfcba0577fabdcb84498441c6605391290f4",
"oid": "fa1ecd6fc308365a299e418977fa8ccb8a9a9b72",
"timestamp": 1715098493
},
{
"id": "58fa929598d924ee12058f9f79993c09c66f1a4d",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "I decided to remove the custom format that `rad cob log ` uses in favor of a simple JSON encoding. This is easier to parse, and needs less maintenance.",
"base": "6ab3bfcba0577fabdcb84498441c6605391290f4",
"oid": "fc140d2f6780a20d6936d2bae7d936e95446d71e",
"timestamp": 1715110046
},
{
"id": "2098f8138eaad60611269c8aa06ffdcaa580480f",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "6ab3bfcba0577fabdcb84498441c6605391290f4",
"oid": "1517707bc4898e20df5603a9075959a6c00cbbd3",
"timestamp": 1715110174
},
{
"id": "e9e9e4147c518786161940dafca23f0f0381b51c",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "6ab3bfcba0577fabdcb84498441c6605391290f4",
"oid": "b45be7a10223aa6721a9087bec38379a837c51bf",
"timestamp": 1715132308
},
{
"id": "2f741674265fc4f99be991ca9e79d0f81f9a19e0",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Address comments after review from Fintan:\n\n - Add option `--pretty` to `rad cob show`\n - Add test for `rad cob show`",
"base": "6ab3bfcba0577fabdcb84498441c6605391290f4",
"oid": "2f14893c24ff2e72a1c966c03c96a8b718b7eeb6",
"timestamp": 1715190387
},
{
"id": "a19c19947defaf8c528775d2e015aca6960d28c8",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Clean up. Should be good to merge now.",
"base": "6ab3bfcba0577fabdcb84498441c6605391290f4",
"oid": "cb85f5a90fd9aabcd97117c3c33179fd0c64bca5",
"timestamp": 1715247361
},
{
"id": "16fcc0669eb08701c6c8847907b70dc2e62814d9",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Trivial rebase.",
"base": "5727359319f2b8ba6311f066391b7ab0004e5558",
"oid": "597e17586f43132263e0df050dd419c90ea92c65",
"timestamp": 1715776101
},
{
"id": "d5401011863beda39b93e2b5ac8d27f5156b0b9e",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Much prettier!",
"base": "749e8239852aa4591277d946e585607dc4f464aa",
"oid": "fbf53182cdf777e0fbd12744aa587d4aea15ec8a",
"timestamp": 1716305016
},
{
"id": "6bc09fd2d5878132de93a7a08ab7af99622551c9",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Fix output order",
"base": "749e8239852aa4591277d946e585607dc4f464aa",
"oid": "e36b2ae4c32901844f8c9c912d0db6d9376a32c5",
"timestamp": 1716368023
},
{
"id": "cbe3f07d550720badba205eada0502039436455f",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Rebase and minor output format changes.",
"base": "30c9b0db0ba5939c2949915c9f68c450700d7254",
"oid": "6d11ea17c88163ad4a5047c0ba4f7b7fd756e99d",
"timestamp": 1716371818
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "d6e38b2d-c1f0-467f-b927-f82df58533b8"
},
"info_url": "https://cci.rad.levitte.org//d6e38b2d-c1f0-467f-b927-f82df58533b8.html"
}
Started at: 2026-09-08 11:51:47.788146+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/d6e38b2d-c1f0-467f-b927-f82df58533b8/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 38 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 4bce378ccf31f83d0bb6e38d01a29f901f6aa8e5
✓ Switched to branch patch/4bce378 at revision 6bc09fd
✓ Branch patch/4bce378 setup to track rad/patches/4bce378ccf31f83d0bb6e38d01a29f901f6aa8e5
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout e36b2ae4c32901844f8c9c912d0db6d9376a32c5
HEAD is now at e36b2ae4 cli: Implement `rad cob show` with new meaning
Exit code: 0
$ rad patch show 4bce378ccf31f83d0bb6e38d01a29f901f6aa8e5 -p
╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Title cli: `rad cob {show → log}`, and new `rad cob show` │
│ Patch 4bce378ccf31f83d0bb6e38d01a29f901f6aa8e5 │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head e36b2ae4c32901844f8c9c912d0db6d9376a32c5 │
│ Base 749e8239852aa4591277d946e585607dc4f464aa │
│ Target master │
│ Branches patch/4bce378 │
│ Commits ahead 3, behind 1329 │
│ Status merged │
│ │
│ Makes `rad cob show` print the COB as materialized object. The previous │
│ implementation of `rad cob show`, which showed operations on the COB, stays │
│ available as `rad cob log`. │
│ │
│ This is a breaking change to `rad cob`. │
│ │
│ Related discussion: │
│ <https://radicle.zulipchat.com/#narrow/stream/411091-dogfood/topic/Plumbing.20Command.20for.20COBs> │
├─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ e36b2ae cli: Implement `rad cob show` with new meaning │
│ 2fc2f2c cli: Rework `rad cob log` options │
│ 45526fb cli: `rad cob {show → log}` │
├─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 4bce378 @ 6ab3bfc..fa1ecd6 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 58fa929 @ 6ab3bfc..fc140d2 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 2098f81 @ 6ab3bfc..1517707 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision e9e9e41 @ 6ab3bfc..b45be7a by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 2f74167 @ 6ab3bfc..2f14893 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision a19c199 @ 6ab3bfc..cb85f5a by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ └─ ✓ accepted by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 16fcc06 @ 5727359..597e175 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision d540101 @ 749e823..fbf5318 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 6bc09fd @ 749e823..e36b2ae by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision cbe3f07 @ 30c9b0d..6d11ea1 by cloudhead z6MksFq…bS9wzpT 2 years ago │
│ └─ ✓ merged by cloudhead z6MksFq…bS9wzpT │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯
commit e36b2ae4c32901844f8c9c912d0db6d9376a32c5
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Tue May 7 17:20:22 2024 +0200
cli: Implement `rad cob show` with new meaning
diff --git a/radicle-cli/examples/rad-cob-show.md b/radicle-cli/examples/rad-cob-show.md
new file mode 100644
index 000000000..12d035f4e
--- /dev/null
+++ b/radicle-cli/examples/rad-cob-show.md
@@ -0,0 +1,183 @@
+Well known COBs, for example issues and patches, can not only be showed via porcelain commands such as
+`rad issue show` and `rad patch show`, but also using the plumbing command `rad cob show`.
+While humans likely prefer to use `rad issue show` and `rad patch show`, this command makes integration
+with other software components easier.
+
+First create an issue.
+
+```
+$ rad issue open --title "spice harvester broken" --description "Fremen have attacked, maybe we went too far?" --no-announce
+╭──────────────────────────────────────────────────╮
+│ Title spice harvester broken │
+│ Issue 9de644864342d7a505eb8d58d1ef20e5bb05de2e │
+│ Author z6MknSL…StBU8Vi (you) │
+│ Status open │
+│ │
+│ Fremen have attacked, maybe we went too far? │
+╰──────────────────────────────────────────────────╯
+```
+
+The issue is now listed under our project.
+
+```
+$ rad issue list
+╭──────────────────────────────────────────────────────────────────────────────────────────────╮
+│ ● ID Title Author Labels Assignees Opened │
+├──────────────────────────────────────────────────────────────────────────────────────────────┤
+│ ● 9de6448 spice harvester broken z6MknSL…StBU8Vi (you) now │
+╰──────────────────────────────────────────────────────────────────────────────────────────────╯
+```
+
+Let's create a patch, too.
+
+```
+$ git checkout -b spice-harvester-broken
+$ touch TREATY.md
+$ git add TREATY.md
+$ git commit -v -m "Start drafting peace treaty"
+[spice-harvester-broken 575ed68] Start drafting peace treaty
+ 1 file changed, 0 insertions(+), 0 deletions(-)
+ create mode 100644 TREATY.md
+$ git push rad -o patch.message="Start drafting peace treaty" -o patch.message="See details." HEAD:refs/patches
+```
+
+Patch can be listed.
+
+```
+$ rad patch
+╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ ● ID Title Author Reviews Head + - Updated │
+├────────────────────────────────────────────────────────────────────────────────────────────────────┤
+│ ● d1f7f86 Start drafting peace treaty z6MknSL…StBU8Vi (you) - 575ed68 +0 -0 now │
+╰────────────────────────────────────────────────────────────────────────────────────────────────────╯
+```
+
+Both issue and patch COBs can be listed.
+
+```
+$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue
+9de644864342d7a505eb8d58d1ef20e5bb05de2e
+$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch
+d1f7f869fde9fac19c1779c4c2e77e8361333f91
+```
+
+We can show the issue COB.
+
+```
+$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object 9de644864342d7a505eb8d58d1ef20e5bb05de2e
+{
+ "assignees": [],
+ "title": "spice harvester broken",
+ "state": {
+ "status": "open"
+ },
+ "labels": [],
+ "thread": {
+ "comments": {
+ "9de644864342d7a505eb8d58d1ef20e5bb05de2e": {
+ "author": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "reactions": [],
+ "resolved": false,
+ "body": "Fremen have attacked, maybe we went too far?",
+ "edits": [
+ {
+ "author": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "timestamp": 1671125284000,
+ "body": "Fremen have attacked, maybe we went too far?",
+ "embeds": []
+ }
+ ]
+ }
+ },
+ "timeline": [
+ "9de644864342d7a505eb8d58d1ef20e5bb05de2e"
+ ]
+ }
+}
+```
+
+We can show the patch COB too.
+
+```
+$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object d1f7f869fde9fac19c1779c4c2e77e8361333f91
+{
+ "title": "Start drafting peace treaty",
+ "author": {
+ "id": "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
+ },
+ "state": {
+ "status": "open"
+ },
+ "target": "delegates",
+ "labels": [],
+ "merges": {},
+ "revisions": {
+ "d1f7f869fde9fac19c1779c4c2e77e8361333f91": {
+ "author": {
+ "id": "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
+ },
+ "description": [
+ {
+ "author": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "timestamp": 1671125284000,
+ "body": "See details.",
+ "embeds": []
+ }
+ ],
+ "base": "f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354",
+ "oid": "575ed68c716d6aae81ea6b718fd9ac66a8eae532",
+ "discussion": {
+ "comments": {},
+ "timeline": []
+ },
+ "reviews": {},
+ "timestamp": 1671125284000,
+ "resolves": [],
+ "reactions": []
+ }
+ },
+ "assignees": [],
+ "timeline": [
+ "d1f7f869fde9fac19c1779c4c2e77e8361333f91"
+ ],
+ "reviews": {}
+}
+```
+
+Finally let's update the issue and see the output of `rad cob show` also changes.
+
+```
+$ rad issue label 9de6448 --add bug --no-announce
+$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object 9de644864342d7a505eb8d58d1ef20e5bb05de2e
+{
+ "assignees": [],
+ "title": "spice harvester broken",
+ "state": {
+ "status": "open"
+ },
+ "labels": [
+ "bug"
+ ],
+ "thread": {
+ "comments": {
+ "9de644864342d7a505eb8d58d1ef20e5bb05de2e": {
+ "author": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "reactions": [],
+ "resolved": false,
+ "body": "Fremen have attacked, maybe we went too far?",
+ "edits": [
+ {
+ "author": "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+ "timestamp": 1671125284000,
+ "body": "Fremen have attacked, maybe we went too far?",
+ "embeds": []
+ }
+ ]
+ }
+ },
+ "timeline": [
+ "9de644864342d7a505eb8d58d1ef20e5bb05de2e"
+ ]
+ }
+}
+```
diff --git a/radicle-cli/src/commands/cob.rs b/radicle-cli/src/commands/cob.rs
index 65a1c355e..d609498b3 100644
--- a/radicle-cli/src/commands/cob.rs
+++ b/radicle-cli/src/commands/cob.rs
@@ -6,6 +6,9 @@ use chrono::prelude::*;
use nonempty::NonEmpty;
use radicle::cob;
use radicle::cob::Op;
+use radicle::identity::Identity;
+use radicle::issue::cache::Issues;
+use radicle::patch::cache::Patches;
use radicle::prelude::RepoId;
use radicle::storage::ReadStorage;
use radicle_cob::object::collaboration::list;
@@ -26,6 +29,8 @@ Usage
rad cob list --repo <rid> --type <typename>
rad cob log --repo <rid> --type <typename> --object <oid>
[--format pretty|json]
+ rad cob show --repo <rid> --type <typename> --object <oid>
+ [--format json]
Commands
@@ -37,6 +42,10 @@ Log options
--format [pretty|json] Desired output format (default: pretty)
+Show options
+
+ --format json Desired output format (default: json)
+
Other options
--help Print help
@@ -47,11 +56,13 @@ Other options
enum OperationName {
List,
Log,
+ Show,
}
enum Operation {
List,
Log(Rev),
+ Show(Rev),
}
enum Format {
@@ -82,6 +93,7 @@ impl Args for Options {
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
"list" => op = Some(OperationName::List),
"log" => op = Some(OperationName::Log),
+ "show" => op = Some(OperationName::Show),
unknown => anyhow::bail!("unknown operation '{unknown}'"),
},
Long("type") | Short('t') => {
@@ -106,10 +118,12 @@ impl Args for Options {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
- Long("format") if op == Some(OperationName::Log) => {
+ Long("format")
+ if op == Some(OperationName::Log) || op == Some(OperationName::Show) =>
+ {
let v: String = term::args::string(&parser.value()?);
match v.as_ref() {
- "pretty" => format = Some(Format::Pretty),
+ "pretty" if op == Some(OperationName::Log) => format = Some(Format::Pretty),
"json" => format = Some(Format::Json),
unknown => anyhow::bail!("unknown format '{unknown}'"),
}
@@ -124,7 +138,10 @@ impl Args for Options {
match op.ok_or_else(|| anyhow!("a command must be specified"))? {
OperationName::List => Operation::List,
OperationName::Log => Operation::Log(oid.ok_or_else(|| {
- anyhow!("an object id must be specified with `--object")
+ anyhow!("an object id must be specified with `--object`")
+ })?),
+ OperationName::Show => Operation::Show(oid.ok_or_else(|| {
+ anyhow!("an object id must be specified with `--object`")
})?),
}
},
@@ -160,6 +177,31 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Format::Pretty => ops.rev().try_for_each(print_op_pretty)?,
}
}
+ Operation::Show(oid) => {
+ let oid = &oid.resolve(&repo.backend)?;
+
+ if options.type_name == cob::patch::TYPENAME.clone() {
+ let patches = profile.patches(&repo)?;
+ let Some(patch) = patches.get(oid)? else {
+ anyhow::bail!(cob::store::Error::NotFound(options.type_name, *oid))
+ };
+ serde_json::to_writer_pretty(std::io::stdout(), &patch)?
+ } else if options.type_name == cob::issue::TYPENAME.clone() {
+ let issues = profile.issues(&repo)?;
+ let Some(issue) = issues.get(oid)? else {
+ anyhow::bail!(cob::store::Error::NotFound(options.type_name, *oid))
+ };
+ serde_json::to_writer_pretty(std::io::stdout(), &issue)?
+ } else if options.type_name == cob::identity::TYPENAME.clone() {
+ let Some(cob) = cob::get::<Identity, _>(&repo, &options.type_name, oid)? else {
+ anyhow::bail!(cob::store::Error::NotFound(options.type_name, *oid))
+ };
+ serde_json::to_writer_pretty(std::io::stdout(), &cob.object)?
+ } else {
+ anyhow::bail!("the type name '{}' is unknown", options.type_name);
+ }
+ println!();
+ }
}
Ok(())
diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs
index a80969b1e..344346451 100644
--- a/radicle-cli/tests/commands.rs
+++ b/radicle-cli/tests/commands.rs
@@ -156,7 +156,7 @@ fn rad_issue() {
}
#[test]
-fn rad_cob() {
+fn rad_cob_log() {
let mut environment = Environment::new();
let profile = environment.profile(config::profile("alice"));
let home = &profile.home;
@@ -169,6 +169,20 @@ fn rad_cob() {
test("examples/rad-cob-log.md", &working, Some(home), []).unwrap();
}
+#[test]
+fn rad_cob_show() {
+ let mut environment = Environment::new();
+ let profile = environment.profile(config::profile("alice"));
+ let home = &profile.home;
+ let working = environment.tmp().join("working");
+
+ // Setup a test repository.
+ fixtures::repository(&working);
+
+ test("examples/rad-init.md", &working, Some(home), []).unwrap();
+ test("examples/rad-cob-show.md", &working, Some(home), []).unwrap();
+}
+
#[test]
fn rad_init() {
let mut environment = Environment::new();
diff --git a/radicle/src/cob/identity.rs b/radicle/src/cob/identity.rs
index 191d2871c..bf21aad74 100644
--- a/radicle/src/cob/identity.rs
+++ b/radicle/src/cob/identity.rs
@@ -133,7 +133,8 @@ pub enum Error {
}
/// An evolving identity document.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
pub struct Identity {
/// The canonical identifier for this identity.
/// This is the object id of the initial document blob.
@@ -612,7 +613,7 @@ impl<R: ReadRepository> cob::Evaluate<R> for Identity {
}
}
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum Verdict {
/// An accepting verdict must supply the [`Signature`] over the
/// new proposed [`Doc`].
@@ -656,7 +657,7 @@ impl std::fmt::Display for State {
///
/// Once a revision has reached the quorum threshold of the previous
/// [`Identity`] it is then adopted as the current identity.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Revision {
/// The id of this revision. Points to a commit.
pub id: RevisionId,
commit 2fc2f2cf4179880ef10c6a3bfd84bfab11953450
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Tue May 21 14:19:23 2024 +0200
cli: Rework `rad cob log` options
- Rename: `--format {custom → pretty}`
- Add: `--format json`
diff --git a/radicle-cli/src/commands/cob.rs b/radicle-cli/src/commands/cob.rs
index 6e4db40ec..65a1c355e 100644
--- a/radicle-cli/src/commands/cob.rs
+++ b/radicle-cli/src/commands/cob.rs
@@ -5,9 +5,11 @@ use anyhow::anyhow;
use chrono::prelude::*;
use nonempty::NonEmpty;
use radicle::cob;
+use radicle::cob::Op;
use radicle::prelude::RepoId;
use radicle::storage::ReadStorage;
use radicle_cob::object::collaboration::list;
+use serde_json::json;
use crate::git::Rev;
use crate::terminal as term;
@@ -21,20 +23,27 @@ pub const HELP: Help = Help {
Usage
rad cob <command> [<option>...]
- rad cob list --repo <rid> --type <typename>
- rad cob log --repo <rid> --type <typename> --object <oid>
+ rad cob list --repo <rid> --type <typename>
+ rad cob log --repo <rid> --type <typename> --object <oid>
+ [--format pretty|json]
Commands
list List all COBs of a given type (--object is not needed)
log Print a log of all raw operations on a COB
+ show Print the materialized object of a COB
-Options
+Log options
- --help Print help
+ --format [pretty|json] Desired output format (default: pretty)
+
+Other options
+
+ --help Print help
"#,
};
+#[derive(PartialEq)]
enum OperationName {
List,
Log,
@@ -45,10 +54,16 @@ enum Operation {
Log(Rev),
}
+enum Format {
+ Json,
+ Pretty,
+}
+
pub struct Options {
rid: RepoId,
op: Operation,
type_name: cob::TypeName,
+ format: Format,
}
impl Args for Options {
@@ -60,6 +75,7 @@ impl Args for Options {
let mut type_name: Option<cob::TypeName> = None;
let mut oid: Option<Rev> = None;
let mut rid: Option<RepoId> = None;
+ let mut format: Option<Format> = None;
while let Some(arg) = parser.next()? {
match arg {
@@ -90,6 +106,14 @@ impl Args for Options {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
+ Long("format") if op == Some(OperationName::Log) => {
+ let v: String = term::args::string(&parser.value()?);
+ match v.as_ref() {
+ "pretty" => format = Some(Format::Pretty),
+ "json" => format = Some(Format::Json),
+ unknown => anyhow::bail!("unknown format '{unknown}'"),
+ }
+ }
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
}
@@ -108,6 +132,7 @@ impl Args for Options {
.ok_or_else(|| anyhow!("a repository id must be specified with `--repo`"))?,
type_name: type_name
.ok_or_else(|| anyhow!("an object type must be specified with `--type`"))?,
+ format: format.unwrap_or(Format::Pretty),
},
vec![],
))
@@ -128,40 +153,59 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
Operation::Log(oid) => {
let oid = oid.resolve(&repo.backend)?;
- let ops = cob::store::ops(&oid, &options.type_name, &repo)?;
-
- for op in ops.into_iter().rev() {
- let time = DateTime::<Utc>::from(
- std::time::UNIX_EPOCH + std::time::Duration::from_secs(op.timestamp.as_secs()),
- )
- .to_rfc2822();
-
- term::print(term::format::yellow(format!("commit {}", op.id)));
- if let Some(oid) = op.identity {
- term::print(term::format::tertiary(format!("resource {oid}")));
- }
- for parent in op.parents {
- term::print(format!("parent {}", parent));
- }
- for parent in op.related {
- term::print(format!("rel {}", parent));
- }
- term::print(format!("author {}", op.author));
- term::print(format!("date {}", time));
- term::blank();
-
- for action in op.actions {
- let obj: serde_json::Value = serde_json::from_slice(&action)?;
- let val = serde_json::to_string_pretty(&obj)?;
+ let mut ops = cob::store::ops(&oid, &options.type_name, &repo)?.into_iter();
- for line in val.lines() {
- term::indented(term::format::dim(line));
- }
- term::blank();
- }
+ match options.format {
+ Format::Json => ops.try_for_each(print_op_json)?,
+ Format::Pretty => ops.rev().try_for_each(print_op_pretty)?,
}
}
}
Ok(())
}
+
+fn print_op_pretty(op: Op<Vec<u8>>) -> anyhow::Result<()> {
+ let time = DateTime::<Utc>::from(
+ std::time::UNIX_EPOCH + std::time::Duration::from_secs(op.timestamp.as_secs()),
+ )
+ .to_rfc2822();
+ term::print(term::format::yellow(format!("commit {}", op.id)));
+ if let Some(oid) = op.identity {
+ term::print(term::format::tertiary(format!("resource {oid}")));
+ }
+ for parent in op.parents {
+ term::print(format!("parent {}", parent));
+ }
+ for parent in op.related {
+ term::print(format!("rel {}", parent));
+ }
+ term::print(format!("author {}", op.author));
+ term::print(format!("date {}", time));
+ term::blank();
+ for action in op.actions {
+ let obj: serde_json::Value = serde_json::from_slice(&action)?;
+ let val = serde_json::to_string_pretty(&obj)?;
+ for line in val.lines() {
+ term::indented(term::format::dim(line));
+ }
+ term::blank();
+ }
+ Ok(())
+}
+
+fn print_op_json(op: Op<Vec<u8>>) -> anyhow::Result<()> {
+ let mut ser = json!(op);
+ ser.as_object_mut().unwrap().insert(
+ "actions".to_string(),
+ json!(op
+ .actions
+ .iter()
+ .map(|action: &Vec<u8>| -> Result<serde_json::Value, _> {
+ serde_json::from_slice(action)
+ })
+ .collect::<Result<Vec<serde_json::Value>, _>>()?),
+ );
+ term::print(ser);
+ Ok(())
+}
\ No newline at end of file
diff --git a/radicle/src/cob/op.rs b/radicle/src/cob/op.rs
index 8350d3575..c958065de 100644
--- a/radicle/src/cob/op.rs
+++ b/radicle/src/cob/op.rs
@@ -1,5 +1,6 @@
use nonempty::NonEmpty;
use radicle_cob::Manifest;
+use serde::Serialize;
use thiserror::Error;
use radicle_cob::history::{Entry, EntryId};
@@ -26,7 +27,7 @@ pub enum OpEncodingError {
///
/// Everything that can be done in the system is represented by an `Op`.
/// Operations are applied to an accumulator to yield a final state.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Op<A> {
/// Id of the entry under which this operation lives.
pub id: EntryId,
commit 45526fbd1bf0dcd1973f7ed1047a6fa1cea6899f
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Tue May 7 15:39:41 2024 +0200
cli: `rad cob {show → log}`
diff --git a/radicle-cli/examples/rad-cob.md b/radicle-cli/examples/rad-cob-log.md
similarity index 93%
rename from radicle-cli/examples/rad-cob.md
rename to radicle-cli/examples/rad-cob-log.md
index 38d743d13..e9a061ce2 100644
--- a/radicle-cli/examples/rad-cob.md
+++ b/radicle-cli/examples/rad-cob-log.md
@@ -61,7 +61,7 @@ aa45913e757cacd46972733bddee5472c78fa32a
We can look at the issue COB.
```
-$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
commit d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
resource 0656c217f917c3e06234771e9ecae53aba5e173e
author z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
@@ -82,7 +82,7 @@ date Thu, 15 Dec 2022 17:28:04 +0000
We can look at the patch COB too.
```
-$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object aa45913e757cacd46972733bddee5472c78fa32a
+$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object aa45913e757cacd46972733bddee5472c78fa32a
commit aa45913e757cacd46972733bddee5472c78fa32a
resource 0656c217f917c3e06234771e9ecae53aba5e173e
rel 3e674d1a1df90807e934f9ae5da2591dd6848a33
@@ -109,7 +109,7 @@ Finally let's updated the issue and see the `parent` header:
```
$ rad issue label d87dcfe --add bug --no-announce
-$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
commit abec0a9f3c945594c4e78d24d8ec679e56b22b79
resource 0656c217f917c3e06234771e9ecae53aba5e173e
parent d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
diff --git a/radicle-cli/examples/rad-id.md b/radicle-cli/examples/rad-id.md
index 4873f541e..e713baf0f 100644
--- a/radicle-cli/examples/rad-id.md
+++ b/radicle-cli/examples/rad-id.md
@@ -93,7 +93,7 @@ $ rad inspect --identity
We can also look at the document's COB directly:
```
-$ rad cob show --object 0656c217f917c3e06234771e9ecae53aba5e173e --type xyz.radicle.id --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+$ rad cob log --object 0656c217f917c3e06234771e9ecae53aba5e173e --type xyz.radicle.id --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
commit 0ca42d376bd566631083c8913cf86bec722da392
parent 0656c217f917c3e06234771e9ecae53aba5e173e
author z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
diff --git a/radicle-cli/src/commands/cob.rs b/radicle-cli/src/commands/cob.rs
index e76fc5123..6e4db40ec 100644
--- a/radicle-cli/src/commands/cob.rs
+++ b/radicle-cli/src/commands/cob.rs
@@ -22,12 +22,12 @@ Usage
rad cob <command> [<option>...]
rad cob list --repo <rid> --type <typename>
- rad cob show --repo <rid> --type <typename> --object <oid>
+ rad cob log --repo <rid> --type <typename> --object <oid>
Commands
list List all COBs of a given type (--object is not needed)
- show Show a COB as raw operations
+ log Print a log of all raw operations on a COB
Options
@@ -37,12 +37,12 @@ Options
enum OperationName {
List,
- Show,
+ Log,
}
enum Operation {
List,
- Show(Rev),
+ Log(Rev),
}
pub struct Options {
@@ -64,8 +64,8 @@ impl Args for Options {
while let Some(arg) = parser.next()? {
match arg {
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
- "l" | "list" => op = Some(OperationName::List),
- "s" | "show" => op = Some(OperationName::Show),
+ "list" => op = Some(OperationName::List),
+ "log" => op = Some(OperationName::Log),
unknown => anyhow::bail!("unknown operation '{unknown}'"),
},
Long("type") | Short('t') => {
@@ -99,7 +99,7 @@ impl Args for Options {
op: {
match op.ok_or_else(|| anyhow!("a command must be specified"))? {
OperationName::List => Operation::List,
- OperationName::Show => Operation::Show(oid.ok_or_else(|| {
+ OperationName::Log => Operation::Log(oid.ok_or_else(|| {
anyhow!("an object id must be specified with `--object")
})?),
}
@@ -126,7 +126,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
println!("{}", cob.id);
}
}
- Operation::Show(oid) => {
+ Operation::Log(oid) => {
let oid = oid.resolve(&repo.backend)?;
let ops = cob::store::ops(&oid, &options.type_name, &repo)?;
diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs
index f31333b15..a80969b1e 100644
--- a/radicle-cli/tests/commands.rs
+++ b/radicle-cli/tests/commands.rs
@@ -166,7 +166,7 @@ fn rad_cob() {
fixtures::repository(&working);
test("examples/rad-init.md", &working, Some(home), []).unwrap();
- test("examples/rad-cob.md", &working, Some(home), []).unwrap();
+ test("examples/rad-cob-log.md", &working, Some(home), []).unwrap();
}
#[test]
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 d6e38b2d-c1f0-467f-b927-f82df58533b8 -v /opt/radcis/ci.rad.levitte.org/cci/state/d6e38b2d-c1f0-467f-b927-f82df58533b8/s:/d6e38b2d-c1f0-467f-b927-f82df58533b8/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/d6e38b2d-c1f0-467f-b927-f82df58533b8/w:/d6e38b2d-c1f0-467f-b927-f82df58533b8/w -w /d6e38b2d-c1f0-467f-b927-f82df58533b8/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /d6e38b2d-c1f0-467f-b927-f82df58533b8/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}