Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: `rad cob {show → log}`, and new `rad cob show`
Merged lorenz opened 1 year ago

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

8 files changed +472 -187 30c9b0db 6d11ea17
added radicle-cli/examples/rad-cob-log.md
@@ -0,0 +1,141 @@
+
Handle arbitrary COBs.
+

+
First create an issue.
+

+
```
+
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
+
╭─────────────────────────────────────────────────────────╮
+
│ Title   flux capacitor underpowered                     │
+
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
+
│ Author  z6MknSL…StBU8Vi (you)                           │
+
│ Status  open                                            │
+
│                                                         │
+
│ Flux capacitor power requirements exceed current supply │
+
╰─────────────────────────────────────────────────────────╯
+
```
+

+
The issue is now listed under our project.
+

+
```
+
$ rad issue list
+
╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●   ID        Title                         Author                    Labels   Assignees   Opened │
+
├───────────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●   d87dcfe   flux capacitor underpowered   z6MknSL…StBU8Vi   (you)                        now    │
+
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
+
```
+

+
Let's create a patch, too.
+

+
```
+
$ git checkout -b flux-capacitor-power
+
$ touch REQUIREMENTS
+
$ git add REQUIREMENTS
+
$ git commit -v -m "Define power requirements"
+
[flux-capacitor-power 3e674d1] Define power requirements
+
 1 file changed, 0 insertions(+), 0 deletions(-)
+
 create mode 100644 REQUIREMENTS
+
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
+
```
+

+
Patch can be listed.
+

+
```
+
$ rad patch
+
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title                      Author                  Reviews  Head     +   -   Updated │
+
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  aa45913  Define power requirements  z6MknSL…StBU8Vi  (you)  -        3e674d1  +0  -0  now     │
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+
```
+

+
Both issue and patch COBs can be listed.
+

+
```
+
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue
+
d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch
+
aa45913e757cacd46972733bddee5472c78fa32a
+
```
+

+
We can look at the issue COB.
+

+
```
+
$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
commit   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
resource 0656c217f917c3e06234771e9ecae53aba5e173e
+
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
date     Thu, 15 Dec 2022 17:28:04 +0000
+

+
    {
+
      "body": "Flux capacitor power requirements exceed current supply",
+
      "type": "comment"
+
    }
+

+
    {
+
      "title": "flux capacitor underpowered",
+
      "type": "edit"
+
    }
+

+
```
+

+
We can look at the patch COB too.
+

+
```
+
$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object aa45913e757cacd46972733bddee5472c78fa32a
+
commit   aa45913e757cacd46972733bddee5472c78fa32a
+
resource 0656c217f917c3e06234771e9ecae53aba5e173e
+
rel      3e674d1a1df90807e934f9ae5da2591dd6848a33
+
rel      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
date     Thu, 15 Dec 2022 17:28:04 +0000
+

+
    {
+
      "base": "f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354",
+
      "description": "See details.",
+
      "oid": "3e674d1a1df90807e934f9ae5da2591dd6848a33",
+
      "type": "revision"
+
    }
+

+
    {
+
      "target": "delegates",
+
      "title": "Define power requirements",
+
      "type": "edit"
+
    }
+

+
```
+

+
Finally let's updated the issue and see the `parent` header:
+

+
```
+
$ rad issue label d87dcfe --add bug --no-announce
+
$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
commit   abec0a9f3c945594c4e78d24d8ec679e56b22b79
+
resource 0656c217f917c3e06234771e9ecae53aba5e173e
+
parent   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
date     Thu, 15 Dec 2022 17:28:04 +0000
+

+
    {
+
      "labels": [
+
        "bug"
+
      ],
+
      "type": "label"
+
    }
+

+
commit   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
resource 0656c217f917c3e06234771e9ecae53aba5e173e
+
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
date     Thu, 15 Dec 2022 17:28:04 +0000
+

+
    {
+
      "body": "Flux capacitor power requirements exceed current supply",
+
      "type": "comment"
+
    }
+

+
    {
+
      "title": "flux capacitor underpowered",
+
      "type": "edit"
+
    }
+

+
```
added 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"
+
    ]
+
  }
+
}
+
```
deleted radicle-cli/examples/rad-cob.md
@@ -1,141 +0,0 @@
-
Handle arbitrary COBs.
-

-
First create an issue.
-

-
```
-
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
-
╭─────────────────────────────────────────────────────────╮
-
│ Title   flux capacitor underpowered                     │
-
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
-
│ Author  z6MknSL…StBU8Vi (you)                           │
-
│ Status  open                                            │
-
│                                                         │
-
│ Flux capacitor power requirements exceed current supply │
-
╰─────────────────────────────────────────────────────────╯
-
```
-

-
The issue is now listed under our project.
-

-
```
-
$ rad issue list
-
╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●   ID        Title                         Author                    Labels   Assignees   Opened │
-
├───────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   z6MknSL…StBU8Vi   (you)                        now    │
-
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
-
```
-

-
Let's create a patch, too.
-

-
```
-
$ git checkout -b flux-capacitor-power
-
$ touch REQUIREMENTS
-
$ git add REQUIREMENTS
-
$ git commit -v -m "Define power requirements"
-
[flux-capacitor-power 3e674d1] Define power requirements
-
 1 file changed, 0 insertions(+), 0 deletions(-)
-
 create mode 100644 REQUIREMENTS
-
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
-
```
-

-
Patch can be listed.
-

-
```
-
$ rad patch
-
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
-
│ ●  ID       Title                      Author                  Reviews  Head     +   -   Updated │
-
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  z6MknSL…StBU8Vi  (you)  -        3e674d1  +0  -0  now     │
-
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
-
```
-

-
Both issue and patch COBs can be listed.
-

-
```
-
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue
-
d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
-
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch
-
aa45913e757cacd46972733bddee5472c78fa32a
-
```
-

-
We can look at the issue COB.
-

-
```
-
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
-
commit   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
-
resource 0656c217f917c3e06234771e9ecae53aba5e173e
-
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
date     Thu, 15 Dec 2022 17:28:04 +0000
-

-
    {
-
      "body": "Flux capacitor power requirements exceed current supply",
-
      "type": "comment"
-
    }
-

-
    {
-
      "title": "flux capacitor underpowered",
-
      "type": "edit"
-
    }
-

-
```
-

-
We can look at the patch COB too.
-

-
```
-
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object aa45913e757cacd46972733bddee5472c78fa32a
-
commit   aa45913e757cacd46972733bddee5472c78fa32a
-
resource 0656c217f917c3e06234771e9ecae53aba5e173e
-
rel      3e674d1a1df90807e934f9ae5da2591dd6848a33
-
rel      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
-
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
date     Thu, 15 Dec 2022 17:28:04 +0000
-

-
    {
-
      "base": "f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354",
-
      "description": "See details.",
-
      "oid": "3e674d1a1df90807e934f9ae5da2591dd6848a33",
-
      "type": "revision"
-
    }
-

-
    {
-
      "target": "delegates",
-
      "title": "Define power requirements",
-
      "type": "edit"
-
    }
-

-
```
-

-
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
-
commit   abec0a9f3c945594c4e78d24d8ec679e56b22b79
-
resource 0656c217f917c3e06234771e9ecae53aba5e173e
-
parent   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
-
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
date     Thu, 15 Dec 2022 17:28:04 +0000
-

-
    {
-
      "labels": [
-
        "bug"
-
      ],
-
      "type": "label"
-
    }
-

-
commit   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
-
resource 0656c217f917c3e06234771e9ecae53aba5e173e
-
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
date     Thu, 15 Dec 2022 17:28:04 +0000
-

-
    {
-
      "body": "Flux capacitor power requirements exceed current supply",
-
      "type": "comment"
-
    }
-

-
    {
-
      "title": "flux capacitor underpowered",
-
      "type": "edit"
-
    }
-

-
```
modified 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
modified radicle-cli/src/commands/cob.rs
@@ -5,9 +5,14 @@ use anyhow::anyhow;
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;
+
use serde_json::json;

use crate::git::Rev;
use crate::terminal as term;
@@ -21,34 +26,55 @@ pub const HELP: Help = Help {
Usage

    rad cob <command> [<option>...]
-
    rad cob list --repo <rid> --type <typename>
-
    rad cob show --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]
+
    rad cob show  --repo <rid> --type <typename> --object <oid>
+
                  [--format json]

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
+
    show       Print the materialized object of a COB

-
Options
+
Log options

-
    --help     Print help
+
    --format [pretty|json]  Desired output format (default: pretty)
+

+
Show options
+

+
    --format json           Desired output format (default: json)
+

+
Other options
+

+
    --help                  Print help
"#,
};

+
#[derive(PartialEq)]
enum OperationName {
    List,
+
    Log,
    Show,
}

enum Operation {
    List,
+
    Log(Rev),
    Show(Rev),
}

+
enum Format {
+
    Json,
+
    Pretty,
+
}
+

pub struct Options {
    rid: RepoId,
    op: Operation,
    type_name: cob::TypeName,
+
    format: Format,
}

impl Args for Options {
@@ -60,12 +86,14 @@ 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 {
                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),
+
                    "show" => op = Some(OperationName::Show),
                    unknown => anyhow::bail!("unknown operation '{unknown}'"),
                },
                Long("type") | Short('t') => {
@@ -90,6 +118,16 @@ impl Args for Options {
                Long("help") | Short('h') => {
                    return Err(Error::Help.into());
                }
+
                Long("format")
+
                    if op == Some(OperationName::Log) || op == Some(OperationName::Show) =>
+
                {
+
                    let v: String = term::args::string(&parser.value()?);
+
                    match v.as_ref() {
+
                        "pretty" if op == Some(OperationName::Log) => format = Some(Format::Pretty),
+
                        "json" => format = Some(Format::Json),
+
                        unknown => anyhow::bail!("unknown format '{unknown}'"),
+
                    }
+
                }
                _ => return Err(anyhow::anyhow!(arg.unexpected())),
            }
        }
@@ -99,8 +137,11 @@ impl Args for Options {
                op: {
                    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`")
+
                        })?),
                        OperationName::Show => Operation::Show(oid.ok_or_else(|| {
-
                            anyhow!("an object id must be specified with `--object")
+
                            anyhow!("an object id must be specified with `--object`")
                        })?),
                    }
                },
@@ -108,6 +149,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![],
        ))
@@ -126,42 +168,86 @@ 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)?;
-

-
            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)?,
+
            }
+
        }
+
        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(())
}
+

+
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
modified 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;
@@ -166,7 +166,21 @@ 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]
+
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]
modified 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,
modified 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,