cli: Introduce `cob [create|update]`
This commit is contained in:
parent
547614a5bb
commit
e4d23fe56a
|
|
@ -36,6 +36,9 @@ Make sure all tests are passing with:
|
||||||
|
|
||||||
$ cargo test --workspace
|
$ cargo test --workspace
|
||||||
|
|
||||||
|
Some tests require `jq`. If `jq` is not detected, these tests will succeed
|
||||||
|
without effectively testing anything.
|
||||||
|
|
||||||
## Checking the docs
|
## Checking the docs
|
||||||
|
|
||||||
If you make documentation changes, you may want to check whether there are any
|
If you make documentation changes, you may want to check whether there are any
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@
|
||||||
cargoExtraArgs = "-p ${name}";
|
cargoExtraArgs = "-p ${name}";
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [asciidoctor installShellFiles];
|
nativeBuildInputs = with pkgs; [asciidoctor installShellFiles jq];
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
for page in ${lib.escapeShellArgs pages}; do
|
for page in ${lib.escapeShellArgs pages}; do
|
||||||
asciidoctor -d manpage -b manpage $page
|
asciidoctor -d manpage -b manpage $page
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
```
|
||||||
|
|
@ -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 │
|
||||||
|
╰─────────────────────────────────────────────────────────────────────╯
|
||||||
|
```
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::io;
|
use std::path::PathBuf;
|
||||||
use std::io::Write;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::{fs, io};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
||||||
use radicle::cob;
|
use radicle::cob;
|
||||||
use radicle::cob::Op;
|
use radicle::cob::store::CobAction;
|
||||||
use radicle::identity::Identity;
|
use radicle::prelude::*;
|
||||||
use radicle::issue::cache::Issues;
|
|
||||||
use radicle::patch::cache::Patches;
|
|
||||||
use radicle::prelude::RepoId;
|
|
||||||
use radicle::storage::git;
|
use radicle::storage::git;
|
||||||
use radicle::storage::ReadStorage;
|
|
||||||
use radicle::Profile;
|
|
||||||
use radicle_cob::object::collaboration::list;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::git::Rev;
|
use crate::git::Rev;
|
||||||
|
|
@ -30,16 +28,28 @@ pub const HELP: Help = Help {
|
||||||
Usage
|
Usage
|
||||||
|
|
||||||
rad cob <command> [<option>...]
|
rad cob <command> [<option>...]
|
||||||
|
|
||||||
|
rad cob create --repo <rid> --type <typename> <filename> [<option>...]
|
||||||
rad cob list --repo <rid> --type <typename>
|
rad cob list --repo <rid> --type <typename>
|
||||||
rad cob log --repo <rid> --type <typename> --object <oid> [<option>...]
|
rad cob log --repo <rid> --type <typename> --object <oid> [<option>...]
|
||||||
rad cob show --repo <rid> --type <typename> --object <oid> [<option>...]
|
|
||||||
rad cob migrate [<option>...]
|
rad cob migrate [<option>...]
|
||||||
|
rad cob show --repo <rid> --type <typename> --object <oid> [<option>...]
|
||||||
|
rad cob update --repo <rid> --type <typename> --object <oid> <filename>
|
||||||
|
[<option>...]
|
||||||
|
|
||||||
Commands
|
Commands
|
||||||
|
|
||||||
|
create Create a new COB of a given type given initial actions
|
||||||
list List all COBs of a given type (--object is not needed)
|
list List all COBs of a given type (--object is not needed)
|
||||||
log Print a log of all raw operations on a COB
|
log Print a log of all raw operations on a COB
|
||||||
migrate Migrate the COB database to the latest version
|
migrate Migrate the COB database to the latest version
|
||||||
|
update Add actions to a COB
|
||||||
|
show Print the state of COBs
|
||||||
|
|
||||||
|
Create, Update options
|
||||||
|
|
||||||
|
--embed-file <name> <path> Supply embed of given name via file at given path
|
||||||
|
--embed-hash <name> <oid> Supply embed of given name via object ID of blob
|
||||||
|
|
||||||
Log options
|
Log options
|
||||||
|
|
||||||
|
|
@ -55,8 +65,10 @@ Other options
|
||||||
"#,
|
"#,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
enum OperationName {
|
enum OperationName {
|
||||||
|
Update,
|
||||||
|
Create,
|
||||||
List,
|
List,
|
||||||
Log,
|
Log,
|
||||||
Migrate,
|
Migrate,
|
||||||
|
|
@ -64,20 +76,36 @@ enum OperationName {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Operation {
|
enum Operation {
|
||||||
|
Create {
|
||||||
|
rid: RepoId,
|
||||||
|
type_name: FilteredTypeName,
|
||||||
|
message: String,
|
||||||
|
actions: PathBuf,
|
||||||
|
embeds: Vec<Embed>,
|
||||||
|
},
|
||||||
List {
|
List {
|
||||||
repo: RepoId,
|
rid: RepoId,
|
||||||
type_name: cob::TypeName,
|
type_name: FilteredTypeName,
|
||||||
},
|
},
|
||||||
Log {
|
Log {
|
||||||
repo: RepoId,
|
rid: RepoId,
|
||||||
rev: Rev,
|
type_name: FilteredTypeName,
|
||||||
type_name: cob::TypeName,
|
oid: Rev,
|
||||||
|
format: Format,
|
||||||
},
|
},
|
||||||
Migrate,
|
Migrate,
|
||||||
Show {
|
Show {
|
||||||
repo: RepoId,
|
rid: RepoId,
|
||||||
revs: Vec<Rev>,
|
type_name: FilteredTypeName,
|
||||||
type_name: cob::TypeName,
|
oids: Vec<Rev>,
|
||||||
|
},
|
||||||
|
Update {
|
||||||
|
rid: RepoId,
|
||||||
|
type_name: FilteredTypeName,
|
||||||
|
oid: Rev,
|
||||||
|
message: String,
|
||||||
|
actions: PathBuf,
|
||||||
|
embeds: Vec<Embed>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,132 +116,290 @@ enum Format {
|
||||||
|
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
op: Operation,
|
op: Operation,
|
||||||
format: Format,
|
}
|
||||||
|
|
||||||
|
/// A precursor to [`cob::Embed`] used for parsing
|
||||||
|
/// that can be initialized without relying on a [`git::Repository`].
|
||||||
|
struct Embed {
|
||||||
|
name: String,
|
||||||
|
content: EmbedContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EmbedContent {
|
||||||
|
Path(PathBuf),
|
||||||
|
Hash(Rev),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A thin wrapper around [`cob::TypeName`] used for parsing.
|
||||||
|
/// Well known COB type names are captured as variants,
|
||||||
|
/// with [`FilteredTypeName::Other`] as an escape hatch for type names
|
||||||
|
/// that are not well known.
|
||||||
|
enum FilteredTypeName {
|
||||||
|
Issue,
|
||||||
|
Patch,
|
||||||
|
Identity,
|
||||||
|
Other(cob::TypeName),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<cob::TypeName> for FilteredTypeName {
|
||||||
|
fn from(value: cob::TypeName) -> Self {
|
||||||
|
if value == *cob::issue::TYPENAME {
|
||||||
|
FilteredTypeName::Issue
|
||||||
|
} else if value == *cob::patch::TYPENAME {
|
||||||
|
FilteredTypeName::Patch
|
||||||
|
} else if value == *cob::identity::TYPENAME {
|
||||||
|
FilteredTypeName::Identity
|
||||||
|
} else {
|
||||||
|
FilteredTypeName::Other(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<cob::TypeName> for FilteredTypeName {
|
||||||
|
fn as_ref(&self) -> &cob::TypeName {
|
||||||
|
match self {
|
||||||
|
FilteredTypeName::Issue => &cob::issue::TYPENAME,
|
||||||
|
FilteredTypeName::Patch => &cob::patch::TYPENAME,
|
||||||
|
FilteredTypeName::Identity => &cob::identity::TYPENAME,
|
||||||
|
FilteredTypeName::Other(value) => value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for FilteredTypeName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.as_ref().fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Embed {
|
||||||
|
fn try_into_bytes(self, repo: &git::Repository) -> anyhow::Result<cob::Embed<cob::Uri>> {
|
||||||
|
Ok(match self.content {
|
||||||
|
EmbedContent::Hash(hash) => cob::Embed {
|
||||||
|
name: self.name,
|
||||||
|
content: hash.resolve::<git::Oid>(&repo.backend)?.into(),
|
||||||
|
},
|
||||||
|
EmbedContent::Path(path) => {
|
||||||
|
cob::Embed::store(self.name, &std::fs::read(path)?, &repo.backend)?
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args for Options {
|
impl Args for Options {
|
||||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
||||||
use lexopt::prelude::*;
|
use lexopt::prelude::*;
|
||||||
|
use term::args::string;
|
||||||
|
use OperationName::*;
|
||||||
|
|
||||||
let mut parser = lexopt::Parser::from_args(args);
|
let mut parser = lexopt::Parser::from_args(args);
|
||||||
let mut op: Option<OperationName> = None;
|
|
||||||
let mut type_name: Option<cob::TypeName> = None;
|
|
||||||
let mut revs: Vec<Rev> = vec![];
|
|
||||||
let mut rid: Option<RepoId> = None;
|
|
||||||
let mut format: Option<Format> = None;
|
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
let op = match parser.next()? {
|
||||||
match arg {
|
None | Some(Long("help") | Short('h')) => {
|
||||||
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
|
||||||
"list" => op = Some(OperationName::List),
|
|
||||||
"log" => op = Some(OperationName::Log),
|
|
||||||
"migrate" => op = Some(OperationName::Migrate),
|
|
||||||
"show" => op = Some(OperationName::Show),
|
|
||||||
unknown => anyhow::bail!("unknown operation '{unknown}'"),
|
|
||||||
},
|
|
||||||
Long("type") | Short('t') => {
|
|
||||||
let v = parser.value()?;
|
|
||||||
let v = term::args::string(&v);
|
|
||||||
let v = cob::TypeName::from_str(&v)?;
|
|
||||||
|
|
||||||
type_name = Some(v);
|
|
||||||
}
|
|
||||||
Long("object") => {
|
|
||||||
let v = parser.value()?;
|
|
||||||
let v = term::args::string(&v);
|
|
||||||
|
|
||||||
revs.push(Rev::from(v));
|
|
||||||
}
|
|
||||||
Long("repo") => {
|
|
||||||
let v = parser.value()?;
|
|
||||||
let v = term::args::rid(&v)?;
|
|
||||||
|
|
||||||
rid = Some(v);
|
|
||||||
}
|
|
||||||
Long("help") | Short('h') => {
|
|
||||||
return Err(Error::Help.into());
|
return Err(Error::Help.into());
|
||||||
}
|
}
|
||||||
Long("format")
|
Some(Value(val)) => match val.to_string_lossy().as_ref() {
|
||||||
if op == Some(OperationName::Log) || op == Some(OperationName::Show) =>
|
"update" => Update,
|
||||||
{
|
"create" => Create,
|
||||||
let v: String = term::args::string(&parser.value()?);
|
"list" => List,
|
||||||
match v.as_ref() {
|
"log" => Log,
|
||||||
"pretty" if op == Some(OperationName::Log) => format = Some(Format::Pretty),
|
"migrate" => Migrate,
|
||||||
"json" => format = Some(Format::Json),
|
"show" => Show,
|
||||||
unknown => anyhow::bail!("unknown format '{unknown}'"),
|
unknown => bail!("unknown operation '{unknown}'"),
|
||||||
|
},
|
||||||
|
Some(arg) => return Err(anyhow!(arg.unexpected())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut type_name: Option<FilteredTypeName> = None;
|
||||||
|
let mut oids: Vec<Rev> = vec![];
|
||||||
|
let mut rid: Option<RepoId> = None;
|
||||||
|
let mut format: Format = Format::Pretty;
|
||||||
|
let mut message: Option<String> = None;
|
||||||
|
let mut embeds: Vec<Embed> = vec![];
|
||||||
|
let mut actions: Option<PathBuf> = None;
|
||||||
|
|
||||||
|
while let Some(arg) = parser.next()? {
|
||||||
|
match (&op, &arg) {
|
||||||
|
(_, Long("help") | Short('h')) => {
|
||||||
|
return Err(Error::Help.into());
|
||||||
|
}
|
||||||
|
(_, Long("repo") | Short('r')) => {
|
||||||
|
rid = Some(term::args::rid(&parser.value()?)?);
|
||||||
|
}
|
||||||
|
(_, Long("type") | Short('t')) => {
|
||||||
|
let v = string(&parser.value()?);
|
||||||
|
type_name = Some(FilteredTypeName::from(cob::TypeName::from_str(&v)?));
|
||||||
|
}
|
||||||
|
(Update | Log | Show, Long("object") | Short('o')) => {
|
||||||
|
let v = string(&parser.value()?);
|
||||||
|
oids.push(Rev::from(v));
|
||||||
|
}
|
||||||
|
(Update | Create, Long("message") | Short('m')) => {
|
||||||
|
message = Some(string(&parser.value()?));
|
||||||
|
}
|
||||||
|
(Log | Show | Update, Long("format")) => {
|
||||||
|
format = match (op, string(&parser.value()?).as_ref()) {
|
||||||
|
(Log, "pretty") => Format::Pretty,
|
||||||
|
(Log | Show | Update, "json") => Format::Json,
|
||||||
|
(_, unknown) => bail!("unknown format '{unknown}'"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(Update | Create, Long("embed-file")) => {
|
||||||
|
let mut values = parser.values()?;
|
||||||
|
|
||||||
|
let name = values
|
||||||
|
.next()
|
||||||
|
.map(|s| term::args::string(&s))
|
||||||
|
.ok_or(anyhow!("expected name of embed"))?;
|
||||||
|
|
||||||
|
let content = EmbedContent::Path(PathBuf::from(
|
||||||
|
values
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow!("expected path to file to embed"))?,
|
||||||
|
));
|
||||||
|
|
||||||
|
embeds.push(Embed { name, content });
|
||||||
|
}
|
||||||
|
(Update | Create, Long("embed-hash")) => {
|
||||||
|
let mut values = parser.values()?;
|
||||||
|
|
||||||
|
let name = values
|
||||||
|
.next()
|
||||||
|
.map(|s| term::args::string(&s))
|
||||||
|
.ok_or(anyhow!("expected name of embed"))?;
|
||||||
|
|
||||||
|
let content = EmbedContent::Hash(Rev::from(term::args::string(
|
||||||
|
&values
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow!("expected hash of file to embed"))?,
|
||||||
|
)));
|
||||||
|
|
||||||
|
embeds.push(Embed { name, content });
|
||||||
|
}
|
||||||
|
(Update | Create, Value(val)) => {
|
||||||
|
actions = Some(PathBuf::from(term::args::string(val)));
|
||||||
|
}
|
||||||
|
_ => return Err(anyhow!(arg.unexpected())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(anyhow::anyhow!(arg.unexpected())),
|
|
||||||
|
if op == OperationName::Migrate {
|
||||||
|
return Ok((
|
||||||
|
Options {
|
||||||
|
op: Operation::Migrate,
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let repo = rid.ok_or_else(|| anyhow!("a repository id must be specified with `--repo`"));
|
let rid = rid.ok_or_else(|| anyhow!("a repository id must be specified with `--repo`"))?;
|
||||||
let type_name =
|
let type_name =
|
||||||
type_name.ok_or_else(|| anyhow!("an object type must be specified with `--type`"));
|
type_name.ok_or_else(|| anyhow!("an object type must be specified with `--type`"))?;
|
||||||
|
|
||||||
|
let missing_oid = || anyhow!("an object id must be specified with `--object`");
|
||||||
|
let missing_message = || anyhow!("a message must be specified with `--message`");
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Options {
|
Options {
|
||||||
op: {
|
op: match op {
|
||||||
match op.ok_or_else(|| anyhow!("a command must be specified"))? {
|
Create => Operation::Create {
|
||||||
OperationName::List => Operation::List {
|
rid,
|
||||||
repo: repo?,
|
type_name,
|
||||||
type_name: type_name?,
|
message: message.ok_or_else(missing_message)?,
|
||||||
},
|
actions: actions.ok_or_else(|| {
|
||||||
OperationName::Log => Operation::Log {
|
anyhow!("a file containing initial actions must be specified")
|
||||||
repo: repo?,
|
|
||||||
rev: revs.pop().ok_or_else(|| {
|
|
||||||
anyhow!("an object id must be specified with `--object`")
|
|
||||||
})?,
|
})?,
|
||||||
type_name: type_name?,
|
embeds,
|
||||||
},
|
},
|
||||||
OperationName::Migrate => Operation::Migrate,
|
List => Operation::List { rid, type_name },
|
||||||
OperationName::Show => {
|
Log => Operation::Log {
|
||||||
if revs.is_empty() {
|
rid,
|
||||||
anyhow::bail!("an object id must be specified with `--object`")
|
type_name,
|
||||||
|
oid: oids.pop().ok_or_else(missing_oid)?,
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
Migrate => Operation::Migrate,
|
||||||
|
Show => {
|
||||||
|
if oids.is_empty() {
|
||||||
|
return Err(missing_oid());
|
||||||
}
|
}
|
||||||
Operation::Show {
|
Operation::Show {
|
||||||
repo: repo?,
|
rid,
|
||||||
revs,
|
oids,
|
||||||
type_name: type_name?,
|
type_name,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Update => Operation::Update {
|
||||||
|
rid,
|
||||||
|
type_name,
|
||||||
|
oid: oids.pop().ok_or_else(missing_oid)?,
|
||||||
|
message: message.ok_or_else(missing_message)?,
|
||||||
|
actions: actions.ok_or_else(|| {
|
||||||
|
anyhow!("a file containing actions must be specified")
|
||||||
|
})?,
|
||||||
|
embeds,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
format: format.unwrap_or(Format::Pretty),
|
|
||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
pub fn run(Options { op }: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
|
use cob::store::Store;
|
||||||
|
use FilteredTypeName::*;
|
||||||
|
use Operation::*;
|
||||||
|
|
||||||
let profile = ctx.profile()?;
|
let profile = ctx.profile()?;
|
||||||
let storage = &profile.storage;
|
let storage = &profile.storage;
|
||||||
|
|
||||||
match options.op {
|
match op {
|
||||||
Operation::List { repo, type_name } => {
|
Create {
|
||||||
let repo = storage.repository(repo)?;
|
rid,
|
||||||
let cobs = list::<NonEmpty<cob::Entry>, _>(&repo, &type_name)?;
|
|
||||||
for cob in cobs {
|
|
||||||
println!("{}", cob.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Operation::Log {
|
|
||||||
repo,
|
|
||||||
rev: oid,
|
|
||||||
type_name,
|
type_name,
|
||||||
|
message,
|
||||||
|
embeds,
|
||||||
|
actions,
|
||||||
} => {
|
} => {
|
||||||
let repo = storage.repository(repo)?;
|
let signer = &profile.signer()?;
|
||||||
let oid = oid.resolve(&repo.backend)?;
|
let repo = storage.repository_mut(rid)?;
|
||||||
let ops = cob::store::ops(&oid, &type_name, &repo)?;
|
|
||||||
|
|
||||||
for op in ops.into_iter().rev() {
|
let reader = io::BufReader::new(fs::File::open(actions)?);
|
||||||
match options.format {
|
|
||||||
Format::Json => print_op_json(op)?,
|
let embeds = embeds
|
||||||
Format::Pretty => print_op_pretty(op)?,
|
.into_iter()
|
||||||
|
.map(|embed| embed.try_into_bytes(&repo))
|
||||||
|
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let oid = match type_name {
|
||||||
|
Patch => {
|
||||||
|
let store: Store<cob::patch::Patch, _> = Store::open(&repo)?;
|
||||||
|
let actions = read_jsonl_actions(reader)?;
|
||||||
|
let (oid, _) = store.create(&message, actions, embeds, signer)?;
|
||||||
|
oid
|
||||||
}
|
}
|
||||||
|
Issue => {
|
||||||
|
let store: Store<cob::issue::Issue, _> = Store::open(&repo)?;
|
||||||
|
let actions = read_jsonl_actions(reader)?;
|
||||||
|
let (oid, _) = store.create(&message, actions, embeds, signer)?;
|
||||||
|
oid
|
||||||
}
|
}
|
||||||
|
Identity => anyhow::bail!(
|
||||||
|
"Creation of collaborative objects of type {} is not supported.",
|
||||||
|
&type_name
|
||||||
|
),
|
||||||
|
Other(type_name) => {
|
||||||
|
let store: Store<cob::external::External, _> =
|
||||||
|
Store::open_for(&type_name, &repo)?;
|
||||||
|
let actions = read_jsonl_actions(reader)?;
|
||||||
|
let (oid, _) = store.create(&message, actions, embeds, signer)?;
|
||||||
|
oid
|
||||||
}
|
}
|
||||||
Operation::Migrate => {
|
};
|
||||||
|
println!("{}", oid);
|
||||||
|
}
|
||||||
|
Migrate => {
|
||||||
let mut db = profile.cobs_db_mut()?;
|
let mut db = profile.cobs_db_mut()?;
|
||||||
if db.check_version().is_ok() {
|
if db.check_version().is_ok() {
|
||||||
term::success!("Collaborative objects database is already up to date");
|
term::success!("Collaborative objects database is already up to date");
|
||||||
|
|
@ -224,71 +410,175 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Operation::Show {
|
List { rid, type_name } => {
|
||||||
repo,
|
let repo = storage.repository(rid)?;
|
||||||
revs,
|
let cobs = radicle_cob::list::<NonEmpty<cob::Entry>, _>(&repo, type_name.as_ref())?;
|
||||||
|
for cob in cobs {
|
||||||
|
println!("{}", cob.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log {
|
||||||
|
rid,
|
||||||
|
type_name,
|
||||||
|
oid,
|
||||||
|
format,
|
||||||
|
} => {
|
||||||
|
let repo = storage.repository(rid)?;
|
||||||
|
let oid = oid.resolve(&repo.backend)?;
|
||||||
|
let ops = cob::store::ops(&oid, type_name.as_ref(), &repo)?;
|
||||||
|
|
||||||
|
for op in ops.into_iter().rev() {
|
||||||
|
match format {
|
||||||
|
Format::Json => print_op_json(op)?,
|
||||||
|
Format::Pretty => print_op_pretty(op)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Show {
|
||||||
|
rid,
|
||||||
|
oids,
|
||||||
type_name,
|
type_name,
|
||||||
} => {
|
} => {
|
||||||
let repo = storage.repository(repo)?;
|
let repo = storage.repository(rid)?;
|
||||||
|
if let Err(e) = show(oids, &repo, type_name, &profile) {
|
||||||
if let Err(e) = show(revs, &repo, type_name, &profile) {
|
if let Some(err) = e.downcast_ref::<std::io::Error>() {
|
||||||
if let Some(err) = e.downcast_ref::<io::Error>() {
|
if err.kind() == std::io::ErrorKind::BrokenPipe {
|
||||||
if err.kind() == io::ErrorKind::BrokenPipe {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Update {
|
||||||
|
rid,
|
||||||
|
type_name,
|
||||||
|
oid,
|
||||||
|
message,
|
||||||
|
actions,
|
||||||
|
embeds,
|
||||||
|
} => {
|
||||||
|
let signer = &profile.signer()?;
|
||||||
|
let repo = storage.repository_mut(rid)?;
|
||||||
|
let reader = io::BufReader::new(fs::File::open(actions)?);
|
||||||
|
let oid = &oid.resolve(&repo.backend)?;
|
||||||
|
let embeds = embeds
|
||||||
|
.into_iter()
|
||||||
|
.map(|embed| embed.try_into_bytes(&repo))
|
||||||
|
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let oid = match type_name {
|
||||||
|
Patch => {
|
||||||
|
let actions: Vec<cob::patch::Action> = read_jsonl(reader)?;
|
||||||
|
let mut patches = profile.patches_mut(&repo)?;
|
||||||
|
let mut patch = patches.get_mut(oid)?;
|
||||||
|
patch.transaction(&message, &profile.signer()?, |tx| {
|
||||||
|
tx.extend(actions)?;
|
||||||
|
tx.embed(embeds)?;
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
Issue => {
|
||||||
|
let actions: Vec<cob::issue::Action> = read_jsonl(reader)?;
|
||||||
|
let mut issues = profile.issues_mut(&repo)?;
|
||||||
|
let mut issue = issues.get_mut(oid)?;
|
||||||
|
issue.transaction(&message, &profile.signer()?, |tx| {
|
||||||
|
tx.extend(actions)?;
|
||||||
|
tx.embed(embeds)?;
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
Identity => anyhow::bail!(
|
||||||
|
"Update of collaborative objects of type {} is not supported.",
|
||||||
|
&type_name
|
||||||
|
),
|
||||||
|
Other(type_name) => {
|
||||||
|
use cob::external::{Action, External};
|
||||||
|
let actions: Vec<Action> = read_jsonl(reader)?;
|
||||||
|
let mut store: Store<External, _> = Store::open_for(&type_name, &repo)?;
|
||||||
|
let tx = cob::store::Transaction::new(type_name.clone(), actions, embeds);
|
||||||
|
let (_, oid) = tx.commit(&message, *oid, &mut store, signer)?;
|
||||||
|
oid
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{}", oid);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(
|
fn show(
|
||||||
revs: Vec<Rev>,
|
oids: Vec<Rev>,
|
||||||
repo: &git::Repository,
|
repo: &git::Repository,
|
||||||
type_name: cob::TypeName,
|
type_name: FilteredTypeName,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
|
use io::Write as _;
|
||||||
let mut stdout = std::io::stdout();
|
let mut stdout = std::io::stdout();
|
||||||
|
|
||||||
if type_name == cob::patch::TYPENAME.clone() {
|
match type_name {
|
||||||
let patches = term::cob::patches(profile, repo)?;
|
FilteredTypeName::Identity => {
|
||||||
for oid in revs {
|
use cob::identity;
|
||||||
|
for oid in oids {
|
||||||
let oid = &oid.resolve(&repo.backend)?;
|
let oid = &oid.resolve(&repo.backend)?;
|
||||||
let Some(patch) = patches.get(oid)? else {
|
let Some(cob) = cob::get::<identity::Identity, _>(repo, type_name.as_ref(), oid)?
|
||||||
anyhow::bail!(cob::store::Error::NotFound(type_name, *oid));
|
else {
|
||||||
};
|
bail!(cob::store::Error::NotFound(
|
||||||
serde_json::to_writer(&stdout, &patch)?;
|
type_name.as_ref().clone(),
|
||||||
stdout.write_all(b"\n")?;
|
*oid
|
||||||
}
|
));
|
||||||
} else if type_name == cob::issue::TYPENAME.clone() {
|
|
||||||
let issues = term::cob::issues(profile, repo)?;
|
|
||||||
for oid in revs {
|
|
||||||
let oid = &oid.resolve(&repo.backend)?;
|
|
||||||
let Some(issue) = issues.get(oid)? else {
|
|
||||||
anyhow::bail!(cob::store::Error::NotFound(type_name, *oid))
|
|
||||||
};
|
|
||||||
serde_json::to_writer(&stdout, &issue)?;
|
|
||||||
stdout.write_all(b"\n")?;
|
|
||||||
}
|
|
||||||
} else if type_name == cob::identity::TYPENAME.clone() {
|
|
||||||
for oid in revs {
|
|
||||||
let oid = &oid.resolve(&repo.backend)?;
|
|
||||||
let Some(cob) = cob::get::<Identity, _>(repo, &type_name, oid)? else {
|
|
||||||
anyhow::bail!(cob::store::Error::NotFound(type_name, *oid));
|
|
||||||
};
|
};
|
||||||
serde_json::to_writer(&stdout, &cob.object)?;
|
serde_json::to_writer(&stdout, &cob.object)?;
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")?;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
anyhow::bail!("the type name '{type_name}' is unknown");
|
FilteredTypeName::Issue => {
|
||||||
|
use radicle::issue::cache::Issues as _;
|
||||||
|
let issues = term::cob::issues(profile, repo)?;
|
||||||
|
for oid in oids {
|
||||||
|
let oid = &oid.resolve(&repo.backend)?;
|
||||||
|
let Some(issue) = issues.get(oid)? else {
|
||||||
|
bail!(cob::store::Error::NotFound(
|
||||||
|
type_name.as_ref().clone(),
|
||||||
|
*oid
|
||||||
|
))
|
||||||
|
};
|
||||||
|
serde_json::to_writer(&stdout, &issue)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilteredTypeName::Patch => {
|
||||||
|
use radicle::patch::cache::Patches as _;
|
||||||
|
let patches = term::cob::patches(profile, repo)?;
|
||||||
|
for oid in oids {
|
||||||
|
let oid = &oid.resolve(&repo.backend)?;
|
||||||
|
let Some(patch) = patches.get(oid)? else {
|
||||||
|
bail!(cob::store::Error::NotFound(
|
||||||
|
type_name.as_ref().clone(),
|
||||||
|
*oid
|
||||||
|
));
|
||||||
|
};
|
||||||
|
serde_json::to_writer(&stdout, &patch)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FilteredTypeName::Other(type_name) => {
|
||||||
|
let store =
|
||||||
|
cob::store::Store::<cob::external::External, _>::open_for(&type_name, repo)?;
|
||||||
|
for oid in oids {
|
||||||
|
let oid = &oid.resolve(&repo.backend)?;
|
||||||
|
let cob = store
|
||||||
|
.get(oid)?
|
||||||
|
.ok_or_else(|| anyhow!(cob::store::Error::NotFound(type_name.clone(), *oid)))?;
|
||||||
|
serde_json::to_writer(&stdout, &cob)?;
|
||||||
|
stdout.write_all(b"\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_op_pretty(op: Op<Vec<u8>>) -> anyhow::Result<()> {
|
fn print_op_pretty(op: cob::Op<Vec<u8>>) -> anyhow::Result<()> {
|
||||||
let time = DateTime::<Utc>::from(
|
let time = DateTime::<Utc>::from(
|
||||||
std::time::UNIX_EPOCH + std::time::Duration::from_secs(op.timestamp.as_secs()),
|
std::time::UNIX_EPOCH + std::time::Duration::from_secs(op.timestamp.as_secs()),
|
||||||
)
|
)
|
||||||
|
|
@ -317,9 +607,11 @@ fn print_op_pretty(op: Op<Vec<u8>>) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_op_json(op: Op<Vec<u8>>) -> anyhow::Result<()> {
|
fn print_op_json(op: cob::Op<Vec<u8>>) -> anyhow::Result<()> {
|
||||||
let mut ser = json!(op);
|
let mut ser = json!(op);
|
||||||
ser.as_object_mut().unwrap().insert(
|
ser.as_object_mut()
|
||||||
|
.expect("ops must serialize to objects")
|
||||||
|
.insert(
|
||||||
"actions".to_string(),
|
"actions".to_string(),
|
||||||
json!(op
|
json!(op
|
||||||
.actions
|
.actions
|
||||||
|
|
@ -332,3 +624,29 @@ fn print_op_json(op: Op<Vec<u8>>) -> anyhow::Result<()> {
|
||||||
term::print(ser);
|
term::print(ser);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Naive implementation for reading JSONL streams,
|
||||||
|
/// see <https://jsonlines.org/>.
|
||||||
|
fn read_jsonl<R, T>(reader: io::BufReader<R>) -> anyhow::Result<Vec<T>>
|
||||||
|
where
|
||||||
|
R: io::Read,
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
use io::BufRead as _;
|
||||||
|
let mut result: Vec<T> = Vec::new();
|
||||||
|
for line in reader.lines() {
|
||||||
|
result.push(serde_json::from_str(&line?)?);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tiny utility to read a [`NonEmpty`] of COB actions.
|
||||||
|
/// This is used for `rad cob create` and `rad cob update`.
|
||||||
|
fn read_jsonl_actions<R, A>(reader: io::BufReader<R>) -> anyhow::Result<NonEmpty<A>>
|
||||||
|
where
|
||||||
|
R: io::Read,
|
||||||
|
A: CobAction + serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
NonEmpty::from_vec(read_jsonl(reader)?)
|
||||||
|
.ok_or_else(|| anyhow!("at least one action is required"))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,92 @@ fn rad_issue() {
|
||||||
test("examples/rad-issue.md", &working, Some(home), []).unwrap();
|
test("examples/rad-issue.md", &working, Some(home), []).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_cob_update() {
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let profile = environment.profile(config::profile("alice"));
|
||||||
|
let working = environment.tmp().join("working");
|
||||||
|
let home = &profile.home;
|
||||||
|
|
||||||
|
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|
||||||
|
std::fs::create_dir_all(base).unwrap();
|
||||||
|
std::fs::create_dir_all(working.clone()).unwrap();
|
||||||
|
|
||||||
|
// Setup a test repository.
|
||||||
|
fixtures::repository(&working);
|
||||||
|
|
||||||
|
test("examples/rad-init.md", &working, Some(home), []).unwrap();
|
||||||
|
test("examples/rad-cob-update.md", &working, Some(home), []).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_cob_update_identity() {
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let profile = environment.profile(config::profile("alice"));
|
||||||
|
let working = environment.tmp().join("working");
|
||||||
|
let home = &profile.home;
|
||||||
|
|
||||||
|
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|
||||||
|
std::fs::create_dir_all(base).unwrap();
|
||||||
|
std::fs::create_dir_all(working.clone()).unwrap();
|
||||||
|
|
||||||
|
// Setup a test repository.
|
||||||
|
fixtures::repository(&working);
|
||||||
|
|
||||||
|
test("examples/rad-init.md", &working, Some(home), []).unwrap();
|
||||||
|
test(
|
||||||
|
"examples/rad-cob-update-identity.md",
|
||||||
|
&working,
|
||||||
|
Some(home),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_cob_multiset() {
|
||||||
|
{
|
||||||
|
// `rad-cob-multiset` is a `jq` script, which requires `jq` to be installed.
|
||||||
|
// We test whether `jq` is installed, and have this test succeed if it is not.
|
||||||
|
// Programmatic skipping of tests is not supported as of 2024-08.
|
||||||
|
|
||||||
|
let output = std::process::Command::new("/usr/bin/env")
|
||||||
|
.arg("jq")
|
||||||
|
.arg("-V")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
log::warn!(target: "test", "`jq` not found. Succeeding prematurely. {:?}", output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let profile = environment.profile(config::profile("alice"));
|
||||||
|
let home = &profile.home;
|
||||||
|
let working = environment.tmp().join("working");
|
||||||
|
|
||||||
|
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
std::fs::create_dir_all(base).unwrap();
|
||||||
|
std::fs::create_dir_all(working.clone()).unwrap();
|
||||||
|
|
||||||
|
// Copy over the script that implements the multiset COB.
|
||||||
|
std::fs::copy(
|
||||||
|
base.join("examples").join("rad-cob-multiset"),
|
||||||
|
working.join("rad-cob-multiset"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Setup a test repository.
|
||||||
|
fixtures::repository(&working);
|
||||||
|
|
||||||
|
test("examples/rad-init.md", &working, Some(home), []).unwrap();
|
||||||
|
test("examples/rad-cob-multiset.md", &working, Some(home), []).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rad_cob_log() {
|
fn rad_cob_log() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,7 @@ fn update_or_remove<R, C, T>(
|
||||||
) -> Result<(), error::Cache>
|
) -> Result<(), error::Cache>
|
||||||
where
|
where
|
||||||
R: cob::Store + ReadRepository,
|
R: cob::Store + ReadRepository,
|
||||||
T: cob::Evaluate<R> + cob::store::Cob,
|
T: cob::Evaluate<R> + cob::store::Cob + cob::store::CobWithType,
|
||||||
C: cob::cache::Update<T> + cob::cache::Remove<T>,
|
C: cob::cache::Update<T> + cob::cache::Remove<T>,
|
||||||
{
|
{
|
||||||
match store.get(&tid.id) {
|
match store.get(&tid.id) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#![warn(clippy::unwrap_used)]
|
#![warn(clippy::unwrap_used)]
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
pub mod external;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod issue;
|
pub mod issue;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
//! # External Collaborative Objects
|
||||||
|
//!
|
||||||
|
//! This module provides an interface for external helper programs to provide
|
||||||
|
//! the evaluation logic of Collaborative Objects (COBs).
|
||||||
|
//!
|
||||||
|
//! An external COB is one that relies on an external program (so called
|
||||||
|
//! "helper", is an executable file, for example a script, or a binary), that
|
||||||
|
//! implements the evaluation logic for that particular COB:
|
||||||
|
//! Whenever an operation is to be applied to an external COB, the helper is
|
||||||
|
//! invoked with the current state of the COB and the operation to be applied.
|
||||||
|
//! It then returns the new state of the COB, according to its internal logic.
|
||||||
|
//! This concept is borrowed from Git, which supports [remote helpers] and
|
||||||
|
//! [credential helpers].
|
||||||
|
//!
|
||||||
|
//! External COBs must be based on JSON, that is, the COB itself and associated
|
||||||
|
//! actions must serialize to and deserialize from JSON.
|
||||||
|
//! Further, the helper must be able to communicate to Radicle using
|
||||||
|
//! [JSON Lines] (see further details below).
|
||||||
|
//!
|
||||||
|
//! # Invocation
|
||||||
|
//!
|
||||||
|
//! The helper is invoked by Radicle without command line arguments.
|
||||||
|
//! In the future, more arguments might be added.
|
||||||
|
//!
|
||||||
|
//! # Helper Protocol
|
||||||
|
//!
|
||||||
|
//! Radicle and the helper communicate back and forth [JSON Lines] via standard
|
||||||
|
//! streams.
|
||||||
|
//!
|
||||||
|
//! 1. The helper must read and process at least one JSON Line (containing one
|
||||||
|
//! operation) from standard input, which represents the operation to be
|
||||||
|
//! applied to the COB, along with possible concurrent operations.
|
||||||
|
//! 2. The helper must write the new state of COB to standard output in a JSON
|
||||||
|
//! Line.
|
||||||
|
//! 3. The helper may read additional JSON Lines from standard input, these are
|
||||||
|
//! to be applied "on top of" the previous operations.
|
||||||
|
//! 4. The helper must reply with the state of the COB in a JSON Line after
|
||||||
|
//! processing each operation.
|
||||||
|
//! 5. The helper must exit with a status code of zero on success, and a
|
||||||
|
//! non-zero status code on failure.
|
||||||
|
//! 6. The helper may write to standard error for logging and debugging
|
||||||
|
//! purposes.
|
||||||
|
//!
|
||||||
|
//! # Syntax of Operations
|
||||||
|
//!
|
||||||
|
//! The operations sent from Radicle to the helper are of the following shape:
|
||||||
|
//!
|
||||||
|
//! ```json
|
||||||
|
//! {
|
||||||
|
//! "title": "Operation as sent to helper"
|
||||||
|
//! "type": "object",
|
||||||
|
//! "properties": {
|
||||||
|
//! "concurrent": {
|
||||||
|
//! "type": "array",
|
||||||
|
//! "items": {
|
||||||
|
//! "$ref": "#/definitions/radicle::cob::Op"
|
||||||
|
//! }
|
||||||
|
//! },
|
||||||
|
//! "value": {
|
||||||
|
//! "type": "object",
|
||||||
|
//! "properties": {
|
||||||
|
//! "prop": {
|
||||||
|
//! "$ref": "#/definitions/radicle::cob::external::External"
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! },
|
||||||
|
//! "op": {
|
||||||
|
//! "type": "object",
|
||||||
|
//! "properties": {
|
||||||
|
//! "prop": {
|
||||||
|
//! "$ref": "#/definitions/radicle::cob::Op"
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! },
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [JSON Lines]: https://jsonlines.org/
|
||||||
|
//! [credential helpers]: https://git-scm.com/doc/credential-helpers
|
||||||
|
//! [remote helpers]: https://git-scm.com/docs/gitremote-helpers
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Error as IoError, ErrorKind};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use serde_json::{from_slice, to_writer, Error as JsonError, Map, Value};
|
||||||
|
|
||||||
|
use crate::cob::object::collaboration::Evaluate;
|
||||||
|
use crate::cob::op::{Op as CobOp, OpEncodingError};
|
||||||
|
use crate::cob::store::{Cob, CobAction};
|
||||||
|
use crate::git::Oid;
|
||||||
|
use crate::storage::ReadRepository;
|
||||||
|
|
||||||
|
/// This prefix is used to generate the name of the command,
|
||||||
|
/// which is executed by the helper to apply operations.
|
||||||
|
static COB_EXTERNAL_COMMAND_PREFIX: &str = "rad-cob-";
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct External(Value);
|
||||||
|
|
||||||
|
impl Default for External {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Value::Object(Map::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("op decoding failed: {0}")]
|
||||||
|
Op(#[from] OpEncodingError),
|
||||||
|
#[error("serde_json: {0}")]
|
||||||
|
Serde(#[from] JsonError),
|
||||||
|
#[error("io: {0}")]
|
||||||
|
Io(#[from] IoError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Action {
|
||||||
|
#[serde(default)]
|
||||||
|
parents: Vec<Oid>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
map: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CobAction for Action {
|
||||||
|
fn parents(&self) -> Vec<Oid> {
|
||||||
|
self.parents.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Action> for nonempty::NonEmpty<Action> {
|
||||||
|
fn from(action: Action) -> Self {
|
||||||
|
Self::new(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Op = CobOp<Action>;
|
||||||
|
|
||||||
|
impl External {
|
||||||
|
fn handle<R: ReadRepository>(
|
||||||
|
&mut self,
|
||||||
|
op: Op,
|
||||||
|
concurrent: Vec<Op>,
|
||||||
|
_repo: &R,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let command_name = {
|
||||||
|
let prefix = String::from(COB_EXTERNAL_COMMAND_PREFIX);
|
||||||
|
let type_name = op.manifest.type_name.to_string();
|
||||||
|
let suffix = type_name
|
||||||
|
.rsplit_once('.')
|
||||||
|
.map(|(_, suffix)| suffix)
|
||||||
|
.unwrap_or(type_name.as_str());
|
||||||
|
prefix + suffix
|
||||||
|
};
|
||||||
|
|
||||||
|
let child = Command::new(command_name)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let Some(stdin) = &child.stdin else {
|
||||||
|
return Err(Error::Io(IoError::new(
|
||||||
|
ErrorKind::BrokenPipe,
|
||||||
|
"stdin not available",
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct OpMessage {
|
||||||
|
value: Value,
|
||||||
|
op: Op,
|
||||||
|
concurrent: Vec<Op>,
|
||||||
|
}
|
||||||
|
|
||||||
|
to_writer(
|
||||||
|
stdin,
|
||||||
|
&OpMessage {
|
||||||
|
value: self.0.clone(),
|
||||||
|
op,
|
||||||
|
concurrent,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.0 = from_slice(&child.wait_with_output()?.stdout)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cob for External {
|
||||||
|
type Action = Action;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn from_root<R: ReadRepository>(
|
||||||
|
op: super::Op<Self::Action>,
|
||||||
|
repo: &R,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let mut root = Self::default();
|
||||||
|
root.handle(op, vec![], repo)?;
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a super::Entry>>(
|
||||||
|
&mut self,
|
||||||
|
op: super::Op<Self::Action>,
|
||||||
|
concurrent: I,
|
||||||
|
repo: &R,
|
||||||
|
) -> Result<(), <Self as Cob>::Error> {
|
||||||
|
let concurrent: Vec<Op> = concurrent
|
||||||
|
.into_iter()
|
||||||
|
.map(Op::try_from)
|
||||||
|
.collect::<Result<Vec<Op>, _>>()?;
|
||||||
|
self.handle(op, concurrent, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: ReadRepository> Evaluate<R> for External {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn init(entry: &radicle_cob::Entry, store: &R) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_root(Op::try_from(entry)?, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply<'a, I: Iterator<Item = (&'a radicle_git_ext::Oid, &'a radicle_cob::Entry)>>(
|
||||||
|
&mut self,
|
||||||
|
entry: &radicle_cob::Entry,
|
||||||
|
concurrent: I,
|
||||||
|
repo: &R,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
let concurrent: Vec<Op> = concurrent
|
||||||
|
.map(|(_, e)| e)
|
||||||
|
.map(Op::try_from)
|
||||||
|
.collect::<Result<Vec<Op>, _>>()?;
|
||||||
|
self.handle(Op::try_from(entry)?, concurrent, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -155,6 +155,12 @@ pub struct Identity {
|
||||||
timeline: Vec<EntryId>,
|
timeline: Vec<EntryId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl cob::store::CobWithType for Identity {
|
||||||
|
fn type_name() -> &'static TypeName {
|
||||||
|
&TYPENAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for Identity {
|
impl std::ops::Deref for Identity {
|
||||||
type Target = Revision;
|
type Target = Revision;
|
||||||
|
|
||||||
|
|
@ -206,6 +212,8 @@ impl Identity {
|
||||||
object: &ObjectId,
|
object: &ObjectId,
|
||||||
repo: &R,
|
repo: &R,
|
||||||
) -> Result<Identity, store::Error> {
|
) -> Result<Identity, store::Error> {
|
||||||
|
use cob::store::CobWithType;
|
||||||
|
|
||||||
cob::get::<Self, _>(repo, Self::type_name(), object)
|
cob::get::<Self, _>(repo, Self::type_name(), object)
|
||||||
.map(|r| r.map(|cob| cob.object))?
|
.map(|r| r.map(|cob| cob.object))?
|
||||||
.ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
|
.ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
|
||||||
|
|
@ -293,10 +301,6 @@ impl store::Cob for Identity {
|
||||||
type Action = Action;
|
type Action = Action;
|
||||||
type Error = ApplyError;
|
type Error = ApplyError;
|
||||||
|
|
||||||
fn type_name() -> &'static TypeName {
|
|
||||||
&TYPENAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
||||||
let mut actions = op.actions.into_iter();
|
let mut actions = op.actions.into_iter();
|
||||||
let Some(Action::Revision {
|
let Some(Action::Revision {
|
||||||
|
|
|
||||||
|
|
@ -142,13 +142,15 @@ pub struct Issue {
|
||||||
pub(super) thread: Thread,
|
pub(super) thread: Thread,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl store::Cob for Issue {
|
impl cob::store::CobWithType for Issue {
|
||||||
type Action = Action;
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn type_name() -> &'static TypeName {
|
fn type_name() -> &'static TypeName {
|
||||||
&TYPENAME
|
&TYPENAME
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl store::Cob for Issue {
|
||||||
|
type Action = Action;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
||||||
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
||||||
|
|
@ -941,7 +943,7 @@ mod test {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::cob::{ActorId, Reaction};
|
use crate::cob::{store::CobWithType, ActorId, Reaction};
|
||||||
use crate::git::Oid;
|
use crate::git::Oid;
|
||||||
use crate::issue::cache::Issues as _;
|
use crate::issue::cache::Issues as _;
|
||||||
use crate::test;
|
use crate::test;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ use crate::git;
|
||||||
use crate::prelude::ReadRepository;
|
use crate::prelude::ReadRepository;
|
||||||
use crate::storage::{Oid, WriteRepository};
|
use crate::storage::{Oid, WriteRepository};
|
||||||
|
|
||||||
|
use super::store::CobWithType;
|
||||||
|
|
||||||
/// The name of this COB type. Note that this is a "beta" COB, which
|
/// The name of this COB type. Note that this is a "beta" COB, which
|
||||||
/// means it's not meant for others to rely on yet, and we may change
|
/// means it's not meant for others to rely on yet, and we may change
|
||||||
/// it without warning.
|
/// it without warning.
|
||||||
|
|
@ -213,10 +215,6 @@ impl Cob for Job {
|
||||||
type Action = Action;
|
type Action = Action;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn type_name() -> &'static TypeName {
|
|
||||||
&TYPENAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_root<R: ReadRepository>(op: Op, _repo: &R) -> Result<Self, Self::Error> {
|
fn from_root<R: ReadRepository>(op: Op, _repo: &R) -> Result<Self, Self::Error> {
|
||||||
let mut actions = op.actions.into_iter();
|
let mut actions = op.actions.into_iter();
|
||||||
let Some(Action::Trigger { commit }) = actions.next() else {
|
let Some(Action::Trigger { commit }) = actions.next() else {
|
||||||
|
|
@ -245,6 +243,12 @@ impl Cob for Job {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CobWithType for Job {
|
||||||
|
fn type_name() -> &'static TypeName {
|
||||||
|
&TYPENAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<R: ReadRepository> cob::Evaluate<R> for Job {
|
impl<R: ReadRepository> cob::Evaluate<R> for Job {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ pub enum Error {
|
||||||
/// This error indicates that the operations are not being applied
|
/// This error indicates that the operations are not being applied
|
||||||
/// in causal order, which is a requirement for this CRDT.
|
/// in causal order, which is a requirement for this CRDT.
|
||||||
///
|
///
|
||||||
/// For example, this can occur if an operation references anothern operation
|
/// For example, this can occur if an operation references another operation
|
||||||
/// that hasn't happened yet.
|
/// that hasn't happened yet.
|
||||||
#[error("causal dependency {0:?} missing")]
|
#[error("causal dependency {0:?} missing")]
|
||||||
Missing(EntryId),
|
Missing(EntryId),
|
||||||
|
|
@ -1156,13 +1156,15 @@ impl Patch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl store::Cob for Patch {
|
impl cob::store::CobWithType for Patch {
|
||||||
type Action = Action;
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn type_name() -> &'static TypeName {
|
fn type_name() -> &'static TypeName {
|
||||||
&TYPENAME
|
&TYPENAME
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl store::Cob for Patch {
|
||||||
|
type Action = Action;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
|
||||||
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
let doc = op.identity_doc(repo)?.ok_or(Error::MissingIdentity)?;
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@ pub trait Cob: Sized + PartialEq + Debug {
|
||||||
/// Error returned by `apply` function.
|
/// Error returned by `apply` function.
|
||||||
type Error: std::error::Error + Send + Sync + 'static;
|
type Error: std::error::Error + Send + Sync + 'static;
|
||||||
|
|
||||||
/// The object type name.
|
|
||||||
fn type_name() -> &'static TypeName;
|
|
||||||
|
|
||||||
/// Initialize a collarorative object from a root operation.
|
/// Initialize a collarorative object from a root operation.
|
||||||
fn from_root<R: ReadRepository>(op: Op<Self::Action>, repo: &R) -> Result<Self, Self::Error>;
|
fn from_root<R: ReadRepository>(op: Op<Self::Action>, repo: &R) -> Result<Self, Self::Error>;
|
||||||
|
|
||||||
|
|
@ -50,7 +47,10 @@ pub trait Cob: Sized + PartialEq + Debug {
|
||||||
fn from_history<R: ReadRepository>(
|
fn from_history<R: ReadRepository>(
|
||||||
history: &crate::cob::History,
|
history: &crate::cob::History,
|
||||||
repo: &R,
|
repo: &R,
|
||||||
) -> Result<Self, test::HistoryError<Self>> {
|
) -> Result<Self, test::HistoryError<Self>>
|
||||||
|
where
|
||||||
|
Self: CobWithType,
|
||||||
|
{
|
||||||
test::from_history::<R, Self>(history, repo)
|
test::from_history::<R, Self>(history, repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +73,15 @@ pub trait Cob: Sized + PartialEq + Debug {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implementations are statically associated with a particular
|
||||||
|
/// type name of a collaborative object.
|
||||||
|
///
|
||||||
|
/// In most cases, this trait should be used in tandem with [`Cob`].
|
||||||
|
pub trait CobWithType {
|
||||||
|
/// The type name of the collaborative object type which backs this implementation.
|
||||||
|
fn type_name() -> &'static TypeName;
|
||||||
|
}
|
||||||
|
|
||||||
/// Store error.
|
/// Store error.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -109,6 +118,7 @@ pub struct Store<'a, T, R> {
|
||||||
identity: Option<git::Oid>,
|
identity: Option<git::Oid>,
|
||||||
repo: &'a R,
|
repo: &'a R,
|
||||||
witness: PhantomData<T>,
|
witness: PhantomData<T>,
|
||||||
|
type_name: &'a TypeName,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, R> AsRef<R> for Store<'_, T, R> {
|
impl<T, R> AsRef<R> for Store<'_, T, R> {
|
||||||
|
|
@ -122,11 +132,12 @@ where
|
||||||
R: ReadRepository + cob::Store,
|
R: ReadRepository + cob::Store,
|
||||||
{
|
{
|
||||||
/// Open a new generic store.
|
/// Open a new generic store.
|
||||||
pub fn open(repo: &'a R) -> Result<Self, Error> {
|
pub fn open_for(type_name: &'a TypeName, repo: &'a R) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
repo,
|
repo,
|
||||||
identity: None,
|
identity: None,
|
||||||
witness: PhantomData,
|
witness: PhantomData,
|
||||||
|
type_name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,10 +147,41 @@ where
|
||||||
repo: self.repo,
|
repo: self.repo,
|
||||||
witness: self.witness,
|
witness: self.witness,
|
||||||
identity: Some(identity),
|
identity: Some(identity),
|
||||||
|
type_name: self.type_name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, T, R> Store<'a, T, R>
|
||||||
|
where
|
||||||
|
R: ReadRepository + cob::Store,
|
||||||
|
T: CobWithType,
|
||||||
|
{
|
||||||
|
/// Open a new generic store.
|
||||||
|
pub fn open(repo: &'a R) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
repo,
|
||||||
|
identity: None,
|
||||||
|
witness: PhantomData,
|
||||||
|
type_name: T::type_name(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, R> Store<'_, T, R>
|
||||||
|
where
|
||||||
|
R: ReadRepository + cob::Store,
|
||||||
|
T: Cob + cob::Evaluate<R>,
|
||||||
|
{
|
||||||
|
pub fn transaction(
|
||||||
|
&self,
|
||||||
|
actions: Vec<T::Action>,
|
||||||
|
embeds: Vec<Embed<Uri>>,
|
||||||
|
) -> Transaction<T, R> {
|
||||||
|
Transaction::new(self.type_name.clone(), actions, embeds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T, R> Store<'_, T, R>
|
impl<T, R> Store<'_, T, R>
|
||||||
where
|
where
|
||||||
R: ReadRepository + SignRepository + cob::Store,
|
R: ReadRepository + SignRepository + cob::Store,
|
||||||
|
|
@ -149,6 +191,7 @@ where
|
||||||
/// Update an object.
|
/// Update an object.
|
||||||
pub fn update<G: Signer>(
|
pub fn update<G: Signer>(
|
||||||
&self,
|
&self,
|
||||||
|
type_name: &TypeName,
|
||||||
object_id: ObjectId,
|
object_id: ObjectId,
|
||||||
message: &str,
|
message: &str,
|
||||||
actions: impl Into<NonEmpty<T::Action>>,
|
actions: impl Into<NonEmpty<T::Action>>,
|
||||||
|
|
@ -175,7 +218,7 @@ where
|
||||||
signer.public_key(),
|
signer.public_key(),
|
||||||
Update {
|
Update {
|
||||||
object_id,
|
object_id,
|
||||||
type_name: T::type_name().clone(),
|
type_name: type_name.clone(),
|
||||||
message: message.to_owned(),
|
message: message.to_owned(),
|
||||||
embeds,
|
embeds,
|
||||||
changes,
|
changes,
|
||||||
|
|
@ -215,7 +258,7 @@ where
|
||||||
parents,
|
parents,
|
||||||
signer.public_key(),
|
signer.public_key(),
|
||||||
Create {
|
Create {
|
||||||
type_name: T::type_name().clone(),
|
type_name: self.type_name.clone(),
|
||||||
version: Version::default(),
|
version: Version::default(),
|
||||||
message: message.to_owned(),
|
message: message.to_owned(),
|
||||||
embeds,
|
embeds,
|
||||||
|
|
@ -225,7 +268,7 @@ where
|
||||||
// Nb. We can't sign our refs before the identity refs exist, which are created after
|
// Nb. We can't sign our refs before the identity refs exist, which are created after
|
||||||
// the identity COB is created. Therefore we manually sign refs when creating identity
|
// the identity COB is created. Therefore we manually sign refs when creating identity
|
||||||
// COBs.
|
// COBs.
|
||||||
if T::type_name() != &*crate::cob::identity::TYPENAME {
|
if self.type_name != &*crate::cob::identity::TYPENAME {
|
||||||
self.repo
|
self.repo
|
||||||
.sign_refs(signer)
|
.sign_refs(signer)
|
||||||
.map_err(|e| Error::SignRefs(Box::new(e)))?;
|
.map_err(|e| Error::SignRefs(Box::new(e)))?;
|
||||||
|
|
@ -235,13 +278,13 @@ where
|
||||||
|
|
||||||
/// Remove an object.
|
/// Remove an object.
|
||||||
pub fn remove<G: Signer>(&self, id: &ObjectId, signer: &G) -> Result<(), Error> {
|
pub fn remove<G: Signer>(&self, id: &ObjectId, signer: &G) -> Result<(), Error> {
|
||||||
let name = git::refs::storage::cob(signer.public_key(), T::type_name(), id);
|
let name = git::refs::storage::cob(signer.public_key(), self.type_name, id);
|
||||||
match self
|
match self
|
||||||
.repo
|
.repo
|
||||||
.reference_oid(signer.public_key(), &name.strip_namespace())
|
.reference_oid(signer.public_key(), &name.strip_namespace())
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
cob::remove(self.repo, signer.public_key(), T::type_name(), id)?;
|
cob::remove(self.repo, signer.public_key(), self.type_name, id)?;
|
||||||
self.repo
|
self.repo
|
||||||
.sign_refs(signer)
|
.sign_refs(signer)
|
||||||
.map_err(|e| Error::SignRefs(Box::new(e)))?;
|
.map_err(|e| Error::SignRefs(Box::new(e)))?;
|
||||||
|
|
@ -259,12 +302,12 @@ where
|
||||||
impl<'a, T, R> Store<'a, T, R>
|
impl<'a, T, R> Store<'a, T, R>
|
||||||
where
|
where
|
||||||
R: ReadRepository + cob::Store,
|
R: ReadRepository + cob::Store,
|
||||||
T: cob::Evaluate<R> + Cob,
|
T: Cob + cob::Evaluate<R>,
|
||||||
T::Action: Serialize,
|
T::Action: Serialize,
|
||||||
{
|
{
|
||||||
/// Get an object.
|
/// Get an object.
|
||||||
pub fn get(&self, id: &ObjectId) -> Result<Option<T>, Error> {
|
pub fn get(&self, id: &ObjectId) -> Result<Option<T>, Error> {
|
||||||
cob::get::<T, _>(self.repo, T::type_name(), id)
|
cob::get::<T, _>(self.repo, self.type_name, id)
|
||||||
.map(|r| r.map(|cob| cob.object))
|
.map(|r| r.map(|cob| cob.object))
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +316,7 @@ where
|
||||||
pub fn all(
|
pub fn all(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + 'a, Error> {
|
) -> Result<impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + 'a, Error> {
|
||||||
let raw = cob::list::<T, _>(self.repo, T::type_name())?;
|
let raw = cob::list::<T, _>(self.repo, self.type_name)?;
|
||||||
|
|
||||||
Ok(raw.into_iter().map(|o| Ok((*o.id(), o.object))))
|
Ok(raw.into_iter().map(|o| Ok((*o.id(), o.object))))
|
||||||
}
|
}
|
||||||
|
|
@ -285,7 +328,7 @@ where
|
||||||
|
|
||||||
/// Return objects count.
|
/// Return objects count.
|
||||||
pub fn count(&self) -> Result<usize, Error> {
|
pub fn count(&self) -> Result<usize, Error> {
|
||||||
let raw = cob::list::<T, _>(self.repo, T::type_name())?;
|
let raw = cob::list::<T, _>(self.repo, self.type_name)?;
|
||||||
|
|
||||||
Ok(raw.len())
|
Ok(raw.len())
|
||||||
}
|
}
|
||||||
|
|
@ -296,15 +339,18 @@ where
|
||||||
pub struct Transaction<T: Cob + cob::Evaluate<R>, R> {
|
pub struct Transaction<T: Cob + cob::Evaluate<R>, R> {
|
||||||
actions: Vec<T::Action>,
|
actions: Vec<T::Action>,
|
||||||
embeds: Vec<Embed<Uri>>,
|
embeds: Vec<Embed<Uri>>,
|
||||||
|
|
||||||
repo: PhantomData<R>,
|
repo: PhantomData<R>,
|
||||||
|
type_name: TypeName,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Cob + cob::Evaluate<R>, R> Default for Transaction<T, R> {
|
impl<T: Cob + CobWithType + cob::Evaluate<R>, R> Default for Transaction<T, R> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
actions: Vec::new(),
|
actions: Vec::new(),
|
||||||
embeds: Vec::new(),
|
embeds: Vec::new(),
|
||||||
repo: PhantomData,
|
repo: PhantomData,
|
||||||
|
type_name: T::type_name().clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -312,6 +358,20 @@ impl<T: Cob + cob::Evaluate<R>, R> Default for Transaction<T, R> {
|
||||||
impl<T, R> Transaction<T, R>
|
impl<T, R> Transaction<T, R>
|
||||||
where
|
where
|
||||||
T: Cob + cob::Evaluate<R>,
|
T: Cob + cob::Evaluate<R>,
|
||||||
|
{
|
||||||
|
pub fn new(type_name: TypeName, actions: Vec<T::Action>, embeds: Vec<Embed<Uri>>) -> Self {
|
||||||
|
Self {
|
||||||
|
actions,
|
||||||
|
embeds,
|
||||||
|
repo: PhantomData,
|
||||||
|
type_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, R> Transaction<T, R>
|
||||||
|
where
|
||||||
|
T: Cob + CobWithType + cob::Evaluate<R>,
|
||||||
{
|
{
|
||||||
/// Create a new transaction to be used as the initial set of operations for a COB.
|
/// Create a new transaction to be used as the initial set of operations for a COB.
|
||||||
pub fn initial<G, F, Tx>(
|
pub fn initial<G, F, Tx>(
|
||||||
|
|
@ -337,14 +397,26 @@ where
|
||||||
|
|
||||||
store.create(message, actions, tx.embeds, signer)
|
store.create(message, actions, tx.embeds, signer)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Add an operation to this transaction.
|
impl<T, R> Transaction<T, R>
|
||||||
|
where
|
||||||
|
T: Cob + cob::Evaluate<R>,
|
||||||
|
{
|
||||||
|
/// Add an action to this transaction.
|
||||||
pub fn push(&mut self, action: T::Action) -> Result<(), Error> {
|
pub fn push(&mut self, action: T::Action) -> Result<(), Error> {
|
||||||
self.actions.push(action);
|
self.actions.push(action);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add actions to this transaction.
|
||||||
|
pub fn extend(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Result<(), Error> {
|
||||||
|
self.actions.extend(actions);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Embed media into the transaction.
|
/// Embed media into the transaction.
|
||||||
pub fn embed(&mut self, embeds: impl IntoIterator<Item = Embed<Uri>>) -> Result<(), Error> {
|
pub fn embed(&mut self, embeds: impl IntoIterator<Item = Embed<Uri>>) -> Result<(), Error> {
|
||||||
self.embeds.extend(embeds);
|
self.embeds.extend(embeds);
|
||||||
|
|
@ -372,7 +444,7 @@ where
|
||||||
head,
|
head,
|
||||||
object: CollaborativeObject { object, .. },
|
object: CollaborativeObject { object, .. },
|
||||||
..
|
..
|
||||||
} = store.update(id, msg, actions, self.embeds, signer)?;
|
} = store.update(&self.type_name, id, msg, actions, self.embeds, signer)?;
|
||||||
|
|
||||||
Ok((object, head))
|
Ok((object, head))
|
||||||
}
|
}
|
||||||
|
|
@ -424,7 +496,7 @@ pub mod test {
|
||||||
|
|
||||||
/// Turn a history into a concrete type, by traversing the history and applying each operation
|
/// Turn a history into a concrete type, by traversing the history and applying each operation
|
||||||
/// to the state, skipping branches that return errors.
|
/// to the state, skipping branches that return errors.
|
||||||
pub fn from_history<R: ReadRepository, T: Cob>(
|
pub fn from_history<R: ReadRepository, T: Cob + CobWithType>(
|
||||||
history: &crate::cob::History,
|
history: &crate::cob::History,
|
||||||
repo: &R,
|
repo: &R,
|
||||||
) -> Result<T, HistoryError<T>> {
|
) -> Result<T, HistoryError<T>> {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use crate::profile::env;
|
||||||
use crate::storage::ReadRepository;
|
use crate::storage::ReadRepository;
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
|
|
||||||
use super::store::Cob;
|
use super::store::{Cob, CobWithType};
|
||||||
use super::thread;
|
use super::thread;
|
||||||
|
|
||||||
/// Convenience type for building histories.
|
/// Convenience type for building histories.
|
||||||
|
|
@ -57,6 +57,7 @@ impl HistoryBuilder<thread::Thread> {
|
||||||
|
|
||||||
impl<T: Cob> HistoryBuilder<T>
|
impl<T: Cob> HistoryBuilder<T>
|
||||||
where
|
where
|
||||||
|
T: CobWithType,
|
||||||
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
|
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
|
||||||
{
|
{
|
||||||
pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
|
pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
|
||||||
|
|
@ -133,12 +134,13 @@ impl<A> Deref for HistoryBuilder<A> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new test history.
|
/// Create a new test history.
|
||||||
pub fn history<T: Cob, G: Signer>(
|
pub fn history<T, G: Signer>(
|
||||||
actions: &[T::Action],
|
actions: &[T::Action],
|
||||||
time: Timestamp,
|
time: Timestamp,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> HistoryBuilder<T>
|
) -> HistoryBuilder<T>
|
||||||
where
|
where
|
||||||
|
T: Cob + CobWithType,
|
||||||
T::Action: Serialize + Eq + 'static,
|
T::Action: Serialize + Eq + 'static,
|
||||||
{
|
{
|
||||||
HistoryBuilder::new(actions, time, signer)
|
HistoryBuilder::new(actions, time, signer)
|
||||||
|
|
@ -163,13 +165,14 @@ impl<G> Actor<G> {
|
||||||
|
|
||||||
impl<G: Signer> Actor<G> {
|
impl<G: Signer> Actor<G> {
|
||||||
/// Create a new operation.
|
/// Create a new operation.
|
||||||
pub fn op_with<T: Cob>(
|
pub fn op_with<T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
actions: impl IntoIterator<Item = T::Action>,
|
actions: impl IntoIterator<Item = T::Action>,
|
||||||
identity: Option<Oid>,
|
identity: Option<Oid>,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
) -> Op<T::Action>
|
) -> Op<T::Action>
|
||||||
where
|
where
|
||||||
|
T: Cob + CobWithType,
|
||||||
T::Action: Clone + Serialize,
|
T::Action: Clone + Serialize,
|
||||||
{
|
{
|
||||||
let actions = actions.into_iter().collect::<Vec<_>>();
|
let actions = actions.into_iter().collect::<Vec<_>>();
|
||||||
|
|
@ -199,8 +202,9 @@ impl<G: Signer> Actor<G> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new operation.
|
/// Create a new operation.
|
||||||
pub fn op<T: Cob>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
|
pub fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
|
||||||
where
|
where
|
||||||
|
T: Cob + CobWithType,
|
||||||
T::Action: Clone + Serialize,
|
T::Action: Clone + Serialize,
|
||||||
{
|
{
|
||||||
let identity = arbitrary::oid();
|
let identity = arbitrary::oid();
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,12 @@ impl<T> Default for Thread<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> cob::store::CobWithType for Thread<T> {
|
||||||
|
fn type_name() -> &'static radicle_cob::TypeName {
|
||||||
|
&TYPENAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Thread<T> {
|
impl<T> Thread<T> {
|
||||||
pub fn new(id: CommentId, comment: T) -> Self {
|
pub fn new(id: CommentId, comment: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -403,10 +409,6 @@ impl cob::store::Cob for Thread {
|
||||||
type Action = Action;
|
type Action = Action;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn type_name() -> &'static radicle_cob::TypeName {
|
|
||||||
&TYPENAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_root<R: ReadRepository>(op: Op<Action>, repo: &R) -> Result<Self, Self::Error> {
|
fn from_root<R: ReadRepository>(op: Op<Action>, repo: &R) -> Result<Self, Self::Error> {
|
||||||
let author = op.author;
|
let author = op.author;
|
||||||
let entry = op.id;
|
let entry = op.id;
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ pub use did::Did;
|
||||||
pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RawDoc, RepoId, Visibility};
|
pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RawDoc, RepoId, Visibility};
|
||||||
pub use project::Project;
|
pub use project::Project;
|
||||||
|
|
||||||
pub use crate::cob::identity::{Error, Identity, IdentityMut};
|
pub use crate::cob::identity::{Action, Error, Identity, IdentityMut, TYPENAME};
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub mod test;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
pub use cob::{issue, patch};
|
pub use cob::{external, issue, patch};
|
||||||
pub use node::Node;
|
pub use node::Node;
|
||||||
pub use profile::Profile;
|
pub use profile::Profile;
|
||||||
pub use storage::git::Storage;
|
pub use storage::git::Storage;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue