From e4d23fe56a7f7329ac98991bc55bb043ddede617 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Wed, 22 May 2024 16:44:31 +0200 Subject: [PATCH] cli: Introduce `cob [create|update]` --- CONTRIBUTING.md | 3 + flake.nix | 2 +- radicle-cli/examples/rad-cob-multiset | 32 + radicle-cli/examples/rad-cob-multiset.md | 72 ++ .../examples/rad-cob-update-identity.md | 6 + radicle-cli/examples/rad-cob-update.md | 178 +++++ radicle-cli/src/commands/cob.rs | 658 +++++++++++++----- radicle-cli/tests/commands.rs | 86 +++ radicle-node/src/worker/fetch.rs | 2 +- radicle/src/cob.rs | 1 + radicle/src/cob/external.rs | 240 +++++++ radicle/src/cob/identity.rs | 12 +- radicle/src/cob/issue.rs | 12 +- radicle/src/cob/job.rs | 12 +- radicle/src/cob/patch.rs | 12 +- radicle/src/cob/store.rs | 108 ++- radicle/src/cob/test.rs | 12 +- radicle/src/cob/thread.rs | 10 +- radicle/src/identity.rs | 2 +- radicle/src/lib.rs | 2 +- 20 files changed, 1244 insertions(+), 218 deletions(-) create mode 100755 radicle-cli/examples/rad-cob-multiset create mode 100644 radicle-cli/examples/rad-cob-multiset.md create mode 100644 radicle-cli/examples/rad-cob-update-identity.md create mode 100644 radicle-cli/examples/rad-cob-update.md create mode 100644 radicle/src/cob/external.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d706d4c4..12f64eb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,9 @@ Make sure all tests are passing with: $ cargo test --workspace +Some tests require `jq`. If `jq` is not detected, these tests will succeed +without effectively testing anything. + ## Checking the docs If you make documentation changes, you may want to check whether there are any diff --git a/flake.nix b/flake.nix index 1cd44814..4cfa9f78 100644 --- a/flake.nix +++ b/flake.nix @@ -179,7 +179,7 @@ cargoExtraArgs = "-p ${name}"; doCheck = false; - nativeBuildInputs = with pkgs; [asciidoctor installShellFiles]; + nativeBuildInputs = with pkgs; [asciidoctor installShellFiles jq]; postInstall = '' for page in ${lib.escapeShellArgs pages}; do asciidoctor -d manpage -b manpage $page diff --git a/radicle-cli/examples/rad-cob-multiset b/radicle-cli/examples/rad-cob-multiset new file mode 100755 index 00000000..23f6de35 --- /dev/null +++ b/radicle-cli/examples/rad-cob-multiset @@ -0,0 +1,32 @@ +#! /usr/bin/env -S jq --from-file --sort-keys --compact-output +# +# Models a multiset as a JSON object. +# +# Takes an operation which contains actions, each in one of two shapes: +# +# Add x: +# +# { "+": $x } +# +# Remove x: +# +# { "-": $x } +# +# (where `$x` is a string). +# +# These actions are reduced to a multiset represented by an +# object. The key being the item to count (corresponding to `$x` above), +# and the value being the count itself (corresponding to how many times +# `$x` was added, minus how many times `$x` was removed). +# +# Errors if any unrecognizable action is encountered. +# +# For an example, see `rad-cob-multiset.md`. +reduce + .op.actions[] +as {"+": $p, "-": $m} ( + .value; + .[$p // $m // ("invalid" | halt_error)] |= ( + [. + (if $p then 1 else -1 end), 0] | max + ) +) diff --git a/radicle-cli/examples/rad-cob-multiset.md b/radicle-cli/examples/rad-cob-multiset.md new file mode 100644 index 00000000..bd52803a --- /dev/null +++ b/radicle-cli/examples/rad-cob-multiset.md @@ -0,0 +1,72 @@ +This example demonstrates handling of arbitrary Collaborative Objects (COBs) by means of an external program. +The external program is called a "COB helper" (analogous to "remote helpers" that can be used to extend Git). +It consumes the current state of the COB and at least one operation (all in [JSON]) to be applied to it via files whose names are passed as arguments. +It returns the resulting COB by printing it to standard output in [JSON]. + +For the sake of example, consider a simplified shopping list, where every item on the list is associated with an integral quantity: + + - 5 Bananas + - 3 Zucchini + - 1 Bar of Chocolate + +One data structure that maps to this concept very well is a [multiset] (sometimes also called "bag"). +So, let us introduce a new COB type with the name `com.example.multiset` that implements multisets, which we will then use to model our shopping list. + +The COB we implement should allow two actions: + + - The action `+`, which adds an item or, if already present, increases the associated quantity. + - The action `-`, which decreases the associated quantity of an item, if it is non-zero. + +We model actions as objects in [JSON], and a sequence of actions in [JSON Lines]. +An example sequence of actions looks as follows: + +``` ./groceries.jsonl +{ "+": "jelly" } +{ "+": "peanut butter" } +{ "-": "jelly" } +{ "-": "jelly" } +{ "+": "salad" } +{ "+": "salad" } +``` + +Starting with an empty grocery list, the expected result after evaluating all actions is: + + - 0 Jelly (this could be omitted) + - 1 Peanut Butter + - 2 Salad + +We have a COB helper, named `rad-cob-multiset`, that implements evaluation of these actions using [jq]. +It reads the current state of the grocery list and operations containing actions from files given as arguments and writes the resulting grocery list to standard output. + +We do not invoke the program directly, but instead use `rad cob create`: + +``` +$ rad cob create --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type com.example.multiset --message "Create grocery shopping multiset" groceries.jsonl +9bba8e6f83ef56b11151ef6ad02cc4595f982aab +``` + +We can verify that the COB evaluated as expected: + +``` +$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type com.example.multiset --object 9bba8e6f83ef56b11151ef6ad02cc4595f982aab +{"jelly":0,"peanut butter":1,"salad":2} +``` + +To apply actions to COBs that already exist, we can use `rad cob update`: + +``` +$ rad cob update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type com.example.multiset --object 9bba8e6f83ef56b11151ef6ad02cc4595f982aab --message "Modify grocery shopping multiset" groceries.jsonl +d36aac77be13c1ca80edbfe7b7bf9b42c723f019 +``` + +Again, we verify the result with `rad cob show`: + +``` +$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type com.example.multiset --object 9bba8e6f83ef56b11151ef6ad02cc4595f982aab +{"jelly":0,"peanut butter":2,"salad":4} +``` + +[multisets]: https://wikipedia.org/wiki/Multiset +[JSON]: https://tools.ietf.org/html/std90 +[JSON Lines]: https://jsonlines.org/ +[jq]: https://github.com/jqlang/jq \ No newline at end of file diff --git a/radicle-cli/examples/rad-cob-update-identity.md b/radicle-cli/examples/rad-cob-update-identity.md new file mode 100644 index 00000000..40083e8c --- /dev/null +++ b/radicle-cli/examples/rad-cob-update-identity.md @@ -0,0 +1,6 @@ +Updating the repository identity via `rad cob update` is forbidden: + +``` (fail) +$ rad cob update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.id --object 0656c217f917c3e06234771e9ecae53aba5e173e --message "Danger" /dev/null +✗ Error: Update of collaborative objects of type xyz.radicle.id is not supported. +``` \ No newline at end of file diff --git a/radicle-cli/examples/rad-cob-update.md b/radicle-cli/examples/rad-cob-update.md new file mode 100644 index 00000000..86516b8b --- /dev/null +++ b/radicle-cli/examples/rad-cob-update.md @@ -0,0 +1,178 @@ +First off, we set up a patch. + +``` +$ git checkout -b changes +$ touch README.md +$ git add README.md +$ git commit --message "Add README, just for the fun" +[changes 03c02af] Add README, just for the fun + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 README.md +``` + +``` (stderr) +$ git push rad -o patch.message="Add README, just for the fun" HEAD:refs/patches +✓ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 opened +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +``` +$ touch LICENSE +$ git add LICENSE +$ git commit -v -m "Define the LICENSE" +[changes 8945f61] Define the LICENSE + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 LICENSE +``` + +``` (stderr) +$ git push -f -o patch.message="Add License" +✓ Patch 89f7afb updated to revision 5d78dd5376453e25df5988ec86951c99cb73742c +To compare against your previous revision 89f7afb, run: + + git range-diff f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 03c02af4b12a593d17a06d38fae50a57fc3c339a 8945f6189adf027892c85ac57f7e9341049c2537 + +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + 03c02af..8945f61 changes -> patches/89f7afb1511b976482b21f6b2f39aef7f4fb88a2 +``` + +Let's look at the patch, to see what it looks like before editing it: + +``` +$ rad patch show 89f7afb +╭─────────────────────────────────────────────────────────────────────╮ +│ Title Add README, just for the fun │ +│ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 │ +│ Author alice (you) │ +│ Head 8945f6189adf027892c85ac57f7e9341049c2537 │ +│ Branches changes │ +│ Commits ahead 2, behind 0 │ +│ Status open │ +├─────────────────────────────────────────────────────────────────────┤ +│ 8945f61 Define the LICENSE │ +│ 03c02af Add README, just for the fun │ +├─────────────────────────────────────────────────────────────────────┤ +│ ● opened by alice (you) (03c02af) now │ +│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │ +╰─────────────────────────────────────────────────────────────────────╯ +``` + +We can change the title and description of the patch itself by using a +multi-line message (using two `--message` options here): + +``` +$ rad patch edit 89f7afb --message "Add Metadata" --message "Add README & LICENSE" --no-announce +$ rad patch show 89f7afb +╭─────────────────────────────────────────────────────────────────────╮ +│ Title Add Metadata │ +│ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 │ +│ Author alice (you) │ +│ Head 8945f6189adf027892c85ac57f7e9341049c2537 │ +│ Branches changes │ +│ Commits ahead 2, behind 0 │ +│ Status open │ +│ │ +│ Add README & LICENSE │ +├─────────────────────────────────────────────────────────────────────┤ +│ 8945f61 Define the LICENSE │ +│ 03c02af Add README, just for the fun │ +├─────────────────────────────────────────────────────────────────────┤ +│ ● opened by alice (you) (03c02af) now │ +│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │ +╰─────────────────────────────────────────────────────────────────────╯ +``` + +We prepare the file `revision-edit.json` which contains one action (thus one line) to be applied. + +``` ./revision-edit.jsonl +{"type": "revision.edit", "description": "Add README and LICENSE", "revision": "89f7afb1511b976482b21f6b2f39aef7f4fb88a2"} +``` + +We now use `rad cob update` to edit the patch another time, rewriting the description. +The action itself is of type `revision.edit` and carries the parameters `revision`, +specifying the revision for which the description should be changed, and `description`, +specifying the new description. + +``` +$ rad cob update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 --message "Edit patch" revision-edit.jsonl +79b816e92735c49b33d93a31890fdf040b36234c +$ rad patch show --verbose 89f7afb +╭─────────────────────────────────────────────────────────────────────╮ +│ Title Add Metadata │ +│ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 │ +│ Author alice (you) │ +│ Head 8945f6189adf027892c85ac57f7e9341049c2537 │ +│ Base f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │ +│ Branches changes │ +│ Commits ahead 2, behind 0 │ +│ Status open │ +│ │ +│ Add README and LICENSE │ +├─────────────────────────────────────────────────────────────────────┤ +│ 8945f61 Define the LICENSE │ +│ 03c02af Add README, just for the fun │ +├─────────────────────────────────────────────────────────────────────┤ +│ ● opened by alice (you) (03c02af) now │ +│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │ +╰─────────────────────────────────────────────────────────────────────╯ +``` + +Notice that the patch now has the description `Add README and LICENSE`. + +We may use `rad cob update` to create a new revision altogether, as well. +Let's create yet another commit, an empty one this time, and do that. + +``` +$ git commit --allow-empty --message="Dummy commit for a new revision" +[changes f1339dd] Dummy commit for a new revision +``` + +We prepare the file `revision-create.jsonl` which contains one action. + +``` ./revision.jsonl +{"type": "revision", "description": "A new revision", "base": "f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354", "oid": "f1339dd109e538c6b3a7fed3e72403e1b4db08c9"} +``` + +Attempting to create the new revision right away would fail: + +``` (fail) +$ rad cob update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 --message "Create new revision" revision.jsonl +✗ Error: store: update error: failed to read 'f1339dd109e538c6b3a7fed3e72403e1b4db08c9' from git odb +``` + +Since we are not using the remote helper `git-remote-rad` here, we need to push +the new commit to storage manually. See `fn patch_open` in `/radicle-remote-helper/src/push.rs` +for more details. + +``` +$ git push rad HEAD:tmp/heads/f1339dd109e538c6b3a7fed3e72403e1b4db08c9 +$ git push rad :tmp/heads/f1339dd109e538c6b3a7fed3e72403e1b4db08c9 +``` + +Now we can invoke `rad cob update`: + +``` +$ rad cob update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 --message "Create new revision" revision.jsonl +2da9c025a1d14d93c4f2cec60a7878afbc5e2a3c +$ rad patch show 89f7afb +╭─────────────────────────────────────────────────────────────────────╮ +│ Title Add Metadata │ +│ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 │ +│ Author alice (you) │ +│ Head f1339dd109e538c6b3a7fed3e72403e1b4db08c9 │ +│ Branches changes │ +│ Commits ahead 3, behind 0 │ +│ Status open │ +│ │ +│ Add README and LICENSE │ +├─────────────────────────────────────────────────────────────────────┤ +│ f1339dd Dummy commit for a new revision │ +│ 8945f61 Define the LICENSE │ +│ 03c02af Add README, just for the fun │ +├─────────────────────────────────────────────────────────────────────┤ +│ ● opened by alice (you) (03c02af) now │ +│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │ +│ ↑ updated to 2da9c025a1d14d93c4f2cec60a7878afbc5e2a3c (f1339dd) now │ +╰─────────────────────────────────────────────────────────────────────╯ +``` \ No newline at end of file diff --git a/radicle-cli/src/commands/cob.rs b/radicle-cli/src/commands/cob.rs index 4c44d7d1..1e116ae8 100644 --- a/radicle-cli/src/commands/cob.rs +++ b/radicle-cli/src/commands/cob.rs @@ -1,21 +1,19 @@ use std::ffi::OsString; -use std::io; -use std::io::Write; +use std::path::PathBuf; use std::str::FromStr; +use std::{fs, io}; + +use anyhow::{anyhow, bail}; -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::cob::store::CobAction; +use radicle::prelude::*; use radicle::storage::git; -use radicle::storage::ReadStorage; -use radicle::Profile; -use radicle_cob::object::collaboration::list; + use serde_json::json; use crate::git::Rev; @@ -30,33 +28,47 @@ pub const HELP: Help = Help { Usage rad cob [