Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Canonical References
Draft fintohaps opened 1 year ago

See RIP-00041 for the specification.

This patch is an implementation of RIP-0004. It implements the rules mechanism within the rules module. This is interplays with the existing canonical mechanisms, already defined (but slightly refactored).

The rules are then used in pushing and fetching references. A test is added to illustrate the canonical references in action via tags.

There were some incidental changes that were made to ensure the tags use case is easy for users. The first change was to add a tags refspec to remotes in order to easily fetch tags from peers – as well ensuring those tags do not pollute the refs/tags namespace in the working copy.

This had a knock on change where there was a bug libgit2 that didn’t allow for deleting multivar entries, which the new remote setup fell under. This was fixed and so we update to git2-0.19.

As well this, the rad id update command would error if the payload identifier was not the project identifier. This would stop adding new payloads to extend the identity – which was needed for introducing canonical references.

1

https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/patches/1d1ce874f7c39ecdcd8c318bbae46ffd02fe1ea8?tab=changes

126 files changed +4642 -1575 59a10214 34014a67
modified Cargo.lock
@@ -693,6 +693,12 @@ dependencies = [
]

[[package]]
+
name = "fast-glob"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3afcf4effa2c44390b9912544582d5af29e10dc4c816c5dbebf748e1c7416faa"
+

+
[[package]]
name = "faster-hex"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -791,9 +797,9 @@ dependencies = [

[[package]]
name = "git-ref-format"
-
version = "0.3.0"
+
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "137adab7111fcb575db6f07dae3a7d37f3c2630878954c9931f7135dfa33eeef"
+
checksum = "7428e0d6e549a9a613d6f019b839a0f5142c331295b79e119ca8f4faac145da1"
dependencies = [
 "git-ref-format-core",
 "git-ref-format-macro",
@@ -801,9 +807,9 @@ dependencies = [

[[package]]
name = "git-ref-format-core"
-
version = "0.3.0"
+
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "ebb6549ddc63ba5722acb98c823b0eccb7f8b979407bd2a8fd616f581ae50982"
+
checksum = "bbaeb9672a55e9e32cb6d3ef781e7526b25ab97d499fae71615649340b143424"
dependencies = [
 "bstr",
 "serde",
@@ -812,14 +818,14 @@ dependencies = [

[[package]]
name = "git-ref-format-macro"
-
version = "0.3.0"
+
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "18ffd0101a3bd9a3aba39602b8b20751ddb7ee11596debb58be3074209dad2ae"
+
checksum = "3b6ca5353accc201f6324dff744ba4660099546d4daf187ba868f07562e36ca4"
dependencies = [
 "git-ref-format-core",
 "proc-macro-error",
 "quote",
-
 "syn 1.0.109",
+
 "syn 2.0.89",
]

[[package]]
@@ -2188,6 +2194,7 @@ dependencies = [
 "crossbeam-channel",
 "cyphernet",
 "emojis",
+
 "fast-glob",
 "fastrand",
 "git2",
 "libc",
@@ -2351,9 +2358,9 @@ dependencies = [

[[package]]
name = "radicle-git-ext"
-
version = "0.8.0"
+
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "16d2e8a6292811e44388e6068fcaf1040401e1f6a7a58cf48cc121cf7453c19d"
+
checksum = "4b78c26e67d1712ad5a0c602ae3b236609461372ac04e200bda359fe4a1c6650"
dependencies = [
 "git-ref-format",
 "git2",
modified rad-id.1.adoc
@@ -18,7 +18,8 @@ rad-id - Manage changes to a Radicle repository's identity.
*rad id* _edit_ <revision-id> [--title <string>] [--description <string>] [<option>...] +
*rad id* _show_ <revision-id> [<option>...] +
*rad id* _accept_ | _reject_ <revision-id> [<option>...] +
-
*rad id* _redact_ <revision-id> [<option>...]
+
*rad id* _redact_ <revision-id> [<option>...] +
+
*rad id* _migrate_ [--from <version>] [<option>...]

== Description

@@ -89,6 +90,27 @@ automatically accepted and included into the identity document.
*--edit*::
  Opens your $EDITOR to edit the JSON contents directly.

+
=== migrate
+

+
Propose a migration revision to the identity document. The migration will
+
automatically convert the current identity document into the latest version,
+
if possible.
+

+
If a title and description are not provided on the command line, you will be
+
prompted to enter one via your text editor.
+

+
Note that if you are the repository's only delegate, the migration changes will
+
be automatically accepted and included into the identity document.
+

+
*--title* _<string>_::
+
  Set the title for the new revision.
+

+
*--description* _<string>_::
+
  Set the description for the new revision.
+

+
*--no-confirm*::
+
  Don't ask for confirmation before creating the revision.
+

=== edit

Edit an existing revision to the identity document. The revision must still be
modified rad-patch.1.adoc
@@ -278,7 +278,7 @@ example, if some patch *26e3e56* is ready to merge, the steps would be:
    $ git merge patch/26e3e56
    $ git push rad
    ✓ Patch 26e3e563ddc7df8dd0c9f81274c0b3cb1b764568 merged
-
    To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
    To rad://z3oVNYXUagaa4L4HqKzTDppHBA1Jo/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
       f2de534..d6399c7  master -> master

In the above, we created a checkout for the patch, and merged that branch into
modified radicle-cli/Cargo.toml
@@ -15,15 +15,15 @@ path = "src/main.rs"
[dependencies]
anyhow = { version = "1" }
chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"] }
-
git-ref-format = { version = "0.3.0", features = ["macro"] }
+
git-ref-format = { version = "0.3.1", features = ["macro"] }
lexopt = { version = "0.3.0" }
localtime = { version = "1.2.0" }
log = { version = "0.4", features = ["std"] }
nonempty = { version = "0.9.0" }
# N.b. this is required to use macros, even though it's re-exported
# through radicle
-
radicle-git-ext = { version = "0.8.0", features = ["serde"] }
-
radicle-surf = { version = "0.22.0" }
+
radicle-git-ext = { version = "0.8", features = ["serde"] }
+
radicle-surf = { version = "0.22" }
serde = { version = "1.0" }
serde_json = { version = "1" }
shlex = { version = "1.1.0" }
modified radicle-cli/examples/git/git-fetch.md
@@ -13,7 +13,7 @@ f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master

``` (stderr)
$ git fetch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new branch]      alice/1    -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
```

modified radicle-cli/examples/git/git-push-amend.md
@@ -1,11 +1,11 @@
``` ~alice
-
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -q
-
c036c0d89ce26aef3ad7da402157dba16b5163b4
+
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 -q
+
f48a2c516aceccde576d9ba8845b21eca1f7902c
```

``` ~bob
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

@@ -20,8 +20,8 @@ $ git commit --amend -m "Neue Änderungen" --allow-empty -q

``` ~alice (stderr)
$ git push rad master -f
-
✓ Canonical head updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
+
✓ Canonical head for refs/heads/master updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + fb25886...9170c87 master -> master (forced update)
```
added radicle-cli/examples/git/git-push-canonical-tags.md
@@ -0,0 +1,174 @@
+
In this example, we will show how we can make other references become canonical.
+
To illustrate, we will use Git tags as an example. The storage of the repository
+
should look something like this by the end of the example:
+

+
~~~
+
storage/z6cFWeWpnZNHh9rUW8phgA3b5yGt/refs
+
├── heads
+
│   └── main
+
├── namespaces
+
│   ├── z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
│   │   └── refs
+
│   │       ├── cobs
+
│   │       │   └── xyz.radicle.id
+
│   │       │       └── 865c48204bd7bb7f088b8db90ffdccb48cfa0a50
+
│   │       ├── heads
+
│   │       │   └── master
+
│   │       ├── tags
+
│   │       │   ├── v1.0-hotfix
+
│   │       │   └── v1.0
+
│   │       └── rad
+
│   │           ├── id
+
│   │           └── sigrefs
+
│   └── z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
│       └── refs
+
│           ├── heads
+
│           │   └── master
+
│           ├── tags
+
│           │   ├── v1.0-hotfix
+
│           │   └── v1.0
+
│           └── rad
+
│               ├── id
+
│               └── sigrefs
+
├── rad
+
│   └── id
+
└── tags
+
    ├── v1.0-hotfix
+
    └── v1.0
+
~~~
+

+
Noting that there are tags under `refs/tags` now.
+

+
To start, Alice will add a new payload to the repository identity. The identifier
+
for this payload is `xyz.radicle.project.canonicalReferences`. It contains a
+
single field with the key `rules`, and the value for this key is an array of
+
rules. In this case, we will have two rules: one for `refs/tags/*` and one for
+
`refs/tags/qa/*` (see RIP-0004 for more information on the rules).
+

+
``` ~alice
+
$ rad cref add refs/tags/* --threshold 1 --title "Add rule for refs/tags/*"
+
✓ Rule for refs/tags/* has been added
+
✓ Identity revision f4eda597611ec04ccf8bb3f18ddede4801a8441a created
+
$ rad cref add refs/tags/qa/* --threshold 1 --title "Add rule for refs/tags/qa/*"
+
✓ Rule for refs/tags/qa/* has been added
+
✓ Identity revision 4b02f0d2040de5970eab9b1889321387f379ef5c created
+
```
+

+
Now, Alice will create a tag and push it:
+

+
``` ~alice
+
$ git tag v1.0-hotfix
+
```
+

+
``` ~alice (stderr)
+
$ git push rad --tags
+
✓ Canonical head for refs/tags/v1.0-hotfix updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
✓ Synced with 1 node(s)
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 * [new tag]         v1.0-hotfix -> v1.0-hotfix
+
```
+

+
Notice that the output included a message about a canonical reference being
+
updated:
+

+
~~~
+
✓ Canonical reference refs/tags/v1.0-hotfix updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
~~~
+

+
On the other side, Bob performs a fetch and now has the tags locally:
+

+
``` ~bob (stderr)
+
$ cd heartwood
+
$ git fetch rad
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
 * [new tag]         v1.0-hotfix -> rad/tags/v1.0-hotfix
+
 * [new tag]         v1.0-hotfix -> v1.0-hotfix
+
```
+

+
In the next portion of this example, we want to show that using a `threshold` of
+
`2` requires both delegates. To do this, Bob creates a `master` reference, Alice
+
adds him as a remote, and adds him to the identity delegates, as well as setting
+
the `threshold` to `2` for the `refs/tags/*` rule:
+

+
``` ~bob
+
$ rad remote add z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --name alice
+
✓ Follow policy updated for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
+
✓ Remote alice added
+
✓ Remote-tracking branch alice/master created for z6MknSL…StBU8Vi
+
$ git push rad master
+
```
+

+
``` ~alice
+
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
+
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
+
✓ Remote bob added
+
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
+
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
+
f0fd3b0c65ec38e52d578df94f96a0f57ac27d65
+
$ rad cref edit refs/tags/* --threshold 2 --title "Change threshold for refs/tags to 2"
+
✓ Rule for refs/tags/* has been modified
+
✓ Identity revision ef105be657f3f112d0a3cfaafdf7362bc2df786a created
+
```
+

+
``` ~bob
+
$ rad sync -f
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
+
✓ Fetched repository from 1 seed(s)
+
$ rad id accept ef105be657f3f112d0a3cfaafdf7362bc2df786a -q
+
```
+

+
When Bob creates a new tag and pushes it, we see that there's a warning that
+
no quorum was found for the new tag:
+

+
``` ~bob (stderr)
+
$ git tag v2.0
+
$ git push rad --tags
+
warn: could not determine tip for canonical reference 'refs/tags/v2.0', no commit with at least 2 vote(s) found (threshold not met)
+
warn: it is recommended to find a commit to agree upon
+
✓ Synced with 1 node(s)
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
 * [new tag]         v1.0-hotfix -> v1.0-hotfix
+
 * [new tag]         v2.0 -> v2.0
+
```
+

+
Alice can then fetch and checkout the new tag, create one on her side, and push
+
it:
+

+
``` ~alice (stderr)
+
$ git fetch bob
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
 * [new tag]         v1.0-hotfix -> bob/tags/v1.0-hotfix
+
 * [new tag]         v2.0        -> bob/tags/v2.0
+
```
+

+
``` ~alice
+
$ git checkout bob/tags/v2.0
+
$ git tag v2.0
+
```
+

+
``` ~alice (stderr)
+
$ git push rad --tags
+
✓ Canonical head for refs/tags/v2.0 updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
✓ Synced with 1 node(s)
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 * [new tag]         v2.0 -> v2.0
+
```
+

+
Now that Bob has also pushed this tag, we can see that the tag was made
+
canonical.
+

+
For the final portion of the example, we will show that both delegates aren't
+
required for pushing tags that match the rule `refs/tags/qa/*`. To show this,
+
Bob will create a tag and push it, and we should see that the canonical
+
reference is created:
+

+
``` ~bob (stderr)
+
$ git tag qa/v2.1
+
$ git push rad --tags
+
✓ Canonical head for refs/tags/qa/v2.1 updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
✓ Synced with 1 node(s)
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
 * [new tag]         qa/v2.1 -> qa/v2.1
+
```
modified radicle-cli/examples/git/git-push-converge.md
@@ -5,8 +5,11 @@ First we add our new delegates, Bob & Eve, to our repo, while also setting the
`threshold` to `3`:

``` ~alice
-
$ rad id update --title "Add Bob & Eve" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --threshold 3 --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -q
-
3143236b2e40338f5574ec04e935a5ab80a6868a
+
$ rad id update --title "Add Bob & Eve" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 -q
+
7dab9524869f5f25abc3d9f2e7a7f975872a24c5
+
$ rad cref edit refs/heads/master --threshold 3 --title "Set threshold for refs/heads/master to 3"
+
✓ Rule for refs/heads/master has been modified
+
✓ Identity revision 3b03a364f20c36c8e5d35599fc53e44fb1950cbf created
```

Bob and Eve will fetch the changes to ensure they hear about their delegate
@@ -14,15 +17,16 @@ responsibilities:

``` ~bob
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 2 seed(s)
+
$ rad id accept 3b03a364f20c36c8e5d35599fc53e44fb1950cbf -q
```

``` ~eve
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 2 seed(s)
```

@@ -58,14 +62,14 @@ found` error is showing up:
``` ~alice
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
$ rad remote add did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --name eve
✓ Follow policy updated for z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z (eve)
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Remote eve added
✓ Remote-tracking branch eve/master created for z6Mkux1…nVhib7Z
```
@@ -89,19 +93,18 @@ commit:

``` ~alice (stderr)
$ git push rad -f
-
warn: could not determine canonical tip for `refs/heads/master`
-
warn: no commit found with at least 3 vote(s) (threshold not met)
+
warn: could not determine tip for canonical reference 'refs/heads/master', no commit with at least 3 vote(s) found (threshold not met)
warn: it is recommended to find a commit to agree upon
✓ Synced with 2 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   d09e634..0f9bd80  master -> master
```

``` ~bob
$ rad remote add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --name alice
✓ Follow policy updated for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Remote alice added
✓ Remote-tracking branch alice/master created for z6MknSL…StBU8Vi
$ git reset --hard alice/master
@@ -115,9 +118,9 @@ become the canonical `master`.

``` ~bob (stderr)
$ git push rad
-
✓ Canonical head updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
+
✓ Canonical head for refs/heads/master updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001
✓ Synced with 2 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
   2a37862..0f9bd80  master -> master
```

@@ -126,8 +129,8 @@ Once Eve also resets to the merge commits, the canonical `master` is set to this
``` ~eve
$ rad remote add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --name alice
✓ Follow policy updated for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Remote alice added
✓ Remote-tracking branch alice/master created for z6MknSL…StBU8Vi
$ git reset --hard alice/master
@@ -136,8 +139,8 @@ HEAD is now at 0f9bd80 Merge remote-tracking branch 'eve/master'

``` ~eve (stderr)
$ git push rad
-
✓ Canonical head updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
+
✓ Canonical head for refs/heads/master updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b
✓ Synced with 2 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
   3a75f66..0f9bd80  master -> master
```
modified radicle-cli/examples/git/git-push-delete.md
@@ -1,7 +1,7 @@
Finally, we can also delete branches with `git push`:

```
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/*
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/*
145e1e69bef3ad93d14946ea212249c2fa9b9828	refs/heads/alice/1
ddcc1f164eacfd7dba41da9bff3261da3ee79fd3	refs/heads/alice/2
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	refs/heads/master
@@ -9,12 +9,12 @@ f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master

``` (stderr) RAD_SOCKET=/dev/null
$ git push rad :alice/1
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 - [deleted]         alice/1
```

```
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/*
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/*
ddcc1f164eacfd7dba41da9bff3261da3ee79fd3	refs/heads/alice/2
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	refs/heads/master
```
@@ -23,13 +23,13 @@ f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
$ git checkout alice/2
Switched to a new branch 'alice/2'
$ git push rad HEAD:refs/patches
-
✓ Patch bb9b0d5b8de8d5e2a4cba45f02bd35b3e2678fbe opened
+
✓ Patch 799833562f9fff5ba32cb699141ca8a162e6bdf7 opened
To [..]
 * [new reference]   HEAD -> refs/patches
```

``` (stderr) RAD_SOCKET=/dev/null
$ git push rad alice/2 -d
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 - [deleted]         alice/2
```
modified radicle-cli/examples/git/git-push-diverge.md
@@ -5,15 +5,15 @@ canonical head.
First we add a second delegate, Bob, to our repo:

``` ~alice
-
$ rad id update --title "Add Bob" --description "" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -q
-
c036c0d89ce26aef3ad7da402157dba16b5163b4
+
$ rad id update --title "Add Bob" --description "" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 -q
+
f48a2c516aceccde576d9ba8845b21eca1f7902c
```

Then, as Bob, we commit some code on top of the canonical head:

``` ~bob
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
$ rad inspect --delegates
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
@@ -43,10 +43,10 @@ integrate Bob's changes before pushing ours:

``` ~alice (stderr) (fail) RAD_HINT=1
$ git push rad
-
hint: you are attempting to push a commit that would cause your upstream to diverge from the canonical head
+
hint: you are attempting to push a commit that would cause your upstream to diverge from the canonical reference refs/heads/master
hint: to integrate the remote changes, run `git pull --rebase` and try again
error: refusing to update branch to commit that is not a descendant of canonical head
-
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
+
error: failed to push some refs to 'rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
```

We do that, and notice that we're now able to push our code:
@@ -61,8 +61,8 @@ f2de534 Second commit
```
``` ~alice RAD_SOCKET=/dev/null (stderr)
$ git push rad
-
✓ Canonical head updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Canonical head for refs/heads/master updated to f6cff86594495e9beccfeda7c20173e55c1dd9fc
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..f6cff86  master -> master
```

@@ -74,7 +74,7 @@ $ git reset --hard HEAD^ -q
```
``` ~alice RAD_SOCKET=/dev/null (stderr)
$ git push -f
-
✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Canonical head for refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + f6cff86...319a7dc master -> master (forced update)
```
modified radicle-cli/examples/git/git-push-rollback.md
@@ -4,16 +4,20 @@ First we add a second delegate, Bob, to our repo. We also change the threshold
to 2:

``` ~alice
-
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --threshold 2 -q
-
069e7d58faa9a7473d27f5510d676af33282796f
+
$ rad id update --title "Add Bob" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 -q
+
f48a2c516aceccde576d9ba8845b21eca1f7902c
+
$ rad cref edit refs/heads/master --threshold 2 --title "Set default branch threshold to 2"
+
✓ Rule for refs/heads/master has been modified
+
✓ Identity revision 33a0e4502bd6325b69d65a831e14bd12dddd9358 created
```

Bob then syncs these changes and adds a new commit:

``` ~bob
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
+
$ rad id accept 33a0e4502bd6325b69d65a831e14bd12dddd9358 -q
$ git commit -m "Third commit" --allow-empty -q
$ git push rad
$ git branch -arv
@@ -34,9 +38,9 @@ Fast-forward

``` ~alice (stderr)
$ git push rad
-
✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
+
✓ Canonical head for refs/heads/master updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..319a7dc  master -> master
```

@@ -53,8 +57,8 @@ push and the new canonical head becomes the previous commit again:

``` ~alice (stderr)
$ git push rad -f
-
✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
✓ Canonical head for refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + 319a7dc...f2de534 master -> master (forced update)
```
modified radicle-cli/examples/git/git-push.md
@@ -6,7 +6,7 @@ $ git commit -m "Alice's commit" --allow-empty -s

``` (stderr) RAD_SOCKET=/dev/null
$ git push rad HEAD:alice/1
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new branch]      HEAD -> alice/1
```

@@ -17,9 +17,9 @@ $ git commit --amend -m "Alice's amended commit" --allow-empty -s
```
``` (stderr) (fail)
$ git push rad HEAD:alice/1
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 ! [rejected]        HEAD -> alice/1 (non-fast-forward)
-
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
+
error: failed to push some refs to 'rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
hint: [..]
hint: [..]
hint: [..]
@@ -30,7 +30,7 @@ And that we can with `+`:

``` (stderr)
$ git push -o no-sync rad +HEAD:alice/1
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + 87fa120...145e1e6 HEAD -> alice/1 (forced update)
```

@@ -45,7 +45,7 @@ $ git branch -r -vv
List our namespaced refs:

```
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
145e1e69bef3ad93d14946ea212249c2fa9b9828	refs/heads/alice/1
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	refs/heads/master
```
@@ -67,7 +67,7 @@ Note that it is forbidden to delete the default/canonical branch:
``` (fail) (stderr)
$ git push rad :master
error: refusing to delete default branch ref 'refs/heads/master'
-
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
+
error: failed to push some refs to 'rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
```

If you pass an unsupported push option, you get an error:
@@ -85,6 +85,6 @@ $ git commit -m "Something good" --allow-empty -s
```
``` (stderr)
$ git push -o no-sync rad ddcc1f164eacfd7dba41da9bff3261da3ee79fd3:refs/heads/alice/2
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new branch]      ddcc1f164eacfd7dba41da9bff3261da3ee79fd3 -> alice/2
```
modified radicle-cli/examples/git/git-tag.md
@@ -13,7 +13,7 @@ $ git tag v1.0 -a -m "Release v1.0"
``` ~alice (stderr)
$ git push rad v1.0
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new tag]         v1.0 -> v1.0
```

@@ -37,7 +37,7 @@ Bob fetches the tag from Alice, by adding her as a remote:
$ cd heartwood
$ rad remote add z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --name alice
✓ Follow policy updated for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Remote alice added
✓ Remote-tracking branch alice/master created for z6MknSL…StBU8Vi
```
@@ -47,7 +47,7 @@ under the `alice` remote:

``` ~bob (stderr)
$ git fetch alice
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new tag]         v1.0       -> alice/tags/v1.0
```

@@ -64,7 +64,7 @@ Updated tag 'v1.0' (was be18ed6)
``` ~alice (stderr)
$ git push rad v1.0 -f
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + be18ed6...9dbdebc v1.0 -> v1.0 (forced update)
```

@@ -73,12 +73,12 @@ update of the tag:

``` ~bob
$ rad sync -f
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

``` ~bob (stderr)
$ git fetch alice -f
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   62d19fd..9dbdebc  v1.0       -> alice/tags/v1.0
```
modified radicle-cli/examples/rad-block.md
@@ -4,8 +4,8 @@ repositories from being seeded.
For instance, if our default policy is to seed, any unknown repository will
have its policy set to allow seeding:
```
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --policy
-
Repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji is being seeded with scope `all`
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --policy
+
Repository rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 is being seeded with scope `all`
```

Since there is no policy specific to this repository, there's nothing to be
@@ -20,15 +20,15 @@ But if we wanted to prevent this repository from being seeded, while
allowing all other repositories, we could use `rad block`:

```
-
$ rad block rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji set to 'block'
+
$ rad block rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Policy for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 set to 'block'
```

We can see that it is now no longer seeded:

```
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --policy
-
Repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji is not being seeded
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --policy
+
Repository rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 is not being seeded
```

And a 'block' policy was added:
@@ -38,15 +38,15 @@ $ rad seed
╭───────────────────────────────────────────────────────────╮
│ Repository                          Name   Policy   Scope │
├───────────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji          block    all   │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2          block    all   │
╰───────────────────────────────────────────────────────────╯
```

If we want to reverse the blocking of the RID we can use `rad unblock`:

```
-
$ rad unblock rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ The 'block' policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji is removed
+
$ rad unblock rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ The 'block' policy for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 is removed
```

```
modified radicle-cli/examples/rad-checkout-repo-config-linux.md
@@ -14,10 +14,10 @@ $ cat .git/config
[push]
	default = upstream
[remote "rad"]
-
	url = rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
	url = rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
	fetch = +refs/heads/*:refs/remotes/rad/*
	fetch = +refs/tags/*:refs/remotes/rad/tags/*
-
	pushurl = rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
	pushurl = rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
[branch "master"]
	remote = rad
	merge = refs/heads/master
modified radicle-cli/examples/rad-checkout-repo-config-macos.md
@@ -16,10 +16,10 @@ $ cat .git/config
[push]
	default = upstream
[remote "rad"]
-
	url = rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
	url = rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
	fetch = +refs/heads/*:refs/remotes/rad/*
	fetch = +refs/tags/*:refs/remotes/rad/tags/*
-
	pushurl = rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
	pushurl = rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
[branch "master"]
	remote = rad
	merge = refs/heads/master
modified radicle-cli/examples/rad-checkout.md
@@ -2,7 +2,7 @@ With the `rad checkout` command, you can create a new working copy from an
existing project.

```
-
$ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad checkout rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
✓ Repository checkout successful under ./heartwood
```

@@ -32,8 +32,8 @@ Check the remote configuration:

```
$ git remote --verbose
-
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji (fetch)
-
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
+
rad	rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 (fetch)
+
rad	rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
```

List the branches:
modified radicle-cli/examples/rad-clean.md
@@ -10,7 +10,7 @@ $ rad ls
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -18,23 +18,23 @@ Let's also inspect what remotes are in the repository:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 99c549702e2bcfe02b0e68d4a2224fb7a1524529
-
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk e9f48ef90fe8592e1b1c95f96c21a59ca1495300
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7c1445cd018b1b0f51e0d815c3c03b289140eafa
+
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk 2694db11d1ce3fb21f4cee6840f7daa6846366b2
```

Now let's clean the `heartwood` project:

``` ~alice
-
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
+
$ rad clean rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --no-confirm
Removed z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
✓ Successfully cleaned rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✓ Successfully cleaned rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

Inspecting the remotes again, we see that Bob is now gone:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 99c549702e2bcfe02b0e68d4a2224fb7a1524529
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7c1445cd018b1b0f51e0d815c3c03b289140eafa
```

Note that Bob will be fetched again if we do not untrack his
@@ -45,23 +45,23 @@ Cleaning a repository again will remove no remotes, since we're
already at the minimal set of remotes:

``` ~alice
-
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
-
✓ Successfully cleaned rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad clean rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --no-confirm
+
✓ Successfully cleaned rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

Since Eve did not fork the repository, and has no refs of their own,
when they run `rad clean` it will remove the project entirely:

``` ~eve
-
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
+
$ rad clean rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --no-confirm
Removed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
✓ Successfully cleaned rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✓ Successfully cleaned rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

And attempting to clean the repository again, or any non-existent
repository, has no effect on the storage at all:

``` ~eve (fail)
-
$ rad clean rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
-
✗ Error: repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji was not found
+
$ rad clean rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --no-confirm
+
✗ Error: repository rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 was not found
```
modified radicle-cli/examples/rad-clone-all.md
@@ -1,7 +1,7 @@
```
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope all
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope all
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'all'
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -63,7 +63,7 @@ We can also create our own fork just by pushing:

``` (stderr)
$ git push -o no-sync rad master
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
 * [new branch]      master -> master
```
```
modified radicle-cli/examples/rad-clone-connect.md
@@ -2,12 +2,12 @@ If we're not connecting to seed nodes when cloning, the `clone` command will
automatically connect to the necessary seeds.

```
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'all'
✓ Connecting to z6Mkt67…v4N1tRk@[..]
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Connecting to z6MknSL…StBU8Vi@[..]
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
modified radicle-cli/examples/rad-clone-directory.md
@@ -2,9 +2,9 @@ We can specify where a repository gets cloned into on our filesystem
by specifying the directory in the `rad clone` command:

```
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed Developer/Radicle
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope followed Developer/Radicle
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'followed'
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Creating checkout in ./Developer/Radicle..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -21,7 +21,7 @@ Note that attempting to clone into a directory that already exists,
and is not empty, will fail:

``` (fail)
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed Developer/Radicle
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope followed Developer/Radicle
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✗ Error: the directory path "Developer/Radicle" already exists
```
modified radicle-cli/examples/rad-clone-partial-fail.md
@@ -5,19 +5,19 @@ $ rad node routing
╭─────────────────────────────────────────────────────╮
│ RID                                 NID             │
├─────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   z6MknSL…StBU8Vi │
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   z6MksFq…bS9wzpT │
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   z6Mkt67…v4N1tRk │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   z6MknSL…StBU8Vi │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   z6MksFq…bS9wzpT │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   z6Mkt67…v4N1tRk │
╰─────────────────────────────────────────────────────╯
```
When she tries to clone, one of those will fail to fetch. But the clone command
still returns successfully.

```
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --timeout 3
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
-
✗ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..].. error: failed to perform fetch handshake
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --timeout 3
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'all'
+
✗ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..].. error: failed to perform fetch handshake
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✗ Connecting to z6MksFq…bS9wzpT@[..].. error: [..]
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
modified radicle-cli/examples/rad-clone.md
@@ -2,9 +2,9 @@ To create a local copy of a repository on the radicle network, we use the
`clone` command, followed by the identifier or *RID* of the repository:

```
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope followed
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'followed'
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -46,10 +46,10 @@ We can also take a look at the remotes:

```
$ git remote -v
-
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
-
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
-
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji (fetch)
-
rad	rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (push)
+
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
+
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi	rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
+
rad	rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 (fetch)
+
rad	rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (push)
```

Let's check the last commit!
@@ -69,6 +69,6 @@ $ rad ls --seeded
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-cob-log.md
@@ -6,7 +6,7 @@ First create an issue.
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
-
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
+
│ Issue   0d18c610be2fbb4f47d45434c581f3bf0b0ff071        │
│ Author  alice (you)                                     │
│ Status  open                                            │
│                                                         │
@@ -21,7 +21,7 @@ $ rad issue list
╭──────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title                         Author           Labels   Assignees   Opened │
├──────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   alice    (you)                        now    │
+
│ ●   0d18c61   flux capacitor underpowered   alice    (you)                        now    │
╰──────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -45,25 +45,25 @@ $ rad patch
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
+
│ ●  c90967c  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

Both issue and patch COBs can be listed.

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

We can look at the issue COB.

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

@@ -82,9 +82,9 @@ date Thu, 15 Dec 2022 17:28:04 +0000
We can look at the patch COB too.

```
-
$ rad cob log --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object aa45913e757cacd46972733bddee5472c78fa32a
-
commit   aa45913e757cacd46972733bddee5472c78fa32a
-
resource 0656c217f917c3e06234771e9ecae53aba5e173e
+
$ rad cob log --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.patch --object c90967c43719b916e0b5a8b5dafe353608f8a08a
+
commit   c90967c43719b916e0b5a8b5dafe353608f8a08a
+
resource eeb8b44890570ccf85db7f3cb2a475100a27408a
rel      3e674d1a1df90807e934f9ae5da2591dd6848a33
rel      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
@@ -108,11 +108,11 @@ date Thu, 15 Dec 2022 17:28:04 +0000
Finally let's updated the issue and see the `parent` header:

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

@@ -123,8 +123,8 @@ date Thu, 15 Dec 2022 17:28:04 +0000
      "type": "label"
    }

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

modified radicle-cli/examples/rad-cob-multiset.md
@@ -41,28 +41,28 @@ It reads the current state of the grocery list and operations containing actions
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
+
$ rad cob create --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --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
+
$ rad cob show --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --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
+
$ rad cob update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --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
+
$ rad cob show --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type com.example.multiset --object 9bba8e6f83ef56b11151ef6ad02cc4595f982aab
{"jelly":0,"peanut butter":2,"salad":4}
```

modified radicle-cli/examples/rad-cob-show.md
@@ -9,7 +9,7 @@ First create an issue.
$ rad issue open --title "spice harvester broken" --description "Fremen have attacked, maybe we went too far?" --no-announce
╭──────────────────────────────────────────────────╮
│ Title   spice harvester broken                   │
-
│ Issue   9de644864342d7a505eb8d58d1ef20e5bb05de2e │
+
│ Issue   fa09289336f9317e0d2573372f7965cf8861d04e │
│ Author  alice (you)                              │
│ Status  open                                     │
│                                                  │
@@ -24,7 +24,7 @@ $ rad issue list
╭─────────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title                    Author           Labels   Assignees   Opened │
├─────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   9de6448   spice harvester broken   alice    (you)                        now    │
+
│ ●   fa09289   spice harvester broken   alice    (you)                        now    │
╰─────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -48,37 +48,37 @@ $ rad patch
╭───────────────────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title                        Author         Reviews  Head     +   -   Updated │
├───────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  d1f7f86  Start drafting peace treaty  alice   (you)  -        575ed68  +0  -0  now     │
+
│ ●  07f94cd  Start drafting peace treaty  alice   (you)  -        575ed68  +0  -0  now     │
╰───────────────────────────────────────────────────────────────────────────────────────────╯
```

Both issue and patch COBs can be listed.

```
-
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue
-
9de644864342d7a505eb8d58d1ef20e5bb05de2e
-
$ rad cob list --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch
-
d1f7f869fde9fac19c1779c4c2e77e8361333f91
+
$ rad cob list --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.issue
+
fa09289336f9317e0d2573372f7965cf8861d04e
+
$ rad cob list --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.patch
+
07f94cddadbf87eca62a4b175c47b03db3015427
```

We can show the issue COB.

```
-
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object 9de644864342d7a505eb8d58d1ef20e5bb05de2e
-
{"assignees":[],"title":"spice harvester broken","state":{"status":"open"},"labels":[],"thread":{"comments":{"9de644864342d7a505eb8d58d1ef20e5bb05de2e":{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","reactions":[],"resolved":false,"body":"Fremen have attacked, maybe we went too far?","edits":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"Fremen have attacked, maybe we went too far?","embeds":[]}]}},"timeline":["9de644864342d7a505eb8d58d1ef20e5bb05de2e"]}}
+
$ rad cob show --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.issue --object fa09289336f9317e0d2573372f7965cf8861d04e
+
{"assignees":[],"title":"spice harvester broken","state":{"status":"open"},"labels":[],"thread":{"comments":{"fa09289336f9317e0d2573372f7965cf8861d04e":{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","reactions":[],"resolved":false,"body":"Fremen have attacked, maybe we went too far?","edits":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"Fremen have attacked, maybe we went too far?","embeds":[]}]}},"timeline":["fa09289336f9317e0d2573372f7965cf8861d04e"]}}
```

We can show the patch COB too.

```
-
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.patch --object d1f7f869fde9fac19c1779c4c2e77e8361333f91
-
{"title":"Start drafting peace treaty","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"state":{"status":"open"},"target":"delegates","labels":[],"merges":{},"revisions":{"d1f7f869fde9fac19c1779c4c2e77e8361333f91":{"id":"d1f7f869fde9fac19c1779c4c2e77e8361333f91","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"description":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"See details.","embeds":[]}],"base":"f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354","oid":"575ed68c716d6aae81ea6b718fd9ac66a8eae532","discussion":{"comments":{},"timeline":[]},"reviews":{},"timestamp":1671125284000,"resolves":[],"reactions":[]}},"assignees":[],"timeline":["d1f7f869fde9fac19c1779c4c2e77e8361333f91"],"reviews":{}}
+
$ rad cob show --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.patch --object 07f94cddadbf87eca62a4b175c47b03db3015427
+
{"title":"Start drafting peace treaty","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"state":{"status":"open"},"target":"delegates","labels":[],"merges":{},"revisions":{"07f94cddadbf87eca62a4b175c47b03db3015427":{"id":"07f94cddadbf87eca62a4b175c47b03db3015427","author":{"id":"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"},"description":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"See details.","embeds":[]}],"base":"f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354","oid":"575ed68c716d6aae81ea6b718fd9ac66a8eae532","discussion":{"comments":{},"timeline":[]},"reviews":{},"timestamp":1671125284000,"resolves":[],"reactions":[]}},"assignees":[],"timeline":["07f94cddadbf87eca62a4b175c47b03db3015427"],"reviews":{}}
```

Finally let's update the issue and see the output of `rad cob show` also changes.

```
-
$ rad issue label 9de6448 --add bug --no-announce
-
$ rad cob show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --type xyz.radicle.issue --object 9de644864342d7a505eb8d58d1ef20e5bb05de2e
-
{"assignees":[],"title":"spice harvester broken","state":{"status":"open"},"labels":["bug"],"thread":{"comments":{"9de644864342d7a505eb8d58d1ef20e5bb05de2e":{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","reactions":[],"resolved":false,"body":"Fremen have attacked, maybe we went too far?","edits":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"Fremen have attacked, maybe we went too far?","embeds":[]}]}},"timeline":["9de644864342d7a505eb8d58d1ef20e5bb05de2e"]}}
+
$ rad issue label fa09289 --add bug --no-announce
+
$ rad cob show --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.issue --object fa09289336f9317e0d2573372f7965cf8861d04e
+
{"assignees":[],"title":"spice harvester broken","state":{"status":"open"},"labels":["bug"],"thread":{"comments":{"fa09289336f9317e0d2573372f7965cf8861d04e":{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","reactions":[],"resolved":false,"body":"Fremen have attacked, maybe we went too far?","edits":[{"author":"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi","timestamp":1671125284000,"body":"Fremen have attacked, maybe we went too far?","embeds":[]}]}},"timeline":["fa09289336f9317e0d2573372f7965cf8861d04e"]}}
```
modified radicle-cli/examples/rad-cob-update-identity.md
@@ -1,6 +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
+
$ rad cob update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.id --object eeb8b44890570ccf85db7f3cb2a475100a27408a --message "Danger" /dev/null
✗ Error: Update of collaborative objects of type xyz.radicle.id is not supported.
-
```

\ No newline at end of file
+
```
modified radicle-cli/examples/rad-cob-update.md
@@ -12,8 +12,8 @@ $ git commit --message "Add README, just for the fun"

``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun" HEAD:refs/patches
-
✓ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch f699e2299e9ee734758626924df7e15fd9a68553 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -28,22 +28,22 @@ $ git commit -v -m "Define the LICENSE"

``` (stderr)
$ git push -f -o patch.message="Add License"
-
✓ Patch 89f7afb updated to revision 5d78dd5376453e25df5988ec86951c99cb73742c
-
To compare against your previous revision 89f7afb, run:
+
✓ Patch f699e22 updated to revision 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f
+
To compare against your previous revision f699e22, run:

   git range-diff f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 03c02af4b12a593d17a06d38fae50a57fc3c339a 8945f6189adf027892c85ac57f7e9341049c2537

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   03c02af..8945f61  changes -> patches/89f7afb1511b976482b21f6b2f39aef7f4fb88a2
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   03c02af..8945f61  changes -> patches/f699e2299e9ee734758626924df7e15fd9a68553
```

Let's look at the patch, to see what it looks like before editing it:

```
-
$ rad patch show 89f7afb
+
$ rad patch show f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
@@ -54,7 +54,7 @@ $ rad patch show 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -62,11 +62,11 @@ 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
+
$ rad patch edit f699e22 --message "Add Metadata" --message "Add README & LICENSE" --no-announce
+
$ rad patch show f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
@@ -79,14 +79,14 @@ $ rad patch show 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (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"}
+
{"type": "revision.edit", "description": "Add README and LICENSE", "revision": "f699e2299e9ee734758626924df7e15fd9a68553"}
```

We now use `rad cob update` to edit the patch another time, rewriting the description.
@@ -95,12 +95,12 @@ specifying the revision for which the description should be changed, and `descri
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
+
$ rad cob update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.patch --object f699e2299e9ee734758626924df7e15fd9a68553 --message "Edit patch" revision-edit.jsonl
+
8f2a1a4c55f976288574e463dff1e85981e7a427
+
$ rad patch show --verbose f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Base      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354                  │
@@ -114,7 +114,7 @@ $ rad patch show --verbose 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -137,7 +137,7 @@ We prepare the file `revision-create.jsonl` which contains one action.
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
+
$ rad cob update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.patch --object f699e2299e9ee734758626924df7e15fd9a68553 --message "Create new revision" revision.jsonl
✗ Error: store: update error: failed to read 'f1339dd109e538c6b3a7fed3e72403e1b4db08c9' from git odb
```

@@ -153,12 +153,12 @@ $ 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
+
$ rad cob update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --type xyz.radicle.patch --object f699e2299e9ee734758626924df7e15fd9a68553 --message "Create new revision" revision.jsonl
+
c60de85722655bfd01867c180307a9064c7acabd
+
$ rad patch show f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      f1339dd109e538c6b3a7fed3e72403e1b4db08c9                  │
│ Branches  changes                                                   │
@@ -172,7 +172,7 @@ $ rad patch show 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
-
│ ↑ updated to 2da9c025a1d14d93c4f2cec60a7878afbc5e2a3c (f1339dd) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (8945f61) now │
+
│ ↑ updated to c60de85722655bfd01867c180307a9064c7acabd (f1339dd) now │
╰─────────────────────────────────────────────────────────────────────╯
-
```

\ No newline at end of file
+
```
added radicle-cli/examples/rad-cref.md
@@ -0,0 +1,181 @@
+
The `cref` command helps manage canonical reference rules associated
+
with a Radicle repository.
+

+
We can list the rules by calling `rad cref` (the `list` subcommand is
+
the default):
+

+
```
+
$ rad cref
+
{
+
  "refs/heads/master": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  }
+
}
+
```
+

+
Here we can see that we have a single rule for `refs/heads/master`. Under the
+
`allow` key it's using the `delegates` value, which means that it will use the
+
delegates of the repository identity. The `threshold` is `1`, so a single vote
+
by a delegate is required to make the reference canonical, if there is no
+
divergence.
+

+
So, let's add another rule through the `add` subcommand:
+

+
```
+
$ rad cref add refs/heads/dev --allow did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2 --title "Add canonical reference rule for refs/heads/dev"
+
✓ Rule for refs/heads/dev has been added
+
✓ Identity revision a92caeb23f9df6f2136c583ef6f01cb81fea853f created
+
```
+

+
Notice the last line of the output:
+

+
    ✓ Identity revision a92caeb23f9df6f2136c583ef6f01cb81fea853f created
+

+
Since canonical reference rules are stored in the identity document, it needs to
+
be updated whenever the rules change. In the case above, there is a single
+
delegate so these updates will be accepted automatically. If there were more
+
delegates then they would require a majority to agree to the changes.
+

+
We can also note that we added the DID
+
`did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk` to the rule. This
+
peer is not a delegate of the repository, but their commit state for
+
`refs/heads/dev` is taken into account when computing the canonical reference.
+
This means that we can collaborate with peers that we trust on certain
+
references, but may not want to trust them on all changes made to the project.
+

+
Let's check that our rule was successfully added:
+

+
```
+
$ rad cref
+
{
+
  "refs/heads/dev": {
+
    "allow": [
+
      "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+
      "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
+
    ],
+
    "threshold": 2
+
  },
+
  "refs/heads/master": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  }
+
}
+
```
+

+
The reference passed to the command can also use the `*` path component to match
+
multiple references. The value must follow the format of [git-check-ref-format].
+
So, let's add a rule for `refs/tags/releases/*`:
+

+
```
+
$ rad cref add refs/tags/releases/* --title "Add canonical reference rule for refs/tags/releases/*"
+
✓ Rule for refs/tags/releases/* has been added
+
✓ Identity revision a880b05441f00cc90bc7bae76e0f1ef16b73daf1 created
+
```
+

+
We can test that our rule matches against the reference name `refs/tags/releases/v1.2`
+
as expected:
+

+
```
+
$ rad cref match refs/tags/releases/v1.2
+
{
+
  "refs/tags/releases/*": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  }
+
}
+
```
+

+
Here, we didn't specify the `--allow` or `--threshold` options. This means
+
the defaults of `delegates` and `1` will be used. So let's check that this is
+
true:
+

+
```
+
$ rad cref
+
{
+
  "refs/tags/releases/*": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  },
+
  "refs/heads/dev": {
+
    "allow": [
+
      "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+
      "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
+
    ],
+
    "threshold": 2
+
  },
+
  "refs/heads/master": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  }
+
}
+
```
+

+
We can also remove rules using the `remove` subcommand and specifying the
+
`refspec` we would like to remove:
+

+
```
+
$ rad cref remove refs/heads/dev --title "Remove rule for refs/heads/dev"
+
✓ Rule for refs/heads/dev has been removed
+
✓ Identity revision db01c9c642c6cb9c870269b1f2863a5bed624df0 created
+
```
+

+
Let's check that we only have 2 rules now:
+

+
```
+
$ rad cref
+
{
+
  "refs/tags/releases/*": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  },
+
  "refs/heads/master": {
+
    "allow": "delegates",
+
    "threshold": 1
+
  }
+
}
+
```
+

+
Let's also check that the refname `refs/heads/weird` does not match any rules:
+

+
```
+
$ rad cref match refs/heads/weird
+
{}
+
```
+

+
The `refspec` value must be a fully-qualified reference name. That is, it must
+
start with `refs`, followed by the category (e.g. `refs/tags`), and the suffix.
+
Here we show that it will fail if this is not the case:
+

+
``` (fails)
+
$ rad cref add tags/v1.0 --title "Add tags/v1.0"
+
✗ Error: rad cref: 'tags/v1.0' is a not a qualified refspec string
+
```
+

+
When we try to make changes that do not change the rule set, then we report that
+
nothing happened. Let's try remove a rule that doesn't exist:
+

+
```
+
$ rad cref remove refs/heads/main --title "Remove refs/heads/main"
+
Nothing to do. The rules are up to date. See `rad cref list`.
+
```
+

+
Finally, rules are verified according to [RIP-0004]. In this case we'll add a rule
+
with a single delegate but a threshold of 2:
+

+
``` (fails)
+
$ rad cref add refs/tags/* --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2 --title "Add canonical reference rule for refs/tags/*"
+
✗ Error: invalid threshold `2`: threshold cannot exceed number of delegates
+
```
+

+
Finally, we refuse to add the rule for `refs/rad/id` since this is a special
+
reference:
+

+
``` (fails)
+
$ rad cref add refs/rad/id --title "Try add refs/rad/id"
+
✗ Error: rad cref: cannot create rule for 'refs/rad/id' since references under 'refs/rad' are protected
+
```
+

+
# FIXME: update the link to post directly to the RIP when merged
+
[RIP-0004]: https://app.radicle.xyz/nodes/seed.radicle.xyz/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/patches/1d1ce874f7c39ecdcd8c318bbae46ffd02fe1ea8
+
[git-check-ref-format]: https://git-scm.com/docs/git-check-ref-format
modified radicle-cli/examples/rad-fetch.md
@@ -5,12 +5,12 @@ necessary.

Instead, we want to fetch the project from the network into our local
storage. In this scenario, we know that the project is
-
`rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji`. In order to fetch it, we first
+
`rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2`. In order to fetch it, we first
have to update our seeding policy for the project.

```
-
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-fetch
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
$ rad seed rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --no-fetch
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'all'
```

Now that the project is seeding we can fetch it and we will have it in
@@ -18,8 +18,8 @@ our local storage. Note that the `seed` command can also be told to fetch
by passing the `--fetch` option.

```
-
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad sync --fetch rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

modified radicle-cli/examples/rad-fork.md
@@ -4,7 +4,7 @@ NID. This is demonstrated below where our NID is
`z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk`:

```
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --refs
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --refs
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
    ├── cobs
@@ -22,15 +22,15 @@ To remedy this, we can use the `rad fork` command for the project we
wish to fork:

```
-
$ rad fork rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Forked repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
$ rad fork rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Forked repository rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
```

Now, if we `rad inspect` the project's refs again we will see that we
have a copy of the main set of refs:

```
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --refs
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --refs
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
    ├── cobs
modified radicle-cli/examples/rad-id-collaboration.md
@@ -7,10 +7,10 @@ First, we'll start off with Alice adding Bob. It's necessary for Bob
to have a fork of the project and Alice must be aware of the fork:

``` ~bob
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope followed
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'followed'
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -30,8 +30,8 @@ to the default branch, she must set the `threshold` to `2` when adding
Bob as a delegate:

``` ~alice (fails)
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --description "" --threshold 2 --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
-
✗ Error: failed to verify delegates for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Add Bob" --description "" --threshold 2 --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
+
✗ Error: failed to verify delegates for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
✗ Error: a threshold of 2 delegates cannot be met, found 1 delegate(s) and the following delegates are missing [did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk]
✗ Hint: run `rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk` to follow this missing peer
✗ Hint: run `rad sync -f` to attempt to fetch the newly followed peers
@@ -50,15 +50,15 @@ So, instead Alice needs to first follow Bob and fetch his references:
$ rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --alias bob
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
$ rad sync
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
✓ Fetched repository from 2 seed(s)
✓ Synced with 1 node(s)
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --description "" --threshold 2 --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Add Bob" --description "" --threshold 2 --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
069e7d58faa9a7473d27f5510d676af33282796f
$ rad sync
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
✓ Fetched repository from 2 seed(s)
✓ Synced with 3 node(s)
```
@@ -67,8 +67,8 @@ Bob can confirm that he was made a delegate by fetching the update:

``` ~bob
$ rad sync
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
✓ Fetched repository from 2 seed(s)
✓ Synced with 1 node(s)
$ rad inspect --delegates
@@ -81,10 +81,10 @@ project. For Bob to propose Eve, similar steps need to happen as
between Alice and Bob. Eve first needs to setup a fork:

``` ~eve
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope followed
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'followed'
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -104,11 +104,11 @@ $ git push rad master
Bob then adds Eve as a delegate:

``` ~bob
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Eve" --description "" --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --no-confirm -q
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Add Eve" --description "" --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --no-confirm -q
3cd3c7f9900de0fcb19705856a7cc339a38fb0b3
$ rad sync
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
✓ Fetched repository from 2 seed(s)
✓ Synced with 3 node(s)
```
@@ -122,8 +122,8 @@ the change to meet a quorum of votes (`votes >= (delegates / 2) + 1`):

``` ~alice
$ rad sync
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
✓ Fetched repository from 2 seed(s)
✓ Nothing to announce, already in sync with 3 node(s) (see `rad sync status`)
$ rad id list
@@ -134,7 +134,7 @@ $ rad id list
│ ●   069e7d5   Add Bob            alice    (you)             accepted   now     │
│ ●   0656c21   Initial revision   alice    (you)             accepted   now     │
╰────────────────────────────────────────────────────────────────────────────────╯
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --sigrefs
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --sigrefs
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 6acd3b370839318d96dbfff43948bab2bcdd3681
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk c40018821dc1b41cad75e91e0c9d00827e815324
$ rad id accept 3cd3c7f
@@ -157,11 +157,11 @@ since she has become a delegate:

``` ~alice
$ rad sync --timeout 3
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
✓ Fetched repository from 2 seed(s)
✓ Synced with 3 node(s)
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --sigrefs
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --sigrefs
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 1f716870f890be0c13fdd0af9f527af849fec792
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk c40018821dc1b41cad75e91e0c9d00827e815324
z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z 95cd447c57de8d232c6154f5dba0451aa593520e
@@ -173,8 +173,8 @@ see that both seeds are `synced`:

``` ~eve
$ rad sync
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkvVv…Z1Ct4tD@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MkuPZ…xEuaPUp@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkvVv…Z1Ct4tD@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MkuPZ…xEuaPUp@[..]..
✓ Fetched repository from 2 seed(s)
✓ Synced with 3 node(s)
$ rad sync status
modified radicle-cli/examples/rad-id-conflict.md
@@ -1,13 +1,13 @@
First let's add Bob as a delegate, and sync the changes to Bob:

``` ~alice
-
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2 -q
-
0ca42d376bd566631083c8913cf86bec722da392
+
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
+
ba5c358894e0a58dd0772fd3eb6d070282dffc26
```
``` ~bob
$ cd heartwood
-
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad sync --fetch rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

@@ -16,28 +16,28 @@ time:

``` ~alice
$ rad id update --title "Edit project name" --description "" --payload "xyz.radicle.project" "name" '"heart"' -q
-
12d7300d1bbba84e4e5760c8c61999bf5fefb81a
+
46b0a1a441cd1646395e3cf893b99aa258ed7c63
```
``` ~bob
$ rad id update --title "Edit project name" --description "" --payload "xyz.radicle.project" "name" '"wood"' -q
-
89b2623e7f2ddf5748661b15b9975ab0b4ee17ab
+
8118b11afaf5e43d4446788e0a223ed85e060a56
```

When Alice syncs with Bob, she notices the problem: there are two active
revisions.

``` ~alice
-
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
$ rad sync --fetch rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Fetched repository from 1 seed(s)
$ rad id list
╭─────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title               Author                     Status     Created │
├─────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   89b2623   Edit project name   bob      z6Mkt67…v4N1tRk   active     now     │
-
│ ●   12d7300   Edit project name   alice    (you)             active     now     │
-
│ ●   0ca42d3   Add Bob             alice    (you)             accepted   now     │
-
│ ●   0656c21   Initial revision    alice    (you)             accepted   now     │
+
│ ●   8118b11   Edit project name   bob      z6Mkt67…v4N1tRk   active     now     │
+
│ ●   46b0a1a   Edit project name   alice    (you)             active     now     │
+
│ ●   ba5c358   Add Bob             alice    (you)             accepted   now     │
+
│ ●   eeb8b44   Initial revision    alice    (you)             accepted   now     │
╰─────────────────────────────────────────────────────────────────────────────────╯
```

@@ -45,15 +45,15 @@ This isn't a problem as long as we don't try to accept both. So let's accept
Bob's:

``` ~alice
-
$ rad id accept 89b2623 -q
+
$ rad id accept 8118b11 -q
$ rad id list
╭─────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title               Author                     Status     Created │
├─────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   89b2623   Edit project name   bob      z6Mkt67…v4N1tRk   accepted   now     │
-
│ ●   12d7300   Edit project name   alice    (you)             stale      now     │
-
│ ●   0ca42d3   Add Bob             alice    (you)             accepted   now     │
-
│ ●   0656c21   Initial revision    alice    (you)             accepted   now     │
+
│ ●   8118b11   Edit project name   bob      z6Mkt67…v4N1tRk   accepted   now     │
+
│ ●   46b0a1a   Edit project name   alice    (you)             stale      now     │
+
│ ●   ba5c358   Add Bob             alice    (you)             accepted   now     │
+
│ ●   eeb8b44   Initial revision    alice    (you)             accepted   now     │
╰─────────────────────────────────────────────────────────────────────────────────╯
```

@@ -61,22 +61,22 @@ Doing so voided the other conflicting revision, and it can no longer be
accepted now.

``` ~bob
-
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad sync --fetch rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```
``` ~bob (fail)
-
$ rad id accept 12d7300 -q
+
$ rad id accept 46b0a1a -q
✗ Error: cannot vote on revision that is stale
-
$ rad id reject 12d7300 -q
+
$ rad id reject 46b0a1a -q
✗ Error: cannot vote on revision that is stale
```
``` ~bob
-
$ rad id show 12d7300
+
$ rad id show 46b0a1a
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Edit project name                                             │
-
│ Revision 12d7300d1bbba84e4e5760c8c61999bf5fefb81a                      │
-
│ Blob     e93aa3e3c5c448bacd3537a81daf1437eccd046a                      │
+
│ Revision 46b0a1a441cd1646395e3cf893b99aa258ed7c63                      │
+
│ Blob     9ae843924509f3f617b044923772171287dab49b                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    stale                                                         │
│ Quorum   no                                                            │
@@ -85,8 +85,9 @@ $ rad id show 12d7300
│ ? did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob   (you) │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,14 +1,14 @@
+
@@ -1,22 +1,22 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -99,6 +100,13 @@ $ rad id show 12d7300
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
     "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
   ],
-
   "threshold": 2
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```
modified radicle-cli/examples/rad-id-multi-delegate.md
@@ -1,33 +1,33 @@
``` ~alice
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --description "" --threshold 2 --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
-
069e7d58faa9a7473d27f5510d676af33282796f
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Add Bob" --description "" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm -q
+
f48a2c516aceccde576d9ba8845b21eca1f7902c
```

``` ~bob
-
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t c9a828fc2fb01f893d6e6e9e17b9092dea2b3aba -i 500 --timeout 5000
-
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad watch --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t 6001fa5f08133dcf91029b4fc0b78a59bfd7883a -i 500 --timeout 5000
+
$ rad sync --fetch rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
-
$ rad id --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad id --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
╭────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title              Author                     Status     Created │
├────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   069e7d5   Add Bob            alice    z6MknSL…StBU8Vi   accepted   now     │
-
│ ●   0656c21   Initial revision   alice    z6MknSL…StBU8Vi   accepted   now     │
+
│ ●   f48a2c5   Add Bob            alice    z6MknSL…StBU8Vi   accepted   now     │
+
│ ●   eeb8b44   Initial revision   alice    z6MknSL…StBU8Vi   accepted   now     │
╰────────────────────────────────────────────────────────────────────────────────╯
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --sigrefs
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --sigrefs
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi [..]
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [..]
z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z [..]
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --delegates
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --delegates
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Eve" --description "" --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --no-confirm
-
✓ Identity revision 3cd3c7f9900de0fcb19705856a7cc339a38fb0b3 created
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Add Eve" --description "" --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --no-confirm
+
✓ Identity revision 4e7e2aca58c18add67cf117ad414e61645cc39c0 created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Add Eve                                                       │
-
│ Revision 3cd3c7f9900de0fcb19705856a7cc339a38fb0b3                      │
-
│ Blob     74581605d1f75396c331487a10ca61c4815ed685                      │
+
│ Revision 4e7e2aca58c18add67cf117ad414e61645cc39c0                      │
+
│ Blob     bad2d965c9022797a711cb2031c041ab2e2d729f                      │
│ Author   did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk      │
│ State    active                                                        │
│ Quorum   no                                                            │
@@ -36,8 +36,9 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Eve" --des
│ ? did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice       │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,14 +1,15 @@
+
@@ -1,22 +1,23 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -51,24 +52,31 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Eve" --des
+    "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
+    "did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z"
   ],
-
   "threshold": 2
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

``` ~alice
-
$ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
$ rad sync --fetch rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Fetched repository from 2 seed(s)
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --delegates
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --delegates
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
-
$ rad id accept 3cd3c7f9900de0fcb19705856a7cc339a38fb0b3 --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --no-confirm
-
✓ Revision 3cd3c7f9900de0fcb19705856a7cc339a38fb0b3 accepted
+
$ rad id accept 4e7e2aca58c18add67cf117ad414e61645cc39c0 --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --no-confirm
+
✓ Revision 4e7e2aca58c18add67cf117ad414e61645cc39c0 accepted
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Add Eve                                                       │
-
│ Revision 3cd3c7f9900de0fcb19705856a7cc339a38fb0b3                      │
-
│ Blob     74581605d1f75396c331487a10ca61c4815ed685                      │
+
│ Revision 4e7e2aca58c18add67cf117ad414e61645cc39c0                      │
+
│ Blob     bad2d965c9022797a711cb2031c041ab2e2d729f                      │
│ Author   did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -76,15 +84,15 @@ $ rad id accept 3cd3c7f9900de0fcb19705856a7cc339a38fb0b3 --repo rad:z42hL2jL4XNk
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
│ ✓ did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob         │
╰────────────────────────────────────────────────────────────────────────╯
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --delegates
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --delegates
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (alice)
did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z (eve)
```

``` ~alice
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Make private" --description "" --visibility private --no-confirm -q
-
e6bf10593b78384eb2b281cbb18a605668a6d1f7
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Make private" --description "" --visibility private --no-confirm -q
+
61a7e9b58b27baf26b6f2e198aea3978e5d4444f
```

We can list all revisions:
@@ -94,34 +102,34 @@ $ rad id list
╭────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title              Author                     Status     Created │
├────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   e6bf105   Make private       alice    (you)             active     now     │
-
│ ●   3cd3c7f   Add Eve            bob      z6Mkt67…v4N1tRk   accepted   now     │
-
│ ●   069e7d5   Add Bob            alice    (you)             accepted   now     │
-
│ ●   0656c21   Initial revision   alice    (you)             accepted   now     │
+
│ ●   61a7e9b   Make private       alice    (you)             active     now     │
+
│ ●   4e7e2ac   Add Eve            bob      z6Mkt67…v4N1tRk   accepted   now     │
+
│ ●   f48a2c5   Add Bob            alice    (you)             accepted   now     │
+
│ ●   eeb8b44   Initial revision   alice    (you)             accepted   now     │
╰────────────────────────────────────────────────────────────────────────────────╯
```

Despite being a delegate, Bob can't edit or redact Alice's revision:

``` ~bob (fail)
-
$ rad id redact e6bf10593b78384eb2b281cbb18a605668a6d1f7
+
$ rad id redact 61a7e9b58b27baf26b6f2e198aea3978e5d4444f
[..]
```
``` ~bob (fail)
-
$ rad id edit --title "Boo!" --description "Boo!" e6bf10593b78384eb2b281cbb18a605668a6d1f7
+
$ rad id edit --title "Boo!" --description "Boo!" 61a7e9b58b27baf26b6f2e198aea3978e5d4444f
[..]
```

Alice can edit:

``` ~alice
-
$ rad id edit --title "Make private" --description "Privacy is cool." e6bf10593b78384eb2b281cbb18a605668a6d1f7
-
✓ Revision e6bf10593b78384eb2b281cbb18a605668a6d1f7 edited
-
$ rad id show e6bf10593b78384eb2b281cbb18a605668a6d1f7
+
$ rad id edit --title "Make private" --description "Privacy is cool." 61a7e9b58b27baf26b6f2e198aea3978e5d4444f
+
✓ Revision 61a7e9b58b27baf26b6f2e198aea3978e5d4444f edited
+
$ rad id show 61a7e9b58b27baf26b6f2e198aea3978e5d4444f
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Make private                                                  │
-
│ Revision e6bf10593b78384eb2b281cbb18a605668a6d1f7                      │
-
│ Blob     c533865b2846ca6c5b4436ec6872257293380c3b                      │
+
│ Revision 61a7e9b58b27baf26b6f2e198aea3978e5d4444f                      │
+
│ Blob     94448eb3f8b81fba6d25c287c626625fd3f53d8f                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    active                                                        │
│ Quorum   no                                                            │
@@ -133,8 +141,9 @@ $ rad id show e6bf10593b78384eb2b281cbb18a605668a6d1f7
│ ? did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z eve         │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,15 +1,18 @@
+
@@ -1,23 +1,26 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -147,33 +156,39 @@ $ rad id show e6bf10593b78384eb2b281cbb18a605668a6d1f7
     "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
     "did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z"
   ],
-
-  "threshold": 2
-
+  "threshold": 2,
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
+  },
+  "visibility": {
+    "type": "private"
-
+  }
+
   }
 }
```

And she can redact her revision:

``` ~alice
-
$ rad id redact e6bf10593b78384eb2b281cbb18a605668a6d1f7
-
✓ Revision e6bf10593b78384eb2b281cbb18a605668a6d1f7 redacted
+
$ rad id redact 61a7e9b58b27baf26b6f2e198aea3978e5d4444f
+
✓ Revision 61a7e9b58b27baf26b6f2e198aea3978e5d4444f redacted
```
``` ~alice (fail)
-
$ rad id show e6bf10593b78384eb2b281cbb18a605668a6d1f7
-
✗ Error: revision `e6bf10593b78384eb2b281cbb18a605668a6d1f7` not found
+
$ rad id show 61a7e9b58b27baf26b6f2e198aea3978e5d4444f
+
✗ Error: revision `61a7e9b58b27baf26b6f2e198aea3978e5d4444f` not found
```

Finally, Alice can also propose to remove Bob:
``` ~alice
-
$ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Remove Bob" --description "" --rescind did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm
-
✓ Identity revision 8ba242a80bc1181f41f9ea7a19286038c7948994 created
+
$ rad id update --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --title "Remove Bob" --description "" --rescind did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --no-confirm
+
✓ Identity revision d8a5c75f44ee99bd66b9f7555066715c552b6fd8 created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Remove Bob                                                    │
-
│ Revision 8ba242a80bc1181f41f9ea7a19286038c7948994                      │
-
│ Blob     254d62de237117e7d7b9ceff85c47f5e3b610c1e                      │
+
│ Revision d8a5c75f44ee99bd66b9f7555066715c552b6fd8                      │
+
│ Blob     14913e80b9585dfa545da9feb8ca68a42b5d085e                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    active                                                        │
│ Quorum   no                                                            │
@@ -183,8 +198,9 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Remove Bob" --
│ ? did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z eve         │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,15 +1,14 @@
+
@@ -1,23 +1,22 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -197,6 +213,13 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Remove Bob" --
-    "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
     "did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z"
   ],
-
   "threshold": 2
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```
modified radicle-cli/examples/rad-id-private.md
@@ -9,6 +9,7 @@ $ rad id update --title "Allow Bob & Eve" --allow did:key:z6Mkt67GdsW7715MEfRuP4
...
$ rad inspect --identity
{
+
  "version": 2,
  "payload": {
    "xyz.radicle.project": {
      "defaultBranch": "master",
@@ -19,7 +20,14 @@ $ rad inspect --identity
  "delegates": [
    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
  ],
-
  "threshold": 1,
+
  "canonicalRefs": {
+
    "rules": {
+
      "refs/heads/master": {
+
        "allow": "delegates",
+
        "threshold": 1
+
      }
+
    }
+
  },
  "visibility": {
    "type": "private",
    "allow": [
@@ -37,6 +45,7 @@ $ rad id update --title "Remove allow list" --disallow did:key:z6Mkt67GdsW7715ME
...
$ rad inspect --identity
{
+
  "version": 2,
  "payload": {
    "xyz.radicle.project": {
      "defaultBranch": "master",
@@ -47,7 +56,14 @@ $ rad inspect --identity
  "delegates": [
    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
  ],
-
  "threshold": 1,
+
  "canonicalRefs": {
+
    "rules": {
+
      "refs/heads/master": {
+
        "allow": "delegates",
+
        "threshold": 1
+
      }
+
    }
+
  },
  "visibility": {
    "type": "private"
  }
modified radicle-cli/examples/rad-id-threshold-soft-fork.md
@@ -3,10 +3,10 @@ without having pushed the canonical default branch. For example, Bob can create
an issue in the repository:

``` ~bob
-
$ rad issue open --title "Add Bob as a delegate" --description "We agreed to add me as a delegate, so I am creating an issue to track that work" --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad issue open --title "Add Bob as a delegate" --description "We agreed to add me as a delegate, so I am creating an issue to track that work" --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
╭──────────────────────────────────────────────────────────────╮
│ Title   Add Bob as a delegate                                │
-
│ Issue   f12d512c51d30429f7916db038ae0360e2e938c2             │
+
│ Issue   9c0484f7b773477d41787d9b0cf772c741b7b4e4             │
│ Author  bob (you)                                            │
│ Status  open                                                 │
│                                                              │
@@ -24,7 +24,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
    ├── cobs
    │   └── xyz.radicle.id
-
    │       └── 0656c217f917c3e06234771e9ecae53aba5e173e
+
    │       └── eeb8b44890570ccf85db7f3cb2a475100a27408a
    ├── heads
    │   └── master
    └── rad
@@ -35,7 +35,7 @@ z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
└── refs
    ├── cobs
    │   └── xyz.radicle.issue
-
    │       └── f12d512c51d30429f7916db038ae0360e2e938c2
+
    │       └── 9c0484f7b773477d41787d9b0cf772c741b7b4e4
    └── rad
        ├── root
        └── sigrefs
@@ -46,5 +46,5 @@ as a delegate, since a threshold of 1 can still be reached:

``` ~alice
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
-
7be665f9fccba97abb21b2fa85a6fd3181c72858
+
ba5c358894e0a58dd0772fd3eb6d070282dffc26
```
modified radicle-cli/examples/rad-id-threshold.md
@@ -6,8 +6,8 @@ branch (`refs/heads/<default branch>` at the top-level of the storage)
can be updated.

``` ~alice (fail)
-
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2
-
✗ Error: failed to verify delegates for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
✗ Error: failed to verify delegates for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
✗ Error: the threshold of 2 delegates cannot be met..
✗ Error: the delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk is missing
✗ Hint: run `rad follow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk` to follow this missing peer
@@ -34,6 +34,7 @@ $ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delega

@@ -1,13 +1,14 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -46,7 +47,14 @@ $ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delega
+    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+    "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
   ],
-
   "threshold": 1
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

@@ -65,7 +73,7 @@ $ git commit -v -m "Define power requirements"
``` ~alice (stderr) RAD_SOCKET=/dev/null
$ git push rad master
✓ Canonical head updated to 3e674d1a1df90807e934f9ae5da2591dd6848a33
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..3e674d1  master -> master
```

@@ -87,7 +95,7 @@ $ git commit -v -m "Add README file"
``` ~alice (stderr) RAD_SOCKET=/dev/null
$ git push rad HEAD:refs/patches
✓ Patch b09b2aa0ee055671c811e9ad4ba73eed211ebaa3 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -95,8 +103,8 @@ Any other seeds can also still fetch changes from Alice without any
errors:

``` ~seed
-
$ rad sync rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -f
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
$ rad sync rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 -f
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

@@ -141,7 +149,6 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
    │       └── b09b2aa0ee055671c811e9ad4ba73eed211ebaa3
    └── rad
        ├── id
-
        ├── root
        └── sigrefs
```

@@ -149,7 +156,7 @@ Similarly, she still does not have Bob's `rad/sigrefs`:

``` ~alice
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7ffed605e4871bb0640ee1538181640e239b182c
```

And she can still list the project, without any worries:
@@ -159,7 +166,7 @@ $ rad ls
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       3e674d1   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       3e674d1   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -168,10 +175,10 @@ branch to `refs/heads/master` for this project, she can then use `rad
sync` and fetch his references:

``` ~bob
-
$ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
+
$ rad clone rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'all'
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -187,15 +194,15 @@ Run `cd ./heartwood` to go to the repository directory.
``` ~bob
$ cd heartwood
$ rad fork
-
✓ Forked repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
✓ Forked repository rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
```

``` ~alice
$ rad sync -f
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Fetched repository from 2 seed(s)
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8
-
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk dace6fe948548168a2bb687718949d9b5d9076ee
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7ffed605e4871bb0640ee1538181640e239b182c
+
z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk cc068d93ee77dc134518d7d0fbe55b39804baf53
```
modified radicle-cli/examples/rad-id-unknown-field.md
@@ -3,11 +3,11 @@ added. Here we will add an emoji alias for the heartwood project:

```
$ rad id update --title "Add emoji alias" --description "Adding alias field" --payload xyz.radicle.project alias '"❤️🪵"'
-
✓ Identity revision 05100d3f0a73b9373681677158615a53ba51940e created
+
✓ Identity revision d322c2b471685aa25d8874273b6ba2d70d5702de created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Add emoji alias                                               │
-
│ Revision 05100d3f0a73b9373681677158615a53ba51940e                      │
-
│ Blob     a0f421c928dcfc6eca129fc2ea1f50877de7dc20                      │
+
│ Revision d322c2b471685aa25d8874273b6ba2d70d5702de                      │
+
│ Blob     92e06a7ec3b877ad66c77815ffd5270896d1d898                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -17,8 +17,9 @@ $ rad id update --title "Add emoji alias" --description "Adding alias field" --p
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,13 +1,14 @@
+
@@ -1,21 +1,22 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
+      "alias": "❤️🪵",
@@ -30,7 +31,14 @@ $ rad id update --title "Add emoji alias" --description "Adding alias field" --p
   "delegates": [
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
   ],
-
   "threshold": 1
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

@@ -42,6 +50,6 @@ $ rad ls
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-id-update-delete-field.md
@@ -2,11 +2,11 @@ Let's add a payload field and then delete it.

```
$ rad id update --title "Add field" --description "Add a new 'web' field" --payload xyz.radicle.project web '"https://acme.example"'
-
✓ Identity revision a8a9fee6c4f83578ab132d375f1da0c81282bef3 created
+
✓ Identity revision 4914cd968cd47b7f310946ba6c8e14269ca5a627 created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Add field                                                     │
-
│ Revision a8a9fee6c4f83578ab132d375f1da0c81282bef3                      │
-
│ Blob     fbe268d13e60f1f3a1972e0ccd592f3cdecf08b5                      │
+
│ Revision 4914cd968cd47b7f310946ba6c8e14269ca5a627                      │
+
│ Blob     74b79e158b1fd4ee3998dd541126d9e14a9ae976                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -16,8 +16,9 @@ $ rad id update --title "Add field" --description "Add a new 'web' field" --payl
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,13 +1,14 @@
+
@@ -1,21 +1,22 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -30,7 +31,14 @@ $ rad id update --title "Add field" --description "Add a new 'web' field" --payl
   "delegates": [
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
   ],
-
   "threshold": 1
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

@@ -38,11 +46,11 @@ Now let's delete it by setting it to `null`.

```
$ rad id update --title "Delete field" --description "Delete 'web'" --payload xyz.radicle.project web null
-
✓ Identity revision d373c35876833105f8aafed8b610660b5737cd67 created
+
✓ Identity revision 899c3a31664f6433bd92ccfd7fe02a3382de9e47 created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Delete field                                                  │
-
│ Revision d373c35876833105f8aafed8b610660b5737cd67                      │
-
│ Blob     d96f425412c9f8ad5d9a9a05c9831d0728e2338d                      │
+
│ Revision 899c3a31664f6433bd92ccfd7fe02a3382de9e47                      │
+
│ Blob     b38d81ee99d880461a3b7b3502e5d1556e440ef3                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -52,8 +60,9 @@ $ rad id update --title "Delete field" --description "Delete 'web'" --payload xy
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,14 +1,13 @@
+
@@ -1,22 +1,21 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -66,7 +75,14 @@ $ rad id update --title "Delete field" --description "Delete 'web'" --payload xy
   "delegates": [
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
   ],
-
   "threshold": 1
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

@@ -74,5 +90,5 @@ Note that we cannot delete mandatory fields:

``` (fails)
$ rad id update --title "Delete default branch" --payload xyz.radicle.project defaultBranch null
-
✗ Error: failed to verify `xyz.radicle.project`, missing field `defaultBranch`
+
✗ Error: failed to verify `xyz.radicle.project`: missing field `defaultBranch`
```
modified radicle-cli/examples/rad-id.md
@@ -1,25 +1,21 @@
At some point in the lifetime of a Radicle project you may want to
collaborate with someone else allowing them to become a project
-
maintainer. This requires adding them as a `delegate` and possibly
-
editing the `threshold` for passing new changes to the identity of the
-
project.
+
maintainer. This requires adding them as a `delegate`.

-
For cases where `threshold > 1`, it is necessary to gather a quorum of
-
signatures to update the Radicle identity. To do this, we use the `rad id`
-
command. For now, since we are the only delegate, and `treshold` is `1`, we
-
can update the identity ourselves.
+
For changes made to the identity, a majority of delegate signatures is required.
+
For now, since we are the only delegate, we can update the identity ourselves.

Let's add Bob as a delegate using their DID,
`did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk`, and update the
threshold to `2`.

```
-
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --threshold 2
-
✓ Identity revision 0ca42d376bd566631083c8913cf86bec722da392 created
+
$ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
✓ Identity revision ba5c358894e0a58dd0772fd3eb6d070282dffc26 created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Add Bob                                                       │
-
│ Revision 0ca42d376bd566631083c8913cf86bec722da392                      │
-
│ Blob     053541ba7b90534b35dd8718e0ceaa408979b02b                      │
+
│ Revision ba5c358894e0a58dd0772fd3eb6d070282dffc26                      │
+
│ Blob     8aa049fbaa433f84073983964a54ab909cb2fe9a                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -27,10 +23,12 @@ $ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delega
│ Add Bob as a delegate                                                  │
├────────────────────────────────────────────────────────────────────────┤
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
+
│ ? did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob         │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,13 +1,14 @@
+
@@ -1,21 +1,22 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -43,8 +41,14 @@ $ rad id update --title "Add Bob" --description "Add Bob as a delegate" --delega
+    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+    "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
   ],
-
-  "threshold": 1
-
+  "threshold": 2
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

@@ -76,6 +80,7 @@ can verify that by listing the current identity document:
```
$ rad inspect --identity
{
+
  "version": 2,
  "payload": {
    "xyz.radicle.project": {
      "defaultBranch": "master",
@@ -87,35 +92,42 @@ $ rad inspect --identity
    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
    "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
  ],
-
  "threshold": 2
+
  "canonicalRefs": {
+
    "rules": {
+
      "refs/heads/master": {
+
        "allow": "delegates",
+
        "threshold": 1
+
      }
+
    }
+
  }
}
```

We can also look at the document's COB directly:
```
-
$ rad cob log --object 0656c217f917c3e06234771e9ecae53aba5e173e --type xyz.radicle.id --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
commit   0ca42d376bd566631083c8913cf86bec722da392
-
parent   0656c217f917c3e06234771e9ecae53aba5e173e
+
$ rad cob log --object eeb8b44 --type xyz.radicle.id --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
commit   ba5c358894e0a58dd0772fd3eb6d070282dffc26
+
parent   eeb8b44890570ccf85db7f3cb2a475100a27408a
author   z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
date     Thu, 15 Dec 2022 17:28:04 +0000

    {
-
      "blob": "053541ba7b90534b35dd8718e0ceaa408979b02b",
+
      "blob": "8aa049fbaa433f84073983964a54ab909cb2fe9a",
      "description": "Add Bob as a delegate",
-
      "parent": "0656c217f917c3e06234771e9ecae53aba5e173e",
-
      "signature": "z3AyzixN2eWLtRfQWowtBXwWyRH3iJ8oJ25W6KFYFw5ANLntbzfavge15muNU6AVAUkxSxQvgg9yh2gupbUecavQY",
+
      "parent": "eeb8b44890570ccf85db7f3cb2a475100a27408a",
+
      "signature": "z23hpnKuBai93fnjm6qJeTtPrT7hDeLUJQLmmoE8xbgFrKCUYjYf6ZrgFKZLL8PqhMnNJTJcfmrZcABUzum2SGiju",
      "title": "Add Bob",
      "type": "revision"
    }

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

    {
-
      "blob": "d96f425412c9f8ad5d9a9a05c9831d0728e2338d",
+
      "blob": "b38d81ee99d880461a3b7b3502e5d1556e440ef3",
      "parent": null,
-
      "signature": "z5nGqUvrmfiSyLjNCHWTWYvVMcPUZcvo9TxPKzEKXYBdSgUzbrqf1cYsmpGgbQvYunnsrLSsubEmxZaRdKM4quqQR",
+
      "signature": "z246mVBUXJmr3YYeiTE7yuYteiHvA3bnqUWASB6VBnEbn6JB6eAxLv8mCGvCqaRL4BgVcn1Aho5fnVUqSdhR44SHv",
      "title": "Initial revision",
      "type": "revision"
    }
@@ -125,11 +137,11 @@ date Thu, 15 Dec 2022 17:28:04 +0000
We can use `rad id show` to show the changes of an accepted update:

```
-
$ rad id show 0ca42d376bd566631083c8913cf86bec722da392
+
$ rad id show ba5c358894e0a58dd0772fd3eb6d070282dffc26
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Add Bob                                                       │
-
│ Revision 0ca42d376bd566631083c8913cf86bec722da392                      │
-
│ Blob     053541ba7b90534b35dd8718e0ceaa408979b02b                      │
+
│ Revision ba5c358894e0a58dd0772fd3eb6d070282dffc26                      │
+
│ Blob     8aa049fbaa433f84073983964a54ab909cb2fe9a                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -139,8 +151,9 @@ $ rad id show 0ca42d376bd566631083c8913cf86bec722da392
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,13 +1,14 @@
+
@@ -1,21 +1,22 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -153,8 +166,14 @@ $ rad id show 0ca42d376bd566631083c8913cf86bec722da392
+    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+    "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
   ],
-
-  "threshold": 1
-
+  "threshold": 2
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }
```

@@ -162,15 +181,15 @@ Note that once a revision is accepted, it can't be edited, redacted or otherwise
acted upon:

``` (fail)
-
$ rad id redact 0ca42d376bd566631083c8913cf86bec722da392
+
$ rad id redact ba5c358894e0a58dd0772fd3eb6d070282dffc26
✗ Error: [..]
```
``` (fail)
-
$ rad id reject 0ca42d376bd566631083c8913cf86bec722da392
+
$ rad id reject ba5c358894e0a58dd0772fd3eb6d070282dffc26
✗ Error: [..]
```
``` (fail)
-
$ rad id accept 0ca42d376bd566631083c8913cf86bec722da392
+
$ rad id accept ba5c358894e0a58dd0772fd3eb6d070282dffc26
✗ Error: [..]
```

modified radicle-cli/examples/rad-inbox.md
@@ -125,6 +125,7 @@ $ rad inbox --all
╰──────────────────────────────────────────────────────────────────────╯
$ rad inbox show 1
{
+
  "version": 2,
  "payload": {
    "xyz.radicle.project": {
      "defaultBranch": "master",
@@ -135,6 +136,13 @@ $ rad inbox show 1
  "delegates": [
    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
  ],
-
  "threshold": 1
+
  "canonicalRefs": {
+
    "rules": {
+
      "refs/heads/master": {
+
        "allow": "delegates",
+
        "threshold": 1
+
      }
+
    }
+
  }
}
```
modified radicle-cli/examples/rad-init-existing.md
@@ -14,24 +14,24 @@ $ rad .

Let's pick an existing repository:
```
-
$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad inspect rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

And initialize this working copy as that existing repository:
```
-
$ rad init --existing rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Initialized existing repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji in [..]/heartwood/..
+
$ rad init --existing rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Initialized existing repository rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 in [..]/heartwood/..
```

We can confirm that the working copy is initialized:
```
$ rad .
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
$ git remote show rad
* remote rad
-
  Fetch URL: rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
  Push  URL: rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
  Fetch URL: rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
  Push  URL: rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
  HEAD branch: (unknown)
  Remote branch:
    master new (next fetch will store in remotes/rad)
modified radicle-cli/examples/rad-init-no-seed.md
@@ -6,7 +6,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK.
+
Your Repository ID (RID) is rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg.
You can show it any time by running `rad .` from this directory.

Your repository will be announced to the network when you start your node.
@@ -19,11 +19,11 @@ $ rad node inventory

If we then seed it, it becomes advertized in our inventory:
```
-
$ rad seed rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
-
✓ Inventory updated with rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
-
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
+
$ rad seed rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg
+
✓ Inventory updated with rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg
+
✓ Seeding policy updated for rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg with scope 'all'
```
```
$ rad node inventory
-
rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
+
rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg
```
modified radicle-cli/examples/rad-init-private-clone-seed.md
@@ -1,4 +1,4 @@
-
Given a private repo `rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu` belonging to Alice,
+
Given a private repo `rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT` belonging to Alice,
Alice allows Bob to fetch it, and Bob, without the updated identity document
is able to fetch it by specifiying Alice as a seed.

@@ -7,6 +7,7 @@ $ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW
...
$ rad inspect --identity
{
+
  "version": 2,
  "payload": {
    "xyz.radicle.project": {
      "defaultBranch": "master",
@@ -17,7 +18,14 @@ $ rad inspect --identity
  "delegates": [
    "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
  ],
-
  "threshold": 1,
+
  "canonicalRefs": {
+
    "rules": {
+
      "refs/heads/master": {
+
        "allow": "delegates",
+
        "threshold": 1
+
      }
+
    }
+
  },
  "visibility": {
    "type": "private",
    "allow": [
@@ -29,9 +37,9 @@ $ rad inspect --identity

``` ~bob
$ rad ls --all --private
-
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
-
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
-
✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
+
✓ Seeding policy updated for rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT with scope 'all'
+
✓ Fetching rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT from z6MknSL…StBU8Vi@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -47,7 +55,7 @@ Run `cd ./heartwood` to go to the repository directory.
We can also use `rad seed` to seed and fetch without creating a checkout.

``` ~bob
-
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --from z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
-
✓ Seeding policy exists for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
-
✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi@[..]..
+
$ rad seed rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --from z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
+
✓ Seeding policy exists for rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT with scope 'all'
+
✓ Fetching rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT from z6MknSL…StBU8Vi@[..]..
```
modified radicle-cli/examples/rad-init-private-clone.md
@@ -1,14 +1,14 @@
-
Given a private repo `rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu` belonging to Alice,
+
Given a private repo `rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT` belonging to Alice,
Bob tries to fetch it, and even though he's connected to Alice, it fails.

``` ~bob
$ rad ls
```
``` ~bob (fail)
-
$ rad clone rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
-
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
-
✗ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi@[..].. error: failed to perform fetch handshake
-
✗ Error: repository rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu not found
+
$ rad clone rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --timeout 1
+
✓ Seeding policy updated for rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT with scope 'all'
+
✗ Fetching rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT from z6MknSL…StBU8Vi@[..].. error: failed to perform fetch handshake
+
✗ Error: repository rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT not found
```

She allows Bob to view the repository. And when she syncs, one node (Bob) gets
@@ -25,14 +25,14 @@ Bob can now fetch the private repo without specifying a seed, because he knows
that Alice has the repo after she announced her refs:

``` ~bob
-
$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch
-
✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi@[..]..
+
$ rad sync rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --fetch
+
✓ Fetching rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
$ rad ls --private --all
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu   private      f2de534   radicle heartwood protocol & stack │
+
│ heartwood   rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT   private      f2de534   radicle heartwood protocol & stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

modified radicle-cli/examples/rad-init-private-no-seed.md
@@ -9,7 +9,7 @@ Initializing private radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu.
+
Your Repository ID (RID) is rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT.
You can show it any time by running `rad .` from this directory.

You have created a private repository.
@@ -27,8 +27,8 @@ No seeding policies to show.
We can decide to seed it later, so that others can fetch it from us, given
that they are part of the allow list:
```
-
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
-
✓ Seeding policy updated for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu with scope 'all'
+
$ rad seed rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT
+
✓ Seeding policy updated for rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT with scope 'all'
```

But it still won't show up in our inventory, since it's private:
modified radicle-cli/examples/rad-init-private-seed.md
@@ -9,22 +9,22 @@ $ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW
First, Bob seeds the repo.

``` ~bob
-
$ rad seed rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --no-fetch
+
$ rad seed rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --no-fetch
[..]
```

If Bob just tries to fetch it without specifying seeds, he gets an error:

``` ~bob
-
$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch
-
✗ Error: no seeds found for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
+
$ rad sync rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --fetch
+
✗ Error: no seeds found for rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT
```

He has to specify a seed that isn't in his routing table:

``` ~bob
-
$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi@[..]..
+
$ rad sync rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --fetch --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Fetching rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

@@ -33,7 +33,7 @@ $ rad ls --private --all
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu   private      f2de534   radicle heartwood protocol & stack │
+
│ heartwood   rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT   private      f2de534   radicle heartwood protocol & stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -41,8 +41,8 @@ Note that if multiple seeds are specified, the command succeeds as long as one
seed succeeds.

``` ~bob
-
$ rad sync rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu --fetch --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --seed z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx
-
✓ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from z6MknSL…StBU8Vi@[..]..
+
$ rad sync rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT --fetch --seed z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --seed z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx
+
✓ Fetching rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT from z6MknSL…StBU8Vi@[..]..
! Warning: no addresses found for z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx, skipping..
✓ Fetched repository from 1 seed(s)
```
modified radicle-cli/examples/rad-init-private.md
@@ -7,7 +7,7 @@ Initializing private radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu.
+
Your Repository ID (RID) is rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT.
You can show it any time by running `rad .` from this directory.

You have created a private repository.
@@ -25,6 +25,6 @@ $ rad seed
╭────────────────────────────────────────────────────────────────╮
│ Repository                          Name        Policy   Scope │
├────────────────────────────────────────────────────────────────┤
-
│ rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu   heartwood   allow    all   │
+
│ rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT   heartwood   allow    all   │
╰────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-init-sync-not-connected.md
@@ -7,7 +7,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji.
+
Your Repository ID (RID) is rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2.
You can show it any time by running `rad .` from this directory.

✗ Announcing.. <canceled>
modified radicle-cli/examples/rad-init-sync-preferred.md
@@ -7,7 +7,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW.
+
Your Repository ID (RID) is rad:z2eCRs3yG5orX2AqYiozcedMzbwg5.
You can show it any time by running `rad .` from this directory.

✓ Repository successfully synced to z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
@@ -16,7 +16,7 @@ You can show it any time by running `rad .` from this directory.
Your repository has been synced to the network and is now discoverable by peers.
View it in your browser at:

-
    https://app.radicle.xyz/nodes/[...]/rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW
+
    https://app.radicle.xyz/nodes/[...]/rad:z2eCRs3yG5orX2AqYiozcedMzbwg5

To push changes, run `git push`.
```
modified radicle-cli/examples/rad-init-sync-timeout.md
@@ -8,7 +8,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW.
+
Your Repository ID (RID) is rad:z2eCRs3yG5orX2AqYiozcedMzbwg5.
You can show it any time by running `rad .` from this directory.

✓ Repository successfully announced to the network.
modified radicle-cli/examples/rad-init-sync.md
@@ -9,7 +9,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji.
+
Your Repository ID (RID) is rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2.
You can show it any time by running `rad .` from this directory.

✓ Repository successfully announced to the network.
modified radicle-cli/examples/rad-init-with-existing-remote.md
@@ -26,7 +26,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:z2D6wQnKapY7dn5meBnbH2rUKNZbT.
+
Your Repository ID (RID) is rad:z4JnKDURwrqarjs9EkCSiY7QExoHz.
You can show it any time by running `rad .` from this directory.

Your repository will be announced to the network when you start your node.
modified radicle-cli/examples/rad-init.md
@@ -14,7 +14,7 @@ Initializing public radicle 👾 repository in [..]
  "defaultBranch": "master"
}

-
Your Repository ID (RID) is rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji.
+
Your Repository ID (RID) is rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2.
You can show it any time by running `rad .` from this directory.

Your repository will be announced to the network when you start your node.
@@ -26,7 +26,7 @@ If we try to initialize it again, we get an error:

``` (fail)
$ rad init
-
✗ Error: repository is already initialized with remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
✗ Error: repository is already initialized with remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

Repositories can be listed with the `ls` command:
@@ -36,7 +36,7 @@ $ rad ls
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -44,5 +44,5 @@ Public repositories are added to our inventory:

```
$ rad node inventory
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```
modified radicle-cli/examples/rad-inspect-noauth.md
@@ -8,5 +8,5 @@ $ rad self

```
$ rad inspect
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```
modified radicle-cli/examples/rad-inspect.md
@@ -3,14 +3,14 @@ command from inside a working copy:

```
$ rad inspect
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

As a shorthand, you can also simply use `rad .`:

```
$ rad .
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

It's also possible to display all of the repository's git references:
@@ -21,7 +21,7 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
    ├── cobs
    │   └── xyz.radicle.id
-
    │       └── 0656c217f917c3e06234771e9ecae53aba5e173e
+
    │       └── eeb8b44890570ccf85db7f3cb2a475100a27408a
    ├── heads
    │   └── master
    └── rad
@@ -34,7 +34,7 @@ And sigrefs:

```
$ rad inspect --sigrefs
-
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 99c549702e2bcfe02b0e68d4a2224fb7a1524529
+
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 7c1445cd018b1b0f51e0d815c3c03b289140eafa
```

Or display the repository identity's payload and delegates:
@@ -57,13 +57,14 @@ history:

```
$ rad inspect --history
-
commit 0656c217f917c3e06234771e9ecae53aba5e173e
-
blob   d96f425412c9f8ad5d9a9a05c9831d0728e2338d
+
commit eeb8b44890570ccf85db7f3cb2a475100a27408a
+
blob   b38d81ee99d880461a3b7b3502e5d1556e440ef3
date   Thu, 15 Dec 2022 17:28:04 +0000

    Initialize identity

 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -74,7 +75,14 @@ date Thu, 15 Dec 2022 17:28:04 +0000
   "delegates": [
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
   ],
-
   "threshold": 1
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
   }
 }

```
modified radicle-cli/examples/rad-issue.md
@@ -7,7 +7,7 @@ Let's say the new car you are designing with your peers has a problem with its f
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
-
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
+
│ Issue   0d18c610be2fbb4f47d45434c581f3bf0b0ff071        │
│ Author  alice (you)                                     │
│ Status  open                                            │
│                                                         │
@@ -22,17 +22,17 @@ $ rad issue list
╭──────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title                         Author           Labels   Assignees   Opened │
├──────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   alice    (you)                        now    │
+
│ ●   0d18c61   flux capacitor underpowered   alice    (you)                        now    │
╰──────────────────────────────────────────────────────────────────────────────────────────╯
```

Show the issue information issue.

```
-
$ rad issue show d87dcfe
+
$ rad issue show 0d18c61
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
-
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
+
│ Issue   0d18c610be2fbb4f47d45434c581f3bf0b0ff071        │
│ Author  alice (you)                                     │
│ Status  open                                            │
│                                                         │
@@ -51,8 +51,8 @@ Let's assign ourselves to this one, this is to ensure work is not
duplicated. While we're at it, let's add a label.

```
-
$ rad issue assign d87dcfe --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --no-announce
-
$ rad issue label d87dcfe --add good-first-issue --no-announce
+
$ rad issue assign 0d18c61 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --no-announce
+
$ rad issue label 0d18c61 --add good-first-issue --no-announce
```

It will now show in the list of issues assigned to us, along with the new label.
@@ -62,14 +62,14 @@ $ rad issue list --assigned
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title                         Author           Labels             Assignees   Opened │
├────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   d87dcfe   flux capacitor underpowered   alice    (you)   good-first-issue   alice       now    │
+
│ ●   0d18c61   flux capacitor underpowered   alice    (you)   good-first-issue   alice       now    │
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

Note: this can always be undone with the `unassign` subcommand.

```
-
$ rad issue assign d87dcfe --delete did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --no-announce
+
$ rad issue assign 0d18c61 --delete did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --no-announce
```

Great, now we have communicated to the world about our car's defect.
@@ -78,29 +78,29 @@ But wait! We've found an important detail about the car's power requirements.
It will help whoever works on a fix.

```
-
$ rad issue comment d87dcfe8c2b3200e78b128d9b959cfdf7063fefe --message 'The flux capacitor needs 1.21 Gigawatts' -q --no-announce
-
2193e871916d18ddd0416b5198cb08c5dc7416b7
-
$ rad issue comment d87dcfe8c2b3200e78b128d9b959cfdf7063fefe --reply-to 2193e871916d18ddd0416b5198cb08c5dc7416b7 --message 'More power!' -q --no-announce
-
880fdcd94b36e412fc906b510f41008700d65424
+
$ rad issue comment 0d18c610be2fbb4f47d45434c581f3bf0b0ff071 --message 'The flux capacitor needs 1.21 Gigawatts' -q --no-announce
+
30d72f0b1e6a96e39c4c408369ea44186430d21b
+
$ rad issue comment 0d18c610be2fbb4f47d45434c581f3bf0b0ff071 --reply-to 30d72f0b1e6a96e39c4c408369ea44186430d21b --message 'More power!' -q --no-announce
+
4ff4fdc106a4fb3a2a035155e1fb3e8b3c1e0df4
```

We can see our comments by showing the issue:

```
-
$ rad issue show d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
+
$ rad issue show 0d18c610be2fbb4f47d45434c581f3bf0b0ff071
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
-
│ Issue   d87dcfe8c2b3200e78b128d9b959cfdf7063fefe        │
+
│ Issue   0d18c610be2fbb4f47d45434c581f3bf0b0ff071        │
│ Author  alice (you)                                     │
│ Labels  good-first-issue                                │
│ Status  open                                            │
│                                                         │
│ Flux capacitor power requirements exceed current supply │
├─────────────────────────────────────────────────────────┤
-
│ alice (you) now 2193e87                                 │
+
│ alice (you) now 30d72f0                                 │
│ The flux capacitor needs 1.21 Gigawatts                 │
├─────────────────────────────────────────────────────────┤
-
│ alice (you) now 880fdcd                                 │
+
│ alice (you) now 4ff4fdc                                 │
│ More power!                                             │
╰─────────────────────────────────────────────────────────╯
```
@@ -108,9 +108,9 @@ $ rad issue show d87dcfe8c2b3200e78b128d9b959cfdf7063fefe
We can also edit a comment:

```
-
$ rad issue comment d87dcfe --edit 880fdcd -m "Even more power!"
+
$ rad issue comment 0d18c61 --edit 4ff4fdc -m "Even more power!"
╭─────────────────────────╮
-
│ alice (you) now 880fdcd │
+
│ alice (you) now 4ff4fdc │
│ Even more power!        │
╰─────────────────────────╯
```
modified radicle-cli/examples/rad-job.md
@@ -2,7 +2,7 @@ The `rad job` command lets you manage job COBs. Let's first checkout the
`heartwood` repository:

```
-
$ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
$ rad checkout rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
✓ Repository checkout successful under ./heartwood
$ cd heartwood
```
modified radicle-cli/examples/rad-merge-after-update.md
@@ -4,8 +4,8 @@ Let's start by creating a patch.
$ git checkout -b feature/1 -q
$ git commit --allow-empty -q -m "First change"
$ git push rad HEAD:refs/patches
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -16,8 +16,8 @@ $ git commit --amend --allow-empty -q -m "Amended change"
$ git checkout master -q
$ git merge feature/1 -q
$ git push rad master
-
✓ Canonical head updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Canonical head for refs/heads/master updated to 954bcdb5008447ce294a61a21d7eb87afbe7f4a6
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..954bcdb  master -> master
```

@@ -27,8 +27,8 @@ update it, we expect it to be updated and merged:
``` (stderr) RAD_SOCKET=/dev/null
$ git checkout feature/1 -q
$ git push -f
-
✓ Patch 696ec55 updated to revision [...]
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 + 20aa5dd...954bcdb feature/1 -> patches/696ec5508494692899337afe6713fe1796d0315c (forced update)
+
✓ Patch 09a3de4 updated to revision [...]
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 merged
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 + 20aa5dd...954bcdb feature/1 -> patches/09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 (forced update)
```
modified radicle-cli/examples/rad-merge-no-ff.md
@@ -4,8 +4,8 @@ First, let's create a patch.
$ git checkout -b feature/1 -q
$ git commit --allow-empty -q -m "First change"
$ git push rad HEAD:refs/patches
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -36,8 +36,8 @@ committer radicle <radicle@localhost> 1671125284 +0000
Finally, we push master and expect the patch to be merged.
``` (stderr) RAD_SOCKET=/dev/null
$ git push rad master
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
-
✓ Canonical head updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 merged
+
✓ Canonical head for refs/heads/master updated to 737a10cfa29111afeb0d43cf3545cee386b939ec
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..737a10c  master -> master
```
modified radicle-cli/examples/rad-merge-via-push.md
@@ -1,3 +1,4 @@
+
1
Let's start by creating two patches.

```
@@ -7,8 +8,8 @@ $ git commit --allow-empty -m "First change"
```
``` (stderr) RAD_SOCKET=/dev/null
$ git push rad HEAD:refs/patches
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```
```
@@ -18,8 +19,8 @@ $ git commit --allow-empty -m "Second change"
```
``` (stderr) RAD_SOCKET=/dev/null
$ git push rad HEAD:refs/patches
-
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -28,8 +29,8 @@ This creates some remote tracking branches for us:
```
$ git branch -r
  rad/master
-
  rad/patches/356f73863a8920455ff6e77cd9c805d68910551b
-
  rad/patches/696ec5508494692899337afe6713fe1796d0315c
+
  rad/patches/09a3de4ac2c4d012c4a9c84c0cb306a066a0b084
+
  rad/patches/0e8cc60585b6bb6a1236dc9958bf09883ecba9f3
```

And some remote refs:
@@ -40,15 +41,15 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
    ├── cobs
    │   ├── xyz.radicle.id
-
    │   │   └── 0656c217f917c3e06234771e9ecae53aba5e173e
+
    │   │   └── eeb8b44890570ccf85db7f3cb2a475100a27408a
    │   └── xyz.radicle.patch
-
    │       ├── 356f73863a8920455ff6e77cd9c805d68910551b
-
    │       └── 696ec5508494692899337afe6713fe1796d0315c
+
    │       ├── 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084
+
    │       └── 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3
    ├── heads
    │   ├── master
    │   └── patches
-
    │       ├── 356f73863a8920455ff6e77cd9c805d68910551b
-
    │       └── 696ec5508494692899337afe6713fe1796d0315c
+
    │       ├── 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084
+
    │       └── 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3
    └── rad
        ├── id
        ├── root
@@ -68,10 +69,10 @@ When we push to `rad/master`, we automatically merge the patches:

``` (stderr) RAD_SOCKET=/dev/null
$ git push rad master
-
✓ Patch 356f73863a8920455ff6e77cd9c805d68910551b merged
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
-
✓ Canonical head updated to d6399c71702b40bae00825b3c444478d06b4e91c
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 merged
+
✓ Patch 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3 merged
+
✓ Canonical head for refs/heads/master updated to d6399c71702b40bae00825b3c444478d06b4e91c
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..d6399c7  master -> master
```
```
@@ -79,28 +80,13 @@ $ rad patch --merged
╭─────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title          Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────┤
-
│ ✔  [ ... ]  Second change  alice   (you)  -        daf349f  +0  -0  now     │
│ ✔  [ ... ]  First change   alice   (you)  -        20aa5dd  +0  -0  now     │
+
│ ✔  [ ... ]  Second change  alice   (you)  -        daf349f  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────╯
-
$ rad patch show 696ec5508494692899337afe6713fe1796d0315c
-
╭────────────────────────────────────────────────────────────────╮
-
│ Title     First change                                         │
-
│ Patch     696ec5508494692899337afe6713fe1796d0315c             │
-
│ Author    alice (you)                                          │
-
│ Head      20aa5dde6210796c3a2f04079b42316a31d02689             │
-
│ Branches  feature/1                                            │
-
│ Commits   ahead 0, behind 2                                    │
-
│ Status    merged                                               │
-
├────────────────────────────────────────────────────────────────┤
-
│ 20aa5dd First change                                           │
-
├────────────────────────────────────────────────────────────────┤
-
│ ● opened by alice (you) (20aa5dd) now                          │
-
│   └─ ✓ merged by alice (you) at revision 696ec55 (20aa5dd) now │
-
╰────────────────────────────────────────────────────────────────╯
-
$ rad patch show 356f73863a8920455ff6e77cd9c805d68910551b
+
$ rad patch show 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3
╭────────────────────────────────────────────────────────────────╮
│ Title     Second change                                        │
-
│ Patch     356f73863a8920455ff6e77cd9c805d68910551b             │
+
│ Patch     0e8cc60585b6bb6a1236dc9958bf09883ecba9f3             │
│ Author    alice (you)                                          │
│ Head      daf349ff76bedf48c5f292290b682ee7be0683cf             │
│ Branches  feature/2                                            │
@@ -110,7 +96,7 @@ $ rad patch show 356f73863a8920455ff6e77cd9c805d68910551b
│ daf349f Second change                                          │
├────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (daf349f) now                          │
-
│   └─ ✓ merged by alice (you) at revision 356f738 (daf349f) now │
+
│   └─ ✓ merged by alice (you) at revision 0e8cc60 (daf349f) now │
╰────────────────────────────────────────────────────────────────╯
```

@@ -129,10 +115,10 @@ z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
    ├── cobs
    │   ├── xyz.radicle.id
-
    │   │   └── 0656c217f917c3e06234771e9ecae53aba5e173e
+
    │   │   └── eeb8b44890570ccf85db7f3cb2a475100a27408a
    │   └── xyz.radicle.patch
-
    │       ├── 356f73863a8920455ff6e77cd9c805d68910551b
-
    │       └── 696ec5508494692899337afe6713fe1796d0315c
+
    │       ├── 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084
+
    │       └── 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3
    ├── heads
    │   └── master
    └── rad
@@ -147,9 +133,9 @@ the first patch, even though they were pushed together.
``` (stderr) RAD_SOCKET=/dev/null
$ git reset --hard HEAD^
$ git push -f rad
-
! Patch 356f73863a8920455ff6e77cd9c805d68910551b reverted at revision 356f738
-
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
! Patch 0e8cc60585b6bb6a1236dc9958bf09883ecba9f3 reverted at revision 0e8cc60
+
✓ Canonical head for refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + d6399c7...20aa5dd master -> master (forced update)
```
```
@@ -157,7 +143,7 @@ $ rad patch --all
╭─────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title          Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────┤
-
│ ●  356f738  Second change  alice   (you)  -        daf349f  +0  -0  now     │
-
│ ✔  696ec55  First change   alice   (you)  -        20aa5dd  +0  -0  now     │
+
│ ✔  09a3de4  First change   alice   (you)  -        20aa5dd  +0  -0  now     │
+
│ ●  0e8cc60  Second change  alice   (you)  -        daf349f  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-node.md
@@ -43,7 +43,7 @@ $ rad seed
╭───────────────────────────────────────────────────────────────────╮
│ Repository                          Name        Policy   Scope    │
├───────────────────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   heartwood   allow    followed │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   heartwood   allow    followed │
╰───────────────────────────────────────────────────────────────────╯
```

@@ -68,7 +68,7 @@ $ rad node routing
╭─────────────────────────────────────────────────────╮
│ RID                                 NID             │
├─────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   z6MknSL…StBU8Vi │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   z6MknSL…StBU8Vi │
╰─────────────────────────────────────────────────────╯
```

@@ -90,8 +90,8 @@ $ rad node stop
Note that if we unseed a repository, it is no longer part of our inventory:

```
-
$ rad unseed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji removed
+
$ rad unseed rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Seeding policy for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 removed
$ rad node inventory
```

@@ -106,11 +106,11 @@ $ rad node inventory
But if we start seeding the repository we have locally again, it'll show
up in our inventory:
```
-
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Inventory updated with rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'all'
+
$ rad seed rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Inventory updated with rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'all'
$ rad node inventory
-
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

Some commands also give us a hint if the node isn't running:
modified radicle-cli/examples/rad-patch-ahead-behind.md
@@ -37,8 +37,8 @@ $ git log --graph --decorate --abbrev-commit --pretty=oneline --all
Then we create a patch from `feature/1`:
``` (stderr)
$ git push rad feature/1:refs/patches
-
✓ Patch 217f050f8891def8fb863f7c0b4f85c89f97299d opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 9128a6dcd3b043b420a1dfd541cf24b5d0e65d39 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   feature/1 -> refs/patches
```

@@ -48,17 +48,17 @@ $ rad patch list
╭────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title     Author         Reviews  Head     +   -   Updated │
├────────────────────────────────────────────────────────────────────────┤
-
│ ●  217f050  Add Alan  alice   (you)  -        5c88a79  +1  -0  now     │
+
│ ●  9128a6d  Add Alan  alice   (you)  -        5c88a79  +1  -0  now     │
╰────────────────────────────────────────────────────────────────────────╯
```

When showing the patch, we see that it is `ahead 1, behind 1`, since master has
diverged by one commit:
```
-
$ rad patch show -v -p 217f050
+
$ rad patch show -v -p 9128a6d
╭────────────────────────────────────────────────────╮
│ Title     Add Alan                                 │
-
│ Patch     217f050f8891def8fb863f7c0b4f85c89f97299d │
+
│ Patch     9128a6dcd3b043b420a1dfd541cf24b5d0e65d39 │
│ Author    alice (you)                              │
│ Head      5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
│ Base      f64fb2c8fe28f7c458c72ec8d700373924794943 │
@@ -93,18 +93,18 @@ $ git checkout -q -b feature/2 feature/1
$ sed -i '$a Mel Farna' CONTRIBUTORS
$ git commit -a -q -m "Add Mel"
$ git push -o patch.message="Add Mel" rad HEAD:refs/patches
-
✓ Patch e22ff008e2a0ed47262890d13263031d7555b555 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 5fd13489ca986a1a7fda5feb4c2aab1982913304 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

When we look at the patch, we see that it has both commits, because this new
patch uses the same base as the previous patch:
```
-
$ rad patch show -v e22ff008e2a0ed47262890d13263031d7555b555
+
$ rad patch show -v 5fd13489ca986a1a7fda5feb4c2aab1982913304
╭────────────────────────────────────────────────────╮
│ Title     Add Mel                                  │
-
│ Patch     e22ff008e2a0ed47262890d13263031d7555b555 │
+
│ Patch     5fd13489ca986a1a7fda5feb4c2aab1982913304 │
│ Author    alice (you)                              │
│ Head      7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
│ Base      f64fb2c8fe28f7c458c72ec8d700373924794943 │
@@ -124,8 +124,8 @@ If we want to instead create a "stacked" patch, we can do so with the

``` (stderr)
$ git push -o patch.message="Add Mel #2" -o patch.base=HEAD^ rad HEAD:refs/patches
-
✓ Patch a467ffa260c4fbe355b6fb550ba0c4956078717e opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch fdde8e3672f7f5eea2371d92325c4ecfbe2066a5 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -136,10 +136,10 @@ However, since the patch is still intended to be merged into `master`, we see
that it is still two commits ahead and one behind from `master`.

```
-
$ rad patch show -v a467ffa260c4fbe355b6fb550ba0c4956078717e
+
$ rad patch show -v fdde8e3672f7f5eea2371d92325c4ecfbe2066a5
╭────────────────────────────────────────────────────╮
│ Title     Add Mel #2                               │
-
│ Patch     a467ffa260c4fbe355b6fb550ba0c4956078717e │
+
│ Patch     fdde8e3672f7f5eea2371d92325c4ecfbe2066a5 │
│ Author    alice (you)                              │
│ Head      7f63fcbcf23fc39eea784c091ad3d20d7e4bd005 │
│ Base      5c88a79d75f5c2b4cc51ee6f163d2db91ee198d7 │
modified radicle-cli/examples/rad-patch-change-base.md
@@ -15,11 +15,11 @@ $ git commit -v -m "Define power requirements"
```
``` (stderr)
$ git push rad flux-capacitor-power
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new branch]      flux-capacitor-power -> flux-capacitor-power
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
-
✓ Patch aa45913e757cacd46972733bddee5472c78fa32a opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch c90967c43719b916e0b5a8b5dafe353608f8a08a opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -35,18 +35,18 @@ $ git commit --message "Add README, just for the fun"
```
``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun" HEAD:refs/patches
-
✓ Patch 183d343ab47d7fe18baf1b24b7209ad033d7fe5c opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 3e1ca74542ded1f51ca9a744ed6266f23bf2507f opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

Our second patch looks like the following:

```
-
$ rad patch show 183d343ab47d7fe18baf1b24b7209ad033d7fe5c -v
+
$ rad patch show 3e1ca74542ded1f51ca9a744ed6266f23bf2507f -v
╭────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun             │
-
│ Patch     183d343ab47d7fe18baf1b24b7209ad033d7fe5c │
+
│ Patch     3e1ca74542ded1f51ca9a744ed6266f23bf2507f │
│ Author    alice (you)                              │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66 │
│ Base      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
@@ -66,18 +66,18 @@ commit `3e674d1` as part of this patch, so we create a new revision
with a new `base`:

```
-
$ rad patch update 183d343 -b 3e674d1 -m "Whoops, forgot to set the base" --no-announce
-
ebe76f9c2148eb595d7a745f82275786bf3458c3
+
$ rad patch update 3e1ca74 -b 3e674d1 -m "Whoops, forgot to set the base" --no-announce
+
852c792f460ca485ce22b6acc41c150d7aeb4642
```

Now, if we show the patch we can see the patch's base has changed and
we have a single commit:

```
-
$ rad patch show 183d343 -v
+
$ rad patch show 3e1ca74 -v
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun                              │
-
│ Patch     183d343ab47d7fe18baf1b24b7209ad033d7fe5c                  │
+
│ Patch     3e1ca74542ded1f51ca9a744ed6266f23bf2507f                  │
│ Author    alice (you)                                               │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                  │
│ Base      3e674d1a1df90807e934f9ae5da2591dd6848a33                  │
@@ -88,6 +88,6 @@ $ rad patch show 183d343 -v
│ 27857ec Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (27857ec) now                               │
-
│ ↑ updated to ebe76f9c2148eb595d7a745f82275786bf3458c3 (27857ec) now │
+
│ ↑ updated to 852c792f460ca485ce22b6acc41c150d7aeb4642 (27857ec) now │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-checkout-force.md
@@ -13,9 +13,9 @@ $ git commit -v -m "Define power requirements"

``` ~alice (stderr)
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
-
✓ Patch aa45913e757cacd46972733bddee5472c78fa32a opened
+
✓ Patch c90967c43719b916e0b5a8b5dafe353608f8a08a opened
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -24,11 +24,11 @@ On the other end, Bob uses `rad patch checkout` to view the patch:
``` ~bob
$ cd heartwood
$ rad sync -f
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
-
$ rad patch checkout aa45913 --name alice-init
-
✓ Switched to branch alice-init at revision aa45913
-
✓ Branch alice-init setup to track rad/patches/aa45913e757cacd46972733bddee5472c78fa32a
+
$ rad patch checkout c90967c --name alice-init
+
✓ Switched to branch alice-init at revision c90967c
+
✓ Branch alice-init setup to track rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```

Meanwhile, we may see some more changes that we need to make, so we
@@ -45,28 +45,28 @@ $ git commit --message "Add README, just for the fun"

``` ~alice (stderr)
$ git push rad -o patch.message="Add README, just for the fun"
-
✓ Patch aa45913 updated to revision 3156bed9d64d4675d6cf56612d217fc5f4e8a53a
-
To compare against your previous revision aa45913, run:
+
✓ Patch c90967c updated to revision 594bb93b4ba836777c111053af7b61ff772afbc5
+
To compare against your previous revision c90967c, run:

   git range-diff f2de534[..] 3e674d1[..] 27857ec[..]

✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   3e674d1..27857ec  flux-capacitor-power -> patches/aa45913e757cacd46972733bddee5472c78fa32a
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   3e674d1..27857ec  flux-capacitor-power -> patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```

Bob fetches these new changes and can see their branch is now behind:

``` ~bob (stderr)
$ git fetch rad
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
   3e674d1..27857ec  patches/aa45913e757cacd46972733bddee5472c78fa32a -> rad/patches/aa45913e757cacd46972733bddee5472c78fa32a
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
   3e674d1..27857ec  patches/c90967c43719b916e0b5a8b5dafe353608f8a08a -> rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```

``` ~bob
$ git status
On branch alice-init
-
Your branch is behind 'rad/patches/aa45913e757cacd46972733bddee5472c78fa32a' by 1 commit, and can be fast-forwarded.
+
Your branch is behind 'rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean
@@ -78,17 +78,17 @@ overwrite any changes. Bob can choose to use the `--force` (`-f`) flag to
ensure that they are looking at the latest changes:

``` ~bob (fail)
-
$ rad patch checkout aa45913 --name alice-init
+
$ rad patch checkout c90967c --name alice-init
✗ Performing checkout... <canceled>
✗ Error: branch 'alice-init' already exists (use `--force` to overwrite)
```

``` ~bob
-
$ rad patch checkout aa45913 -f --name alice-init
-
✓ Switched to branch alice-init at revision 3156bed
+
$ rad patch checkout c90967c -f --name alice-init
+
✓ Switched to branch alice-init at revision 594bb93
$ git status
On branch alice-init
-
Your branch is up to date with 'rad/patches/aa45913e757cacd46972733bddee5472c78fa32a'.
+
Your branch is up to date with 'rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a'.

nothing to commit, working tree clean
```
modified radicle-cli/examples/rad-patch-checkout-revision.md
@@ -5,7 +5,7 @@ So first, let's add another change to the patch and a `LICENSE` file.
$ touch LICENSE
$ git add LICENSE
$ git commit --message "Add LICENSE, just for the business"
-
[patch/aa45913 639f44a] Add LICENSE, just for the business
+
[patch/c90967c 639f44a] Add LICENSE, just for the business
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 LICENSE
$ git push rad -o patch.message="Add LICENSE, just for the business"
@@ -14,13 +14,13 @@ $ git push rad -o patch.message="Add LICENSE, just for the business"
We can see the list of revisions of the patch by `show`ing it:

```
-
$ rad patch show aa45913
+
$ rad patch show c90967c
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define power requirements                                 │
-
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a                  │
+
│ Patch     c90967c43719b916e0b5a8b5dafe353608f8a08a                  │
│ Author    alice (you)                                               │
│ Head      639f44a25145a37f747f3c84265037a9461e44c5                  │
-
│ Branches  patch/aa45913                                             │
+
│ Branches  patch/c90967c                                             │
│ Commits   ahead 3, behind 0                                         │
│ Status    open                                                      │
│                                                                     │
@@ -31,16 +31,16 @@ $ rad patch show aa45913
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (3e674d1) now                               │
-
│ ↑ updated to 3156bed9d64d4675d6cf56612d217fc5f4e8a53a (27857ec) now │
-
│ ↑ updated to 2f5324f61e05cda65b667eeea02570d077a8e724 (639f44a) now │
+
│ ↑ updated to 594bb93b4ba836777c111053af7b61ff772afbc5 (27857ec) now │
+
│ ↑ updated to 92a95e995d436248d844bdd6c94704725efc283d (639f44a) now │
╰─────────────────────────────────────────────────────────────────────╯
```

So, let's checkout the previous revision, `0c0942e2`:

```
-
$ rad patch checkout aa45913 --revision 3156bed9d64d4675d6cf56612d217fc5f4e8a53a -f
-
✓ Switched to branch patch/aa45913 at revision 3156bed
+
$ rad patch checkout c90967c --revision 594bb93b4ba836777c111053af7b61ff772afbc5 -f
+
✓ Switched to branch patch/c90967c at revision 594bb93
```

And we can confirm that the current commit corresponds to `27857ec`:
modified radicle-cli/examples/rad-patch-checkout.md
@@ -22,17 +22,17 @@ Once the code is ready, we open (or create) a patch with our changes for the pro

``` (stderr)
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
-
✓ Patch aa45913e757cacd46972733bddee5472c78fa32a opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch c90967c43719b916e0b5a8b5dafe353608f8a08a opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

Now, let's checkout the patch that we just created:

```
-
$ rad patch checkout aa45913e757cacd46972733bddee5472c78fa32a
-
✓ Switched to branch patch/aa45913 at revision aa45913
-
✓ Branch patch/aa45913 setup to track rad/patches/aa45913e757cacd46972733bddee5472c78fa32a
+
$ rad patch checkout c90967c43719b916e0b5a8b5dafe353608f8a08a
+
✓ Switched to branch patch/c90967c at revision c90967c
+
✓ Branch patch/c90967c setup to track rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```

Note that `rad patch checkout` can be used to switch to the patch branch
@@ -40,8 +40,8 @@ as long as we haven't made changes to it.

```
$ git checkout master -q
-
$ rad patch checkout aa45913
-
✓ Switched to branch patch/aa45913 at revision aa45913
+
$ rad patch checkout c90967c
+
✓ Switched to branch patch/c90967c at revision c90967c
```

Now, let's add a README too!
@@ -50,7 +50,7 @@ Now, let's add a README too!
$ touch README.md
$ git add README.md
$ git commit --message "Add README, just for the fun"
-
[patch/aa45913 27857ec] Add README, just for the fun
+
[patch/c90967c 27857ec] Add README, just for the fun
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md
```
@@ -59,11 +59,11 @@ We can now finish off the update:

``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun"
-
✓ Patch aa45913 updated to revision 3156bed9d64d4675d6cf56612d217fc5f4e8a53a
-
To compare against your previous revision aa45913, run:
+
✓ Patch c90967c updated to revision 594bb93b4ba836777c111053af7b61ff772afbc5
+
To compare against your previous revision c90967c, run:

   git range-diff f2de534[..] 3e674d1[..] 27857ec[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   3e674d1..27857ec  patch/aa45913 -> patches/aa45913e757cacd46972733bddee5472c78fa32a
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   3e674d1..27857ec  patch/c90967c -> patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```
modified radicle-cli/examples/rad-patch-delete.md
@@ -10,30 +10,30 @@ $ git commit -m "Introduce license"

``` ~alice (stderr)
$ git push rad -o patch.draft -o patch.message="Define LICENSE for project" HEAD:refs/patches
-
✓ Patch 6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b drafted
+
✓ Patch e5dc5fd15fbe952da6a0f43934eae57d47b93e36 drafted
✓ Synced with 2 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

``` ~bob
$ cd heartwood
$ rad sync -f
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
-
$ rad patch comment 6c61ef1 -m "I think we should use MIT"
+
$ rad patch comment e5dc5fd -m "I think we should use MIT"
╭───────────────────────────╮
-
│ bob (you) now 833db19     │
+
│ bob (you) now 2ec2cc1     │
│ I think we should use MIT │
╰───────────────────────────╯
✓ Synced with 2 node(s)
```

``` ~alice
-
$ rad patch show 6c61ef1 -v
+
$ rad patch show e5dc5fd -v
╭────────────────────────────────────────────────────╮
│ Title     Define LICENSE for project               │
-
│ Patch     6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b │
+
│ Patch     e5dc5fd15fbe952da6a0f43934eae57d47b93e36 │
│ Author    alice (you)                              │
│ Head      717c900ec17735639587325e0fd9fe09991c9edd │
│ Base      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
@@ -45,12 +45,12 @@ $ rad patch show 6c61ef1 -v
├────────────────────────────────────────────────────┤
│ ● opened by alice (you) (717c900) now              │
├────────────────────────────────────────────────────┤
-
│ bob z6Mkt67…v4N1tRk now 833db19                    │
+
│ bob z6Mkt67…v4N1tRk now 2ec2cc1                    │
│ I think we should use MIT                          │
╰────────────────────────────────────────────────────╯
-
$ rad patch comment 6c61ef1 --reply-to 833db19 -m "Thanks, I'll add it!"
+
$ rad patch comment e5dc5fd --reply-to 2ec2cc1 -m "Thanks, I'll add it!"
╭─────────────────────────╮
-
│ alice (you) now 1803a38 │
+
│ alice (you) now 737dcab │
│ Thanks, I'll add it!    │
╰─────────────────────────╯
✓ Synced with 2 node(s)
@@ -68,24 +68,24 @@ $ git commit -am "Add MIT License"

``` ~alice (stderr)
$ git push -f
-
✓ Patch 6c61ef1 updated to revision 93915b9afa94a9dc4f52f12cdf077d4613ea3eb3
-
To compare against your previous revision 6c61ef1, run:
+
✓ Patch e5dc5fd updated to revision 1a1082a96f552767d352d69b8e6524aeb82f67a4
+
To compare against your previous revision e5dc5fd, run:

   git range-diff f2de534[..] 717c900[..] 1cc8cd9[..]

✓ Synced with 2 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   717c900..1cc8cd9  prepare-license -> patches/6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   717c900..1cc8cd9  prepare-license -> patches/e5dc5fd15fbe952da6a0f43934eae57d47b93e36
```

``` ~bob
-
$ rad patch review 6c61ef1 --accept -m "LGTM!"
-
✓ Patch 6c61ef1 accepted
+
$ rad patch review e5dc5fd --accept -m "LGTM!"
+
✓ Patch e5dc5fd accepted
✓ Synced with 2 node(s)
-
$ rad patch show 6c61ef1 -v
+
$ rad patch show e5dc5fd -v
╭─────────────────────────────────────────────────────────────────────╮
│ Title    Define LICENSE for project                                 │
-
│ Patch    6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b                   │
+
│ Patch    e5dc5fd15fbe952da6a0f43934eae57d47b93e36                   │
│ Author   alice z6MknSL…StBU8Vi                                      │
│ Head     1cc8cd9de8ccc44b4fe3876f2dbd2cd1cf9ddc0e                   │
│ Base     f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354                   │
@@ -96,21 +96,21 @@ $ rad patch show 6c61ef1 -v
│ 717c900 Introduce license                                           │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice z6MknSL…StBU8Vi (717c900) now                     │
-
│ ↑ updated to 93915b9afa94a9dc4f52f12cdf077d4613ea3eb3 (1cc8cd9) now │
+
│ ↑ updated to 1a1082a96f552767d352d69b8e6524aeb82f67a4 (1cc8cd9) now │
│   └─ ✓ accepted by bob (you) now                                    │
╰─────────────────────────────────────────────────────────────────────╯
```

``` ~bob
-
$ rad patch delete 6c61ef1
+
$ rad patch delete e5dc5fd
✓ Synced with 2 node(s)
```

``` ~alice
-
$ rad patch show 6c61ef1 -v
+
$ rad patch show e5dc5fd -v
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define LICENSE for project                                │
-
│ Patch     6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b                  │
+
│ Patch     e5dc5fd15fbe952da6a0f43934eae57d47b93e36                  │
│ Author    alice (you)                                               │
│ Head      1cc8cd9de8ccc44b4fe3876f2dbd2cd1cf9ddc0e                  │
│ Base      f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354                  │
@@ -122,7 +122,7 @@ $ rad patch show 6c61ef1 -v
│ 717c900 Introduce license                                           │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (717c900) now                               │
-
│ ↑ updated to 93915b9afa94a9dc4f52f12cdf077d4613ea3eb3 (1cc8cd9) now │
+
│ ↑ updated to 1a1082a96f552767d352d69b8e6524aeb82f67a4 (1cc8cd9) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -130,11 +130,11 @@ If Alice also decides to delete the patch, then any seeds that have synced with
Alice should no longer have the patch:

``` ~alice
-
$ rad patch delete 6c61ef1
+
$ rad patch delete e5dc5fd
✓ Synced with 2 node(s)
```

``` ~seed (fails)
-
$ rad patch show --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji 6c61ef1 -v
-
✗ Error: Patch `6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b` not found
+
$ rad patch show --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 e5dc5fd -v
+
✗ Error: Patch `e5dc5fd15fbe952da6a0f43934eae57d47b93e36` not found
```
modified radicle-cli/examples/rad-patch-detached-head.md
@@ -30,10 +30,10 @@ Now, we can create a commit on top of this and create a patch, as usual:
``` (stderr) RAD_HINT=1
$ git commit -a -m "Add things" -q --allow-empty
$ git push -o patch.message="Add things #1" -o patch.message="See commits for details." rad HEAD:refs/patches
-
✓ Patch 6035d2f582afbe01ff23ea87528ae523d76875b6 opened
+
✓ Patch a183e324b82e94c548eb43b7acb7c7d92ebe7761 opened
hint: offline push, your node is not running
hint: to sync with the network, run `rad node start`
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

modified radicle-cli/examples/rad-patch-diff.md
@@ -10,7 +10,13 @@ $ git commit -m "Add README" -q
$ git push rad HEAD:refs/patches
```
```
-
$ rad patch diff 147309e
+
$ rad patch
+
╭──────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title       Author         Reviews  Head     +   -   Updated │
+
├──────────────────────────────────────────────────────────────────────────┤
+
│ ●  a44b0da  Add README  alice   (you)  -        2420bc3  +1  -0  now     │
+
╰──────────────────────────────────────────────────────────────────────────╯
+
$ rad patch diff a44b0da
╭───────────────────────────╮
│ README.md +1 ❲created❳    │
├───────────────────────────┤
@@ -31,7 +37,7 @@ $ git commit --amend -q
$ git push -f
```
```
-
$ rad patch diff 147309e
+
$ rad patch diff a44b0da
╭─────────────────────────────╮
│ RADICLE.md +1 ❲created❳     │
├─────────────────────────────┤
@@ -52,7 +58,7 @@ Buf if we only want to see the changes from the first revision, we can do that
too.

```
-
$ rad patch diff 147309e --revision 147309e
+
$ rad patch diff a44b0da --revision a44b0da
╭───────────────────────────╮
│ README.md +1 ❲created❳    │
├───────────────────────────┤
modified radicle-cli/examples/rad-patch-draft.md
@@ -9,18 +9,18 @@ To open a patch in draft mode, we use the `--draft` option:

``` (stderr)
$ git push -o patch.draft -o patch.message="Nothing yet" rad HEAD:refs/patches
-
✓ Patch 97e18f8598237a396a1c0ac1509c89028e666c97 drafted
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch acee9948a4ff68e49a678734e8a0b86ff29f2e40 drafted
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

We can confirm it's a draft by running `show`:

```
-
$ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
+
$ rad patch show acee9948a4ff68e49a678734e8a0b86ff29f2e40
╭────────────────────────────────────────────────────╮
│ Title     Nothing yet                              │
-
│ Patch     97e18f8598237a396a1c0ac1509c89028e666c97 │
+
│ Patch     acee9948a4ff68e49a678734e8a0b86ff29f2e40 │
│ Author    alice (you)                              │
│ Head      2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches  cloudhead/draft                          │
@@ -36,14 +36,14 @@ $ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
Once the patch is ready for review, we can use the `ready` command:

```
-
$ rad patch ready 97e18f8598237a396a1c0ac1509c89028e666c97 --no-announce
+
$ rad patch ready acee9948a4ff68e49a678734e8a0b86ff29f2e40 --no-announce
```

```
-
$ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
+
$ rad patch show acee9948a4ff68e49a678734e8a0b86ff29f2e40
╭────────────────────────────────────────────────────╮
│ Title     Nothing yet                              │
-
│ Patch     97e18f8598237a396a1c0ac1509c89028e666c97 │
+
│ Patch     acee9948a4ff68e49a678734e8a0b86ff29f2e40 │
│ Author    alice (you)                              │
│ Head      2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches  cloudhead/draft                          │
@@ -60,11 +60,11 @@ If for whatever reason, it needed to go back into draft mode, we could use
the `--undo` flag:

```
-
$ rad patch ready --undo 97e18f8598237a396a1c0ac1509c89028e666c97 --no-announce
-
$ rad patch show 97e18f8598237a396a1c0ac1509c89028e666c97
+
$ rad patch ready --undo acee9948a4ff68e49a678734e8a0b86ff29f2e40 --no-announce
+
$ rad patch show acee9948a4ff68e49a678734e8a0b86ff29f2e40
╭────────────────────────────────────────────────────╮
│ Title     Nothing yet                              │
-
│ Patch     97e18f8598237a396a1c0ac1509c89028e666c97 │
+
│ Patch     acee9948a4ff68e49a678734e8a0b86ff29f2e40 │
│ Author    alice (you)                              │
│ Head      2a465832b5a76abe25be44a3a5d224bbd7741ba7 │
│ Branches  cloudhead/draft                          │
modified radicle-cli/examples/rad-patch-edit.md
@@ -16,8 +16,8 @@ $ git commit --message "Add README, just for the fun"

``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun" HEAD:refs/patches
-
✓ Patch 89f7afb1511b976482b21f6b2f39aef7f4fb88a2 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch f699e2299e9ee734758626924df7e15fd9a68553 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -32,22 +32,22 @@ $ git commit -v -m "Define the LICENSE"

``` (stderr)
$ git push -f -o patch.message="Add License"
-
✓ Patch 89f7afb updated to revision 5d78dd5376453e25df5988ec86951c99cb73742c
-
To compare against your previous revision 89f7afb, run:
+
✓ Patch f699e22 updated to revision 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f
+
To compare against your previous revision f699e22, run:

   git range-diff f2de534[..] 03c02af[..] 8945f61[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   03c02af..8945f61  changes -> patches/89f7afb1511b976482b21f6b2f39aef7f4fb88a2
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   03c02af..8945f61  changes -> patches/f699e2299e9ee734758626924df7e15fd9a68553
```

Let's look at the patch, to see what it looks like before editing it:

```
-
$ rad patch show 89f7afb
+
$ rad patch show f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add README, just for the fun                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
@@ -58,7 +58,7 @@ $ rad patch show 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -66,11 +66,11 @@ 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
+
$ rad patch edit f699e22 --message "Add Metadata" --message "Add README & LICENSE" --no-announce
+
$ rad patch show f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
@@ -83,7 +83,7 @@ $ rad patch show 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -94,11 +94,11 @@ If we want to change a specific revision's description, we can use the
`--revision` option:

```
-
$ rad patch edit 89f7afb --revision 5d78dd5 --message "Changes: Adds LICENSE file" --no-announce
-
$ rad patch show 89f7afb
+
$ rad patch edit f699e22 --revision 13b5240 --message "Changes: Adds LICENSE file" --no-announce
+
$ rad patch show f699e22
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add Metadata                                              │
-
│ Patch     89f7afb1511b976482b21f6b2f39aef7f4fb88a2                  │
+
│ Patch     f699e2299e9ee734758626924df7e15fd9a68553                  │
│ Author    alice (you)                                               │
│ Head      8945f6189adf027892c85ac57f7e9341049c2537                  │
│ Branches  changes                                                   │
@@ -111,7 +111,7 @@ $ rad patch show 89f7afb
│ 03c02af Add README, just for the fun                                │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (03c02af) now                               │
-
│ ↑ updated to 5d78dd5376453e25df5988ec86951c99cb73742c (8945f61) now │
+
│ ↑ updated to 13b5240e3f7ecb60fea4f66eb1a09fa3ffc1de7f (8945f61) now │
╰─────────────────────────────────────────────────────────────────────╯
```

modified radicle-cli/examples/rad-patch-fetch-1.md
@@ -20,9 +20,9 @@ $ git fetch --all
Fetching rad
Fetching alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ cat .git/FETCH_HEAD
-
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354		branch 'master' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
7461703ce0fda972df450d071d1d3702057a6352	not-for-merge	branch 'alice/1' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	not-for-merge	branch 'master' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354		branch 'master' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
7461703ce0fda972df450d071d1d3702057a6352	not-for-merge	branch 'alice/1' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	not-for-merge	branch 'master' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ git merge FETCH_HEAD
Already up to date.
$ git rev-parse master
modified radicle-cli/examples/rad-patch-fetch-2.md
@@ -12,7 +12,7 @@ $ git push rad -o patch.message="Changes" HEAD:refs/patches
$ git checkout master -q
$ git branch -D alice/1 -q
$ git update-ref -d refs/remotes/rad/alice/1
-
$ git update-ref -d refs/remotes/rad/patches/5e2dedcc5d515fcbc1cca483d3376609fe889bfb
+
$ git update-ref -d refs/remotes/rad/patches/819af456d77847724bcaed7e1c999ffd398e499e
$ git gc --prune=now
$ git branch -r
  rad/master
@@ -23,5 +23,5 @@ $ git pull
Already up to date.
$ git branch -r
  rad/master
-
  rad/patches/5e2dedcc5d515fcbc1cca483d3376609fe889bfb
+
  rad/patches/819af456d77847724bcaed7e1c999ffd398e499e
```
modified radicle-cli/examples/rad-patch-merge-draft.md
@@ -4,8 +4,8 @@ Let's start by creating a draft patch.
$ git checkout -b feature/1 -q
$ git commit --allow-empty -q -m "First change"
$ git push -o patch.draft rad HEAD:refs/patches
-
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe drafted
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 304d0a97bf9c4dadd4c732196a0f68dcfb2b6738 drafted
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -13,8 +13,8 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
$ git checkout master -q
$ git merge feature/1
$ git push rad master
-
✓ Patch 8dfb4dcafc4346158c8160410dd3f2b0616ad4fe merged
-
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 304d0a97bf9c4dadd4c732196a0f68dcfb2b6738 merged
+
✓ Canonical head for refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..20aa5dd  master -> master
```
modified radicle-cli/examples/rad-patch-open-explore.md
@@ -4,12 +4,12 @@ When preferred seeds are configured, opening a patch outputs the patch URL.
$ git checkout -b changes -q
$ git commit --allow-empty -q -m "Changes"
$ git push rad HEAD:refs/patches
-
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 opened
+
✓ Patch c47f80b133e5c0b930cbd21890e4fc535c854c16 opened
✓ Synced with 1 node(s)

-
  https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/acab0ec777a97d013f30be5d5d1aec32562ecb02
+
  https://app.radicle.xyz/nodes/[..]/rad:zPHvWyMMwBBH24oGGtkndq9wZmDC/patches/c47f80b133e5c0b930cbd21890e4fc535c854c16

-
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
To rad://zPHvWyMMwBBH24oGGtkndq9wZmDC/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
 * [new reference]   HEAD -> refs/patches
```

@@ -18,17 +18,17 @@ If we update the patch, the URL is also output.
``` (stderr)
$ git commit --amend --allow-empty -q -m "Other changes"
$ git push -f
-
✓ Patch acab0ec updated to revision f7a830d829d0cdf398f63a32b0d5ee31f08e21ab
-
To compare against your previous revision acab0ec, run:
+
✓ Patch c47f80b updated to revision 9c9e16fdeac971460cff8d4dbd4fbfd651bc1e72
+
To compare against your previous revision c47f80b, run:

   git range-diff f2de534[..] e12525d[..] b2b6432[..]

✓ Synced with 1 node(s)

-
  https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/acab0ec777a97d013f30be5d5d1aec32562ecb02
+
  https://app.radicle.xyz/nodes/[..]/rad:zPHvWyMMwBBH24oGGtkndq9wZmDC/patches/c47f80b133e5c0b930cbd21890e4fc535c854c16

-
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
 + e12525d...b2b6432 changes -> patches/acab0ec777a97d013f30be5d5d1aec32562ecb02 (forced update)
+
To rad://zPHvWyMMwBBH24oGGtkndq9wZmDC/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
 + e12525d...b2b6432 changes -> patches/c47f80b133e5c0b930cbd21890e4fc535c854c16 (forced update)
```

While simply pushing a commit outputs a URL to the new source tree.
@@ -37,12 +37,12 @@ While simply pushing a commit outputs a URL to the new source tree.
$ git checkout master -q
$ git merge changes -q
$ git push rad master
-
✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged
-
✓ Canonical head updated to b2b6432af93f8fe188e32d400263021b602cfec8
+
✓ Patch c47f80b133e5c0b930cbd21890e4fc535c854c16 merged
+
✓ Canonical head for refs/heads/master updated to b2b6432af93f8fe188e32d400263021b602cfec8
✓ Synced with 1 node(s)

-
  https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
+
  https://app.radicle.xyz/nodes/[..]/rad:zPHvWyMMwBBH24oGGtkndq9wZmDC/tree/b2b6432af93f8fe188e32d400263021b602cfec8

-
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
To rad://zPHvWyMMwBBH24oGGtkndq9wZmDC/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
   f2de534..b2b6432  master -> master
```
modified radicle-cli/examples/rad-patch-pull-update.md
@@ -9,7 +9,7 @@ Initializing public radicle 👾 repository in [..]

✓ Repository heartwood created.

-
Your Repository ID (RID) is rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK.
+
Your Repository ID (RID) is rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg.
You can show it any time by running `rad .` from this directory.

✓ Repository successfully announced to the network.
@@ -21,9 +21,9 @@ To push changes, run `git push`.
```

``` ~bob
-
$ rad clone rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK
-
✓ Seeding policy updated for rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK with scope 'all'
-
✓ Fetching rad:zhbMU4DUXrzB8xT6qAJh6yZ7bFMK from z6MknSL…StBU8Vi@[..]..
+
$ rad clone rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg
+
✓ Seeding policy updated for rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg with scope 'all'
+
✓ Fetching rad:z3Lr338KCqbiwiLSh9DQZxTiLQUHg from z6MknSL…StBU8Vi@[..]..
✓ Creating checkout in ./heartwood..
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
@@ -43,7 +43,7 @@ our fork:
$ cd heartwood
$ git push rad master
✓ Synced with 1 node(s)
-
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
To rad://z3Lr338KCqbiwiLSh9DQZxTiLQUHg/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
 * [new branch]      master -> master
```

@@ -53,22 +53,22 @@ Bob then opens a patch.
$ git checkout -b bob/feature -q
$ git commit --allow-empty -m "Bob's commit #1" -q
$ git push rad -o sync -o patch.message="Bob's patch" HEAD:refs/patches
-
✓ Patch 55b9721ed7f6bfec38f43729e9b6631c5dc812fb opened
+
✓ Patch f4563fc729c1361df8040cc26fd7bc7cf51a81fc opened
✓ Synced with 1 node(s)
-
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
To rad://z3Lr338KCqbiwiLSh9DQZxTiLQUHg/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
 * [new reference]   HEAD -> refs/patches
```
``` ~bob
$ git status --short --branch
-
## bob/feature...rad/patches/55b9721ed7f6bfec38f43729e9b6631c5dc812fb
+
## bob/feature...rad/patches/f4563fc729c1361df8040cc26fd7bc7cf51a81fc
```

Alice checks it out.

``` ~alice
-
$ rad patch checkout 55b9721ed7f6bfec38f43729e9b6631c5dc812fb
-
✓ Switched to branch patch/55b9721 at revision 55b9721
-
✓ Branch patch/55b9721 setup to track rad/patches/55b9721ed7f6bfec38f43729e9b6631c5dc812fb
+
$ rad patch checkout f4563fc729c1361df8040cc26fd7bc7cf51a81fc
+
✓ Switched to branch patch/f4563fc at revision f4563fc
+
✓ Branch patch/f4563fc setup to track rad/patches/f4563fc729c1361df8040cc26fd7bc7cf51a81fc
$ git show
commit bdcdb30b3c0f513620dd0f1c24ff8f4f71de956b
Author: radicle <radicle@localhost>
@@ -82,23 +82,23 @@ Bob then updates the patch.
``` ~bob (stderr)
$ git commit --allow-empty -m "Bob's commit #2" -q
$ git push rad -o sync -o patch.message="Updated."
-
✓ Patch 55b9721 updated to revision f91e056da05b2d9a58af1160c76245bc3debf7a8
-
To compare against your previous revision 55b9721, run:
+
✓ Patch f4563fc updated to revision ae9105fdd4c9d91ea920f8a651e088a3bbdab830
+
To compare against your previous revision f4563fc, run:

   git range-diff f2de534[..] bdcdb30[..] cad2666[..]

✓ Synced with 1 node(s)
-
To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
   bdcdb30..cad2666  bob/feature -> patches/55b9721ed7f6bfec38f43729e9b6631c5dc812fb
+
To rad://z3Lr338KCqbiwiLSh9DQZxTiLQUHg/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
   bdcdb30..cad2666  bob/feature -> patches/f4563fc729c1361df8040cc26fd7bc7cf51a81fc
```

Alice pulls the update.

``` ~alice
-
$ rad patch show 55b9721
+
$ rad patch show f4563fc
╭─────────────────────────────────────────────────────────────────────╮
│ Title    Bob's patch                                                │
-
│ Patch    55b9721ed7f6bfec38f43729e9b6631c5dc812fb                   │
+
│ Patch    f4563fc729c1361df8040cc26fd7bc7cf51a81fc                   │
│ Author   bob z6Mkt67…v4N1tRk                                        │
│ Head     cad2666a8a2250e4dee175ed5044be2c251ff08b                   │
│ Commits  ahead 2, behind 0                                          │
@@ -108,16 +108,16 @@ $ rad patch show 55b9721
│ bdcdb30 Bob's commit #1                                             │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by bob z6Mkt67…v4N1tRk (bdcdb30) now                       │
-
│ ↑ updated to f91e056da05b2d9a58af1160c76245bc3debf7a8 (cad2666) now │
+
│ ↑ updated to ae9105fdd4c9d91ea920f8a651e088a3bbdab830 (cad2666) now │
╰─────────────────────────────────────────────────────────────────────╯
$ git ls-remote rad
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	refs/heads/master
-
cad2666a8a2250e4dee175ed5044be2c251ff08b	refs/heads/patches/55b9721ed7f6bfec38f43729e9b6631c5dc812fb
+
cad2666a8a2250e4dee175ed5044be2c251ff08b	refs/heads/patches/f4563fc729c1361df8040cc26fd7bc7cf51a81fc
```
``` ~alice
$ git fetch rad
$ git status --short --branch
-
## patch/55b9721...rad/patches/55b9721ed7f6bfec38f43729e9b6631c5dc812fb [behind 1]
+
## patch/f4563fc...rad/patches/f4563fc729c1361df8040cc26fd7bc7cf51a81fc [behind 1]
```
``` ~alice
$ git pull
modified radicle-cli/examples/rad-patch-revert-merge.md
@@ -4,26 +4,26 @@ Let's create a patch, merge it and then revert it.
$ git checkout -b feature/1 -q
$ git commit --allow-empty -q -m "First change"
$ git push rad HEAD:refs/patches
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
$ git checkout master
Switched to branch 'master'
$ git merge feature/1
$ git push rad master
-
✓ Patch 696ec5508494692899337afe6713fe1796d0315c merged
-
✓ Canonical head updated to 20aa5dde6210796c3a2f04079b42316a31d02689
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 merged
+
✓ Canonical head for refs/heads/master updated to 20aa5dde6210796c3a2f04079b42316a31d02689
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..20aa5dd  master -> master
```

First we see the patch as merged.

```
-
$ rad patch show 696ec5508494692899337afe6713fe1796d0315c
+
$ rad patch show 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084
╭────────────────────────────────────────────────────────────────╮
│ Title     First change                                         │
-
│ Patch     696ec5508494692899337afe6713fe1796d0315c             │
+
│ Patch     09a3de4ac2c4d012c4a9c84c0cb306a066a0b084             │
│ Author    alice (you)                                          │
│ Head      20aa5dde6210796c3a2f04079b42316a31d02689             │
│ Branches  feature/1, master                                    │
@@ -33,7 +33,7 @@ $ rad patch show 696ec5508494692899337afe6713fe1796d0315c
│ 20aa5dd First change                                           │
├────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (20aa5dd) now                          │
-
│   └─ ✓ merged by alice (you) at revision 696ec55 (20aa5dd) now │
+
│   └─ ✓ merged by alice (you) at revision 09a3de4 (20aa5dd) now │
╰────────────────────────────────────────────────────────────────╯
```

@@ -49,19 +49,19 @@ When pushing, notice that we're told our patch is reverted.

``` (stderr) RAD_SOCKET=/dev/null
$ git push rad master --force
-
! Patch 696ec5508494692899337afe6713fe1796d0315c reverted at revision 696ec55
-
✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
! Patch 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 reverted at revision 09a3de4
+
✓ Canonical head for refs/heads/master updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 + 20aa5dd...f2de534 master -> master (forced update)
```

The patch shows up as open again.

```
-
$ rad patch show 696ec5508494692899337afe6713fe1796d0315c
+
$ rad patch show 09a3de4ac2c4d012c4a9c84c0cb306a066a0b084
╭────────────────────────────────────────────────────╮
│ Title     First change                             │
-
│ Patch     696ec5508494692899337afe6713fe1796d0315c │
+
│ Patch     09a3de4ac2c4d012c4a9c84c0cb306a066a0b084 │
│ Author    alice (you)                              │
│ Head      20aa5dde6210796c3a2f04079b42316a31d02689 │
│ Branches  feature/1                                │
modified radicle-cli/examples/rad-patch-update.md
@@ -6,16 +6,16 @@ $ git commit -q -m "Not a real change" --allow-empty
```
``` (stderr)
$ git push rad HEAD:refs/patches
-
✓ Patch b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch e5ae577cdccf08de7dde10f7c136c75e5fa17633 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

```
-
$ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
+
$ rad patch show e5ae577cdccf08de7dde10f7c136c75e5fa17633
╭────────────────────────────────────────────────────╮
│ Title     Not a real change                        │
-
│ Patch     b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5 │
+
│ Patch     e5ae577cdccf08de7dde10f7c136c75e5fa17633 │
│ Author    alice (you)                              │
│ Head      51b2f0f77b9849bfaa3e9d3ff68ee2f57771d20c │
│ Branches  feature/1                                │
@@ -46,17 +46,17 @@ Now, instead of using `git push` to update the patch, as we normally would,
we run:

```
-
$ rad patch update b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5 -m "Updated patch" --no-announce
-
ea7def3857f62f404606d7cd6490cd0de4eaebd1
+
$ rad patch update e5ae577cdccf08de7dde10f7c136c75e5fa17633 -m "Updated patch" --no-announce
+
d65bde5c0374b4488406c75a3fbef395067726fb
```

The command outputs the new Revision ID, which we can now see here:

```
-
$ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
+
$ rad patch show e5ae577cdccf08de7dde10f7c136c75e5fa17633
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Not a real change                                         │
-
│ Patch     b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5                  │
+
│ Patch     e5ae577cdccf08de7dde10f7c136c75e5fa17633                  │
│ Author    alice (you)                                               │
│ Head      4d272148458a17620541555b1f0905c01658aa9f                  │
│ Branches  feature/1                                                 │
@@ -67,6 +67,6 @@ $ rad patch show b6a23eb08656de0ef1fcc0b5fe8820841e5cb2e5
│ 51b2f0f Not a real change                                           │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (51b2f0f) now                               │
-
│ ↑ updated to ea7def3857f62f404606d7cd6490cd0de4eaebd1 (4d27214) now │
+
│ ↑ updated to d65bde5c0374b4488406c75a3fbef395067726fb (4d27214) now │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-patch-via-push.md
@@ -8,21 +8,21 @@ $ git checkout -b feature/1
Switched to a new branch 'feature/1'
$ git commit -a -m "Add things" -q --allow-empty
$ git push -o patch.message="Add things #1" -o patch.message="See commits for details." rad HEAD:refs/patches
-
✓ Patch 6035d2f582afbe01ff23ea87528ae523d76875b6 opened
-
hint: to update, run `git push` or `git push rad -f HEAD:patches/6035d2f582afbe01ff23ea87528ae523d76875b6`
+
✓ Patch a183e324b82e94c548eb43b7acb7c7d92ebe7761 opened
+
hint: to update, run `git push` or `git push rad -f HEAD:patches/a183e324b82e94c548eb43b7acb7c7d92ebe7761`
hint: offline push, your node is not running
hint: to sync with the network, run `rad node start`
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

We can see a patch was created:

```
-
$ rad patch show 6035d2f582afbe01ff23ea87528ae523d76875b6
+
$ rad patch show a183e324b82e94c548eb43b7acb7c7d92ebe7761
╭────────────────────────────────────────────────────╮
│ Title     Add things #1                            │
-
│ Patch     6035d2f582afbe01ff23ea87528ae523d76875b6 │
+
│ Patch     a183e324b82e94c548eb43b7acb7c7d92ebe7761 │
│ Author    alice (you)                              │
│ Head      42d894a83c9c356552a57af09ccdbd5587a99045 │
│ Branches  feature/1                                │
@@ -42,7 +42,7 @@ branch associated with this patch:

```
$ git branch -vv
-
* feature/1 42d894a [rad/patches/6035d2f582afbe01ff23ea87528ae523d76875b6] Add things
+
* feature/1 42d894a [rad/patches/a183e324b82e94c548eb43b7acb7c7d92ebe7761] Add things
  master    f2de534 [rad/master] Second commit
```

@@ -50,7 +50,7 @@ Let's check that it's up to date with our local head:

```
$ git status --short --branch
-
## feature/1...rad/patches/6035d2f582afbe01ff23ea87528ae523d76875b6
+
## feature/1...rad/patches/a183e324b82e94c548eb43b7acb7c7d92ebe7761
$ git fetch
$ git push
```
@@ -62,14 +62,14 @@ $ git show-ref
42d894a83c9c356552a57af09ccdbd5587a99045 refs/heads/feature/1
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/remotes/rad/master
-
42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/6035d2f582afbe01ff23ea87528ae523d76875b6
+
42d894a83c9c356552a57af09ccdbd5587a99045 refs/remotes/rad/patches/a183e324b82e94c548eb43b7acb7c7d92ebe7761
```
```
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji 'refs/heads/patches/*'
-
42d894a83c9c356552a57af09ccdbd5587a99045	refs/heads/patches/6035d2f582afbe01ff23ea87528ae523d76875b6
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/cobs/*'
-
0656c217f917c3e06234771e9ecae53aba5e173e	refs/cobs/xyz.radicle.id/0656c217f917c3e06234771e9ecae53aba5e173e
-
6035d2f582afbe01ff23ea87528ae523d76875b6	refs/cobs/xyz.radicle.patch/6035d2f582afbe01ff23ea87528ae523d76875b6
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 'refs/heads/patches/*'
+
42d894a83c9c356552a57af09ccdbd5587a99045	refs/heads/patches/a183e324b82e94c548eb43b7acb7c7d92ebe7761
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/cobs/*'
+
eeb8b44890570ccf85db7f3cb2a475100a27408a	refs/cobs/xyz.radicle.id/eeb8b44890570ccf85db7f3cb2a475100a27408a
+
a183e324b82e94c548eb43b7acb7c7d92ebe7761	refs/cobs/xyz.radicle.patch/a183e324b82e94c548eb43b7acb7c7d92ebe7761
```

We can create another patch:
@@ -78,8 +78,8 @@ We can create another patch:
$ git checkout -b feature/2 -q master
$ git commit -a -m "Add more things" -q --allow-empty
$ git push rad HEAD:refs/patches
-
✓ Patch 95808913573cead52ad7b42c7b475260ec45c4b2 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch 9831feba4a7cef108346b32703023934b9265f66 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -87,8 +87,8 @@ We see both branches with upstreams now:

```
$ git branch -vv
-
  feature/1 42d894a [rad/patches/6035d2f582afbe01ff23ea87528ae523d76875b6] Add things
-
* feature/2 8b0ea80 [rad/patches/95808913573cead52ad7b42c7b475260ec45c4b2] Add more things
+
  feature/1 42d894a [rad/patches/a183e324b82e94c548eb43b7acb7c7d92ebe7761] Add things
+
* feature/2 8b0ea80 [rad/patches/9831feba4a7cef108346b32703023934b9265f66] Add more things
  master    f2de534 [rad/master] Second commit
```

@@ -99,8 +99,8 @@ $ rad patch
╭───────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title            Author         Reviews  Head     +   -   Updated │
├───────────────────────────────────────────────────────────────────────────────┤
-
│ ●  6035d2f  Add things #1    alice   (you)  -        42d894a  +0  -0  now     │
-
│ ●  9580891  Add more things  alice   (you)  -        8b0ea80  +0  -0  now     │
+
│ ●  9831feb  Add more things  alice   (you)  -        8b0ea80  +0  -0  now     │
+
│ ●  a183e32  Add things #1    alice   (you)  -        42d894a  +0  -0  now     │
╰───────────────────────────────────────────────────────────────────────────────╯
```

@@ -112,13 +112,13 @@ $ git commit -a -m "Improve code" -q --allow-empty

``` (stderr)
$ git push rad
-
✓ Patch 9580891 updated to revision d7040c6c97629c2b94f86fb639bebbff5de39697
-
To compare against your previous revision 9580891, run:
+
✓ Patch 9831feb updated to revision d5bf76a631d5b47eb80ab99014d5f9c170fa3421
+
To compare against your previous revision 9831feb, run:

   git range-diff f2de534[..] 8b0ea80[..] 02bef3f[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   8b0ea80..02bef3f  feature/2 -> patches/95808913573cead52ad7b42c7b475260ec45c4b2
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   8b0ea80..02bef3f  feature/2 -> patches/9831feba4a7cef108346b32703023934b9265f66
```

This last `git push` worked without specifying an upstream branch despite the
@@ -136,10 +136,10 @@ This allows for pushing to the remote patch branch without using the full
We can then see that the patch head has moved:

```
-
$ rad patch show 9580891
+
$ rad patch show 9831feb
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add more things                                           │
-
│ Patch     95808913573cead52ad7b42c7b475260ec45c4b2                  │
+
│ Patch     9831feba4a7cef108346b32703023934b9265f66                  │
│ Author    alice (you)                                               │
│ Head      02bef3fac41b2f98bb3c02b868a53ddfecb55b5f                  │
│ Branches  feature/2                                                 │
@@ -150,7 +150,7 @@ $ rad patch show 9580891
│ 8b0ea80 Add more things                                             │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (8b0ea80) now                               │
-
│ ↑ updated to d7040c6c97629c2b94f86fb639bebbff5de39697 (02bef3f) now │
+
│ ↑ updated to d5bf76a631d5b47eb80ab99014d5f9c170fa3421 (02bef3f) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -163,14 +163,14 @@ $ git rev-parse HEAD

```
$ git status --short --branch
-
## feature/2...rad/patches/95808913573cead52ad7b42c7b475260ec45c4b2
+
## feature/2...rad/patches/9831feba4a7cef108346b32703023934b9265f66
```

```
-
$ git rev-parse refs/remotes/rad/patches/95808913573cead52ad7b42c7b475260ec45c4b2
+
$ git rev-parse refs/remotes/rad/patches/9831feba4a7cef108346b32703023934b9265f66
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/95808913573cead52ad7b42c7b475260ec45c4b2
-
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f	refs/heads/patches/95808913573cead52ad7b42c7b475260ec45c4b2
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi refs/heads/patches/9831feba4a7cef108346b32703023934b9265f66
+
02bef3fac41b2f98bb3c02b868a53ddfecb55b5f	refs/heads/patches/9831feba4a7cef108346b32703023934b9265f66
```

## Force push
@@ -190,9 +190,9 @@ Now let's push to the patch head.

``` (stderr) (fail)
$ git push
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 ! [rejected]        feature/2 -> patches/95808913573cead52ad7b42c7b475260ec45c4b2 (non-fast-forward)
-
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 ! [rejected]        feature/2 -> patches/9831feba4a7cef108346b32703023934b9265f66 (non-fast-forward)
+
error: failed to push some refs to 'rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
hint: [..]
hint: [..]
hint: [..]
@@ -204,22 +204,22 @@ use `--force` to force the update.

``` (stderr)
$ git push --force
-
✓ Patch 9580891 updated to revision 670d02794aa05afd6e0851f4aa848bc87c4712c7
-
To compare against your previous revision d7040c6, run:
+
✓ Patch 9831feb updated to revision af84c8793b8eabbfc07f125c247334a388b143ea
+
To compare against your previous revision d5bf76a, run:

   git range-diff f2de534[..] 02bef3f[..] 9304dbc[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 + 02bef3f...9304dbc feature/2 -> patches/95808913573cead52ad7b42c7b475260ec45c4b2 (forced update)
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 + 02bef3f...9304dbc feature/2 -> patches/9831feba4a7cef108346b32703023934b9265f66 (forced update)
```

That worked. We can see the new revision if we call `rad patch show`:

```
-
$ rad patch show 9580891
+
$ rad patch show 9831feb
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Add more things                                           │
-
│ Patch     95808913573cead52ad7b42c7b475260ec45c4b2                  │
+
│ Patch     9831feba4a7cef108346b32703023934b9265f66                  │
│ Author    alice (you)                                               │
│ Head      9304dbc445925187994a7a93222a3f8bde73b785                  │
│ Branches  feature/2                                                 │
@@ -230,8 +230,8 @@ $ rad patch show 9580891
│ 8b0ea80 Add more things                                             │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (8b0ea80) now                               │
-
│ ↑ updated to d7040c6c97629c2b94f86fb639bebbff5de39697 (02bef3f) now │
-
│ ↑ updated to 670d02794aa05afd6e0851f4aa848bc87c4712c7 (9304dbc) now │
+
│ ↑ updated to d5bf76a631d5b47eb80ab99014d5f9c170fa3421 (02bef3f) now │
+
│ ↑ updated to af84c8793b8eabbfc07f125c247334a388b143ea (9304dbc) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -242,7 +242,7 @@ we should get an error:

``` (stderr) (fail)
$ git push rad master:refs/patches
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 ! [remote rejected] master -> refs/patches (patch commits are already included in the base branch)
-
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
+
error: failed to push some refs to 'rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
```
modified radicle-cli/examples/rad-patch.md
@@ -1,6 +1,6 @@
When contributing to another's project, it is common for the contribution to be
of many commits and involve a discussion with the project's maintainer.  This is supported
-
via Radicle's patches.
+
via Radicle's Patches.

Here we give a brief overview for using patches in our hypothetical car
scenario.  It turns out instructions containing the power requirements were
@@ -26,8 +26,8 @@ Once the code is ready, we open (or create) a patch with our changes for the pro

``` (stderr)
$ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
-
✓ Patch aa45913e757cacd46972733bddee5472c78fa32a opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch c90967c43719b916e0b5a8b5dafe353608f8a08a opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -38,14 +38,14 @@ $ rad patch
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
+
│ ●  c90967c  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```
```
-
$ rad patch show aa45913e757cacd46972733bddee5472c78fa32a -p
+
$ rad patch show c90967c43719b916e0b5a8b5dafe353608f8a08a -p
╭────────────────────────────────────────────────────╮
│ Title     Define power requirements                │
-
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a │
+
│ Patch     c90967c43719b916e0b5a8b5dafe353608f8a08a │
│ Author    alice (you)                              │
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33 │
│ Branches  flux-capacitor-power                     │
@@ -78,14 +78,14 @@ $ rad patch list --authored
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
+
│ ●  c90967c  Define power requirements  alice   (you)  -        3e674d1  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

We can also see that it set an upstream for our patch branch:
```
$ git branch -vv
-
* flux-capacitor-power 3e674d1 [rad/patches/aa45913e757cacd46972733bddee5472c78fa32a] Define power requirements
+
* flux-capacitor-power 3e674d1 [rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a] Define power requirements
  master               f2de534 [rad/master] Second commit
```

@@ -93,12 +93,12 @@ We can also label patches as well as assign DIDs to the patch to help
organise your workflow:

```
-
$ rad patch label aa45913 --add fun --no-announce
-
$ rad patch assign aa45913 --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --no-announce
-
$ rad patch show aa45913
+
$ rad patch label c90967c --add fun --no-announce
+
$ rad patch assign c90967c --add did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --no-announce
+
$ rad patch show c90967c
╭────────────────────────────────────────────────────╮
│ Title     Define power requirements                │
-
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a │
+
│ Patch     c90967c43719b916e0b5a8b5dafe353608f8a08a │
│ Author    alice (you)                              │
│ Labels    fun                                      │
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33 │
@@ -126,33 +126,33 @@ $ git commit --message "Add README, just for the fun"
```
``` (stderr)
$ git push rad -o patch.message="Add README, just for the fun"
-
✓ Patch aa45913 updated to revision 6e5a3b7b2ce27b32e7ccc2f0b3f4594897dde638
-
To compare against your previous revision aa45913, run:
+
✓ Patch c90967c updated to revision a7fe44ba5d9c2339b0e9731874791db375aeebbe
+
To compare against your previous revision c90967c, run:

   git range-diff f2de534[..] 3e674d1[..] 27857ec[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   3e674d1..27857ec  flux-capacitor-power -> patches/aa45913e757cacd46972733bddee5472c78fa32a
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   3e674d1..27857ec  flux-capacitor-power -> patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```

And let's leave a quick comment for our team:

```
-
$ rad patch comment aa45913 --message 'I cannot wait to get back to the 90s!' --no-announce
+
$ rad patch comment c90967c --message 'I cannot wait to get back to the 90s!' --no-announce
╭───────────────────────────────────────╮
-
│ alice (you) now 686ec1c               │
+
│ alice (you) now 055f0d2               │
│ I cannot wait to get back to the 90s! │
╰───────────────────────────────────────╯
-
$ rad patch comment aa45913 --message 'My favorite decade!' --reply-to 686ec1c -q --no-announce
-
f4336e42daf76342f787d574b5ee779d89d05c7a
+
$ rad patch comment c90967c --message 'My favorite decade!' --reply-to 055f0d2 -q --no-announce
+
84336c0ffd31e607839d2f4dd3389556dd766124
```

If we realize we made a mistake in the comment, we can go back and edit it:

```
-
$ rad patch comment aa45913 --edit 686ec1c --message 'I cannot wait to get back to the 80s!' --no-announce
+
$ rad patch comment c90967c --edit 055f0d2 --message 'I cannot wait to get back to the 80s!' --no-announce
╭───────────────────────────────────────╮
-
│ alice (you) now 686ec1c               │
+
│ alice (you) now 055f0d2               │
│ I cannot wait to get back to the 80s! │
╰───────────────────────────────────────╯
```
@@ -160,36 +160,36 @@ $ rad patch comment aa45913 --edit 686ec1c --message 'I cannot wait to get back
And if we really made a mistake, then we can redact the comment entirely:

```
-
$ rad patch comment aa45913 --redact f4336e4 --no-announce
-
✓ Redacted comment f4336e42daf76342f787d574b5ee779d89d05c7a
+
$ rad patch comment c90967c --redact 84336c0 --no-announce
+
✓ Redacted comment 84336c0ffd31e607839d2f4dd3389556dd766124
```

Now, let's checkout the patch that we just created:

```
-
$ rad patch checkout aa45913
-
✓ Switched to branch patch/aa45913 at revision 6e5a3b7
-
✓ Branch patch/aa45913 setup to track rad/patches/aa45913e757cacd46972733bddee5472c78fa32a
+
$ rad patch checkout c90967c
+
✓ Switched to branch patch/c90967c at revision a7fe44b
+
✓ Branch patch/c90967c setup to track rad/patches/c90967c43719b916e0b5a8b5dafe353608f8a08a
```

We can also add a review verdict as such:

```
-
$ rad patch review aa45913 --accept --no-message --no-announce
-
✓ Patch aa45913 accepted
+
$ rad patch review c90967c --accept --no-message --no-announce
+
✓ Patch c90967c accepted
```

Showing the patch list now will reveal the favorable verdict:

```
-
$ rad patch show aa45913
+
$ rad patch show c90967c
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define power requirements                                 │
-
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a                  │
+
│ Patch     c90967c43719b916e0b5a8b5dafe353608f8a08a                  │
│ Author    alice (you)                                               │
│ Labels    fun                                                       │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                  │
-
│ Branches  flux-capacitor-power, patch/aa45913                       │
+
│ Branches  flux-capacitor-power, patch/c90967c                       │
│ Commits   ahead 2, behind 0                                         │
│ Status    open                                                      │
│                                                                     │
@@ -199,29 +199,29 @@ $ rad patch show aa45913
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (3e674d1) now                               │
-
│ ↑ updated to 6e5a3b7b2ce27b32e7ccc2f0b3f4594897dde638 (27857ec) now │
+
│ ↑ updated to a7fe44ba5d9c2339b0e9731874791db375aeebbe (27857ec) now │
│   └─ ✓ accepted by alice (you) now                                  │
╰─────────────────────────────────────────────────────────────────────╯
$ rad patch list
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  aa45913  Define power requirements  alice   (you)  ✔        27857ec  +0  -0  now     │
+
│ ●  c90967c  Define power requirements  alice   (you)  ✔        27857ec  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

If you make a mistake on the patch description, you can always change it!

```
-
$ rad patch edit aa45913 --message "Define power requirements" --message "Add requirements file" --no-announce
-
$ rad patch show aa45913
+
$ rad patch edit c90967c --message "Define power requirements" --message "Add requirements file" --no-announce
+
$ rad patch show c90967c
╭─────────────────────────────────────────────────────────────────────╮
│ Title     Define power requirements                                 │
-
│ Patch     aa45913e757cacd46972733bddee5472c78fa32a                  │
+
│ Patch     c90967c43719b916e0b5a8b5dafe353608f8a08a                  │
│ Author    alice (you)                                               │
│ Labels    fun                                                       │
│ Head      27857ec9eb04c69cacab516e8bf4b5fd36090f66                  │
-
│ Branches  flux-capacitor-power, patch/aa45913                       │
+
│ Branches  flux-capacitor-power, patch/c90967c                       │
│ Commits   ahead 2, behind 0                                         │
│ Status    open                                                      │
│                                                                     │
@@ -231,7 +231,7 @@ $ rad patch show aa45913
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by alice (you) (3e674d1) now                               │
-
│ ↑ updated to 6e5a3b7b2ce27b32e7ccc2f0b3f4594897dde638 (27857ec) now │
+
│ ↑ updated to a7fe44ba5d9c2339b0e9731874791db375aeebbe (27857ec) now │
│   └─ ✓ accepted by alice (you) now                                  │
╰─────────────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-publish.md
@@ -15,7 +15,7 @@ public
The repository is now in our inventory:
```
$ rad node inventory
-
rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu
+
rad:z2gud85wgGxzN7MNvi8wDEBFqLqmT
```

If we try to publish again, we get an error:
@@ -34,11 +34,11 @@ repository private again __will not_ be replicated.

```
$ rad id update --visibility private --title "Privatise" --description "Reverting the rad publish event"
-
✓ Identity revision 774cc1e72641d97d7dc9377745b7f454a9171747 created
+
✓ Identity revision 26ed629bb7c835bd94537e8226ca359c53b32cd3 created
╭────────────────────────────────────────────────────────────────────────╮
│ Title    Privatise                                                     │
-
│ Revision 774cc1e72641d97d7dc9377745b7f454a9171747                      │
-
│ Blob     88f759a4d46e9535766fccec0cbfe1fed6160b1a                      │
+
│ Revision 26ed629bb7c835bd94537e8226ca359c53b32cd3                      │
+
│ Blob     792ab13be76827e022b71941582a1b6217e6368a                      │
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
│ State    accepted                                                      │
│ Quorum   yes                                                           │
@@ -48,8 +48,9 @@ $ rad id update --visibility private --title "Privatise" --description "Revertin
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,13 +1,16 @@
+
@@ -1,21 +1,24 @@
 {
+
   "version": 2,
   "payload": {
     "xyz.radicle.project": {
       "defaultBranch": "master",
@@ -60,10 +61,16 @@ $ rad id update --visibility private --title "Privatise" --description "Revertin
   "delegates": [
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
   ],
-
-  "threshold": 1
-
+  "threshold": 1,
+
   "canonicalRefs": {
+
     "rules": {
+
       "refs/heads/master": {
+
         "allow": "delegates",
+
         "threshold": 1
+
       }
+
     }
+
+  },
+  "visibility": {
+    "type": "private"
-
+  }
+
   }
 }
```
modified radicle-cli/examples/rad-push-and-pull-patches.md
@@ -12,34 +12,40 @@ $ git checkout -b alice/1 -q
$ git rev-parse HEAD
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
$ git checkout master -q
-
$ rad patch checkout d004b67
-
✓ Switched to branch patch/d004b67 at revision d004b67
-
✓ Branch patch/d004b67 setup to track rad/patches/d004b67355456c46de10c0d287e4a791ad1a6945
+
$ rad patch
+
╭─────────────────────────────────────────────────────────────────────────────────╮
+
│ ●  ID       Title    Author                   Reviews  Head     +   -   Updated │
+
├─────────────────────────────────────────────────────────────────────────────────┤
+
│ ●  74aa72b  Changes  bob     z6Mkt67…v4N1tRk  -        8d5f1ba  +0  -0  now     │
+
╰─────────────────────────────────────────────────────────────────────────────────╯
+
$ rad patch checkout 74aa72b
+
✓ Switched to branch patch/74aa72b at revision 74aa72b
+
✓ Branch patch/74aa72b setup to track rad/patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
$ git checkout master -q
$ cat .git/FETCH_HEAD
-
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	not-for-merge	branch 'master' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
8d5f1bae4b69d8e3f6cbfc6f4bd675ed19990afc	not-for-merge	branch 'patches/d004b67355456c46de10c0d287e4a791ad1a6945' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354	not-for-merge	branch 'master' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
8d5f1bae4b69d8e3f6cbfc6f4bd675ed19990afc	not-for-merge	branch 'patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
$ git rev-parse master
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
```

``` ~alice (stderr)
-
$ git checkout patch/d004b67 -q
+
$ git checkout patch/74aa72b -q
$ git commit --allow-empty -m "Changes #2" -q
$ git push
-
✓ Patch d004b67 updated to revision 2eb705c3da98e05c083df15be5b1bd6856a0bd77
-
To compare against your previous revision d004b67, run:
+
✓ Patch 74aa72b updated to revision 2bc70f5c1d567db16df991a10a618733f3e29d82
+
To compare against your previous revision 74aa72b, run:

   git range-diff f2de534[..] 8d5f1ba[..] c2aaf1c[..]

✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 * [new branch]      patch/d004b67 -> patches/d004b67355456c46de10c0d287e4a791ad1a6945
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 * [new branch]      patch/74aa72b -> patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5
```

``` ~bob
@@ -50,32 +56,32 @@ $ git push
``` ~alice (stderr)
$ git checkout master -q
$ git pull
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
 + c2aaf1c...8d5f1ba patches/d004b67355456c46de10c0d287e4a791ad1a6945 -> rad/patches/d004b67355456c46de10c0d287e4a791ad1a6945  (forced update)
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
 + c2aaf1c...8d5f1ba patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5 -> rad/patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5  (forced update)
$ git checkout - -q
$ git commit --allow-empty -m "Changes #3" -q
$ git push
-
✓ Patch d004b67 updated to revision 7b5015a8dac188bb0d44a334aa68a51298750b07
-
To compare against your previous revision d004b67, run:
+
✓ Patch 74aa72b updated to revision c541164492cae34700c601bbe5fdf068183a7d6f
+
To compare against your previous revision 74aa72b, run:

   git range-diff f2de534[..] 8d5f1ba[..] d9f8caf[..]

✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
   c2aaf1c..d9f8caf  patch/d004b67 -> patches/d004b67355456c46de10c0d287e4a791ad1a6945
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
   c2aaf1c..d9f8caf  patch/74aa72b -> patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5
```

``` ~alice
$ cat .git/FETCH_HEAD
-
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354		branch 'master' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
8d5f1bae4b69d8e3f6cbfc6f4bd675ed19990afc	not-for-merge	branch 'patches/d004b67355456c46de10c0d287e4a791ad1a6945' of rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354		branch 'master' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
8d5f1bae4b69d8e3f6cbfc6f4bd675ed19990afc	not-for-merge	branch 'patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5' of rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
```

``` ~bob (stderr)
$ git checkout master -q
$ git pull
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
 + c2aaf1c...8d5f1ba patches/d004b67355456c46de10c0d287e4a791ad1a6945 -> rad/patches/d004b67355456c46de10c0d287e4a791ad1a6945  (forced update)
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
 + c2aaf1c...8d5f1ba patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5 -> rad/patches/74aa72bbd79a88dcb5ae98bb6d06bb493b5ed4c5  (forced update)
```

``` ~bob
modified radicle-cli/examples/rad-review-by-hunk.md
@@ -61,8 +61,8 @@ $ git commit -q -m "Update files"

``` (stderr)
$ git push rad HEAD:refs/patches
-
✓ Patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
✓ Patch d34084970fdd4de9d8125165f5ac39ac70d3806c opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
 * [new reference]   HEAD -> refs/patches
```

@@ -70,7 +70,7 @@ Finally, we do a review of the patch by hunk. The output of this command should
match `git diff master -W100% -U5 --patience`:

```
-
$ rad patch review --patch -U5 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
+
$ rad patch review --patch -U5 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
╭──────────────────────╮
│ .gitignore ❲deleted❳ │
├──────────────────────┤
@@ -117,8 +117,8 @@ $ rad patch review --patch -U5 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-ann
Now let's accept these hunks one by one..

```
-
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
-
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
+
$ rad patch review --patch --accept --hunk 1 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
+
✓ Loaded existing review ([..]) for patch d34084970fdd4de9d8125165f5ac39ac70d3806c
╭──────────────────────╮
│ .gitignore ❲deleted❳ │
├──────────────────────┤
@@ -128,8 +128,8 @@ $ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1
✓ Updated brain to a5fccf0e977225ff13c3f74c43faf4cb679bf835
```
```
-
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
-
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
+
$ rad patch review --patch --accept --hunk 1 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
+
✓ Loaded existing review ([..]) for patch d34084970fdd4de9d8125165f5ac39ac70d3806c
╭──────────────────────────────────────────────────────────╮
│ DISCLAIMER.txt ❲created❳                                 │
├──────────────────────────────────────────────────────────┤
@@ -139,8 +139,8 @@ $ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1
✓ Updated brain to 2cdb82ea726e64d3b52847c7699d0d4759198f5c
```
```
-
$ rad patch review --patch --accept -U3 --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
-
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
+
$ rad patch review --patch --accept -U3 --hunk 1 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
+
✓ Loaded existing review ([..]) for patch d34084970fdd4de9d8125165f5ac39ac70d3806c
╭─────────────────────────────╮
│ MENU.txt                    │
├─────────────────────────────┤
@@ -155,8 +155,8 @@ $ rad patch review --patch --accept -U3 --hunk 1 7a2ac7e2841cc1e7394f99f107555a4
✓ Updated brain to d4aecbb859a802a3215def0b538358bf63593953
```
```
-
$ rad patch review --patch --accept -U3 --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
-
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
+
$ rad patch review --patch --accept -U3 --hunk 1 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
+
✓ Loaded existing review ([..]) for patch d34084970fdd4de9d8125165f5ac39ac70d3806c
╭───────────────────────────────────────╮
│ MENU.txt                              │
├───────────────────────────────────────┤
@@ -172,8 +172,8 @@ $ rad patch review --patch --accept -U3 --hunk 1 7a2ac7e2841cc1e7394f99f107555a4
```

```
-
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
-
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
+
$ rad patch review --patch --accept --hunk 1 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
+
✓ Loaded existing review ([..]) for patch d34084970fdd4de9d8125165f5ac39ac70d3806c
╭────────────────────────────────────────────────────╮
│ INSTRUCTIONS.txt -> notes/INSTRUCTIONS.txt ❲moved❳ │
╰────────────────────────────────────────────────────╯
@@ -181,7 +181,7 @@ $ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1
```

```
-
$ rad patch review --patch --accept --hunk 1 7a2ac7e2841cc1e7394f99f107555a499b1d3f23 --no-announce
-
✓ Loaded existing review ([..]) for patch 7a2ac7e2841cc1e7394f99f107555a499b1d3f23
+
$ rad patch review --patch --accept --hunk 1 d34084970fdd4de9d8125165f5ac39ac70d3806c --no-announce
+
✓ Loaded existing review ([..]) for patch d34084970fdd4de9d8125165f5ac39ac70d3806c
✓ All hunks have been reviewed
```
modified radicle-cli/examples/rad-seed-and-follow.md
@@ -21,8 +21,8 @@ $ rad follow
Now let's seed one of Eve's repositories:

```
-
$ rad seed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope followed --no-fetch
-
✓ Seeding policy updated for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji with scope 'followed'
+
$ rad seed rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --scope followed --no-fetch
+
✓ Seeding policy updated for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 with scope 'followed'
```

We can list the repositories we are seeding by omitting the RID:
@@ -32,6 +32,6 @@ $ rad seed
╭──────────────────────────────────────────────────────────────╮
│ Repository                          Name   Policy   Scope    │
├──────────────────────────────────────────────────────────────┤
-
│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji          allow    followed │
+
│ rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2          allow    followed │
╰──────────────────────────────────────────────────────────────╯
```
modified radicle-cli/examples/rad-seed-many.md
@@ -3,9 +3,9 @@ same time, where each repository specified will be fetched (unless `--no-fetch`
is used):

```
-
$ rad seed rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm
-
✓ Seeding policy updated for rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW with scope 'all'
-
✓ Fetching rad:z3Rry7rpdWuGpfjPYGzdJKQADsoNW from z6Mkt67…v4N1tRk@[..]
-
✓ Seeding policy updated for rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm with scope 'all'
-
✓ Fetching rad:z3zTnCfi6cVSZG8eCGn6AMDypgAPm from z6Mkt67…v4N1tRk@[..]
+
$ rad seed rad:z2eCRs3yG5orX2AqYiozcedMzbwg5 rad:z2kYoZsp3L6PuDD6s2dizTwX7p6jk
+
✓ Seeding policy updated for rad:z2eCRs3yG5orX2AqYiozcedMzbwg5 with scope 'all'
+
✓ Fetching rad:z2eCRs3yG5orX2AqYiozcedMzbwg5 from z6Mkt67…v4N1tRk@[..]
+
✓ Seeding policy updated for rad:z2kYoZsp3L6PuDD6s2dizTwX7p6jk with scope 'all'
+
✓ Fetching rad:z2kYoZsp3L6PuDD6s2dizTwX7p6jk from z6Mkt67…v4N1tRk@[..]
```
modified radicle-cli/examples/rad-sync.md
@@ -15,9 +15,9 @@ $ rad sync status --sort-by alias
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   Node                      Address                      Status        Tip       Timestamp │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   alice   (you)             alice.radicle.example:8776   unannounced   056b1db   [  ...  ] │
-
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     out-of-sync   99c5497   [  ...  ] │
-
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     out-of-sync   99c5497   [  ...  ] │
+
│ ●   alice   (you)             alice.radicle.example:8776   unannounced   0d6b305   [  ...  ] │
+
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     out-of-sync   7c1445c   [  ...  ] │
+
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     out-of-sync   7c1445c   [  ...  ] │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -37,9 +37,9 @@ $ rad sync status --sort-by alias
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   Node                      Address                      Status   Tip       Timestamp │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   alice   (you)             alice.radicle.example:8776            056b1db   [  ...  ] │
-
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     synced   056b1db   [  ...  ] │
-
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     synced   056b1db   [  ...  ] │
+
│ ●   alice   (you)             alice.radicle.example:8776            0d6b305   [  ...  ] │
+
│ ●   bob     z6Mkt67…v4N1tRk   bob.radicle.example:8776     synced   0d6b305   [  ...  ] │
+
│ ●   eve     z6Mkux1…nVhib7Z   eve.radicle.example:8776     synced   0d6b305   [  ...  ] │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -55,8 +55,8 @@ We can also use the `--fetch` option to only fetch objects:

```
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Fetched repository from 2 seed(s)
```

@@ -64,8 +64,8 @@ Specifying both `--fetch` and `--announce` is equivalent to specifying none:

```
$ rad sync --fetch --announce
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Fetched repository from 2 seed(s)
✓ Nothing to announce, already in sync with 2 node(s) (see `rad sync status`)
```
@@ -74,7 +74,7 @@ It's also possible to use the `--seed` flag to only sync with a specific node:

```
$ rad sync --fetch --seed z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Fetched repository from 1 seed(s)
```

@@ -87,7 +87,7 @@ $ rad issue open --title "Test `rad sync --replicas`" --description "Check that

```
$ rad sync --replicas 1
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkux1…nVhib7Z@[..]..
✓ Fetched repository from 1 seed(s)
✓ Synced with 1 node(s)
```
modified radicle-cli/examples/rad-unseed-many.md
@@ -8,15 +8,15 @@ $ rad ls
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
-
│ nixpkgs     rad:zyFFr2iwoTEfNF4jGNZHuoy7odMh    public       f2de534   Home for Nix Packages              │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       f2de534   Radicle Heartwood Protocol & Stack │
+
│ nixpkgs     rad:z3rK5Ldp958XdzwL88vYRvhdQj5WR   public       f2de534   Home for Nix Packages              │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

We could stop seeding them if we didn't want other nodes to fetch them from us:

```
-
$ rad unseed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji rad:zyFFr2iwoTEfNF4jGNZHuoy7odMh
-
✓ Seeding policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji removed
-
✓ Seeding policy for rad:zyFFr2iwoTEfNF4jGNZHuoy7odMh removed
+
$ rad unseed rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 rad:z3rK5Ldp958XdzwL88vYRvhdQj5WR
+
✓ Seeding policy for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 removed
+
✓ Seeding policy for rad:z3rK5Ldp958XdzwL88vYRvhdQj5WR removed
```
modified radicle-cli/examples/rad-unseed.md
@@ -5,15 +5,15 @@ $ rad ls
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   public       f2de534   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   public       f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

We could stop seeding it if we didn't want other nodes to fetch it from us:

```
-
$ rad unseed rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
-
✓ Seeding policy for rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji removed
+
$ rad unseed rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
+
✓ Seeding policy for rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 removed
```

Now, if we run `rad ls`, we see it's gone:
@@ -32,7 +32,7 @@ $ rad ls --all
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Name        RID                                 Visibility   Head      Description                        │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────┤
-
│ heartwood   rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji   local        f2de534   Radicle Heartwood Protocol & Stack │
+
│ heartwood   rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2   local        f2de534   Radicle Heartwood Protocol & Stack │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

modified radicle-cli/examples/rad-watch.md
@@ -16,5 +16,5 @@ $ git push rad master
```

``` ~bob
-
$ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --ref 'refs/heads/master' --target e09c4dc1b54443ceea715ea648afecdcfd1dd7d0 --interval 500
+
$ rad watch --repo rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 --node z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi --ref 'refs/heads/master' --target e09c4dc1b54443ceea715ea648afecdcfd1dd7d0 --interval 500
```
modified radicle-cli/examples/workflow/3-issues.md
@@ -7,7 +7,7 @@ Let's say the new car you are designing with your peers has a problem with its f
$ rad issue open --title "flux capacitor underpowered" --description "Flux capacitor power requirements exceed current supply" --no-announce
╭─────────────────────────────────────────────────────────╮
│ Title   flux capacitor underpowered                     │
-
│ Issue   9037b7a42323d4b79e6a48b7d05d3bbaae11d69b        │
+
│ Issue   3b2f7e674bc39d5ff93abf2c68d8233fa4aa8806        │
│ Author  bob (you)                                       │
│ Status  open                                            │
│                                                         │
@@ -22,7 +22,7 @@ $ rad issue list
╭──────────────────────────────────────────────────────────────────────────────────────────╮
│ ●   ID        Title                         Author           Labels   Assignees   Opened │
├──────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●   9037b7a   flux capacitor underpowered   bob      (you)                        now    │
+
│ ●   3b2f7e6   flux capacitor underpowered   bob      (you)                        now    │
╰──────────────────────────────────────────────────────────────────────────────────────────╯
```

@@ -31,6 +31,6 @@ found an important detail about the car's power requirements. It will help
whoever works on a fix.

```
-
$ rad issue comment 9037b7a42323d4b79e6a48b7d05d3bbaae11d69b --message 'The flux capacitor needs 1.21 Gigawatts' -q --no-announce
-
400cb155f512b4880858bb05f935104c34167b28
+
$ rad issue comment 3b2f7e674bc39d5ff93abf2c68d8233fa4aa8806 --message 'The flux capacitor needs 1.21 Gigawatts' -q --no-announce
+
3a4261f49c44a5832c7f18179d00cffa0deb17f9
```
modified radicle-cli/examples/workflow/4-patching-contributor.md
@@ -26,8 +26,8 @@ Once the code is ready, we open a patch with our changes.

``` (stderr)
$ git push rad -o no-sync -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches
-
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 opened
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
✓ Patch 3aa3bbfbc4162e34ab6787b3508e7ec84166d182 opened
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
 * [new reference]   HEAD -> refs/patches
```

@@ -38,12 +38,12 @@ $ rad patch
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ ●  ID       Title                      Author         Reviews  Head     +   -   Updated │
├─────────────────────────────────────────────────────────────────────────────────────────┤
-
│ ●  e4934b6  Define power requirements  bob     (you)  -        3e674d1  +0  -0  now     │
+
│ ●  3aa3bbf  Define power requirements  bob     (you)  -        3e674d1  +0  -0  now     │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
-
$ rad patch show e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
+
$ rad patch show 3aa3bbfbc4162e34ab6787b3508e7ec84166d182
╭────────────────────────────────────────────────────╮
│ Title     Define power requirements                │
-
│ Patch     e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 │
+
│ Patch     3aa3bbfbc4162e34ab6787b3508e7ec84166d182 │
│ Author    bob (you)                                │
│ Head      3e674d1a1df90807e934f9ae5da2591dd6848a33 │
│ Branches  flux-capacitor-power                     │
@@ -61,8 +61,8 @@ $ rad patch show e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
We can also confirm that the patch branch is in storage:

```
-
$ git ls-remote rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk refs/heads/patches/*
-
3e674d1a1df90807e934f9ae5da2591dd6848a33	refs/heads/patches/e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
+
$ git ls-remote rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk refs/heads/patches/*
+
3e674d1a1df90807e934f9ae5da2591dd6848a33	refs/heads/patches/3aa3bbfbc4162e34ab6787b3508e7ec84166d182
```

Wait, let's add a README too! Just for fun.
@@ -77,19 +77,19 @@ $ git commit --message "Add README, just for the fun"
```
``` (stderr) RAD_SOCKET=/dev/null
$ git push -o patch.message="Add README, just for the fun"
-
✓ Patch e4934b6 updated to revision 773b9aab58b11e9fa83d0ed0baca2bea6ff889c9
-
To compare against your previous revision e4934b6, run:
+
✓ Patch 3aa3bbf updated to revision 8ea87be8cb7d590f381338348532200b230368af
+
To compare against your previous revision 3aa3bbf, run:

   git range-diff f2de534[..] 3e674d1[..] 27857ec[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
-
   3e674d1..27857ec  flux-capacitor-power -> patches/e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
   3e674d1..27857ec  flux-capacitor-power -> patches/3aa3bbfbc4162e34ab6787b3508e7ec84166d182
```

And let's leave a quick comment for our team:

```
-
$ rad patch comment e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 --message 'I cannot wait to get back to the 90s!' -q
-
8c66f87afadc7c7c857f8bb92973c25f64e75776
+
$ rad patch comment 3aa3bbfbc4162e34ab6787b3508e7ec84166d182 --message 'I cannot wait to get back to the 90s!' -q
+
528abde17e16bef2aa12157c745a9a74e4005051
✓ Synced with 1 node(s)
```
modified radicle-cli/examples/workflow/5-patching-maintainer.md
@@ -8,7 +8,7 @@ of this.
```
$ rad remote add z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob --sync --fetch
✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6Mkt67…v4N1tRk@[..]..
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
```
@@ -20,16 +20,16 @@ $ rad inbox --sort-by id
╭────────────────────────────────────────────────────────────────────────────╮
│ heartwood                                                                  │
├────────────────────────────────────────────────────────────────────────────┤
-
│ 001   ●   9037b7a   flux capacitor underpowered   issue   open   bob   now │
-
│ 002   ●   e4934b6   Define power requirements     patch   open   bob   now │
+
│ 001   ●   3b2f7e6   flux capacitor underpowered   issue   open   bob   now │
+
│ 002   ●   3aa3bbf   Define power requirements     patch   open   bob   now │
╰────────────────────────────────────────────────────────────────────────────╯
$ git branch -r
-
  bob/patches/e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
+
  bob/patches/3aa3bbfbc4162e34ab6787b3508e7ec84166d182
  rad/master
-
$ rad patch show e4934b6
+
$ rad patch show 3aa3bbf
╭─────────────────────────────────────────────────────────────────────╮
│ Title    Define power requirements                                  │
-
│ Patch    e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46                   │
+
│ Patch    3aa3bbfbc4162e34ab6787b3508e7ec84166d182                   │
│ Author   bob z6Mkt67…v4N1tRk                                        │
│ Head     27857ec9eb04c69cacab516e8bf4b5fd36090f66                   │
│ Commits  ahead 2, behind 0                                          │
@@ -41,7 +41,7 @@ $ rad patch show e4934b6
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by bob z6Mkt67…v4N1tRk (3e674d1) now                       │
-
│ ↑ updated to 773b9aab58b11e9fa83d0ed0baca2bea6ff889c9 (27857ec) now │
+
│ ↑ updated to 8ea87be8cb7d590f381338348532200b230368af (27857ec) now │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -51,35 +51,35 @@ way will tell others about the corrections we needed before merging the
changes.

```
-
$ rad patch checkout e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
-
✓ Switched to branch patch/e4934b6 at revision 773b9aa
-
✓ Branch patch/e4934b6 setup to track rad/patches/e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
+
$ rad patch checkout 3aa3bbfbc4162e34ab6787b3508e7ec84166d182
+
✓ Switched to branch patch/3aa3bbf at revision 8ea87be
+
✓ Branch patch/3aa3bbf setup to track rad/patches/3aa3bbfbc4162e34ab6787b3508e7ec84166d182
$ git mv REQUIREMENTS REQUIREMENTS.md
$ git commit -m "Use markdown for requirements"
-
[patch/e4934b6 f567f69] Use markdown for requirements
+
[patch/3aa3bbf f567f69] Use markdown for requirements
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename REQUIREMENTS => REQUIREMENTS.md (100%)
```
``` (stderr)
$ git push rad -o no-sync -o patch.message="Use markdown for requirements"
-
✓ Patch e4934b6 updated to revision 9d62420e779e5cfe1dc02c51eddec9a0907aa844
-
To compare against your previous revision 773b9aa, run:
+
✓ Patch 3aa3bbf updated to revision 83812f465f23c6f1262e4d526f52e5a5f02330a0
+
To compare against your previous revision 8ea87be, run:

   git range-diff f2de534[..] 27857ec[..] f567f69[..]

-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
-
 * [new branch]      patch/e4934b6 -> patches/e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
 * [new branch]      patch/3aa3bbf -> patches/3aa3bbfbc4162e34ab6787b3508e7ec84166d182
```

Great, all fixed up, lets accept and merge the code.

```
-
$ rad patch review e4934b6 --revision 9d62420 --accept
-
✓ Patch e4934b6 accepted
+
$ rad patch review 3aa3bbf --revision 83812f4 --accept
+
✓ Patch 3aa3bbf accepted
✓ Synced with 1 node(s)
$ git checkout master
Your branch is up to date with 'rad/master'.
-
$ git merge patch/e4934b6
+
$ git merge patch/3aa3bbf
Updating f2de534..f567f69
Fast-forward
 README.md       | 0
@@ -90,20 +90,20 @@ Fast-forward
```
``` (stderr)
$ git push rad master
-
✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420
-
✓ Canonical head updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
+
✓ Patch 3aa3bbfbc4162e34ab6787b3508e7ec84166d182 merged at revision 83812f4
+
✓ Canonical head for refs/heads/master updated to f567f695d25b4e8fb63b5f5ad2a584529826e908
✓ Synced with 1 node(s)
-
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
To rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..f567f69  master -> master
```

The patch is now merged and closed :).

```
-
$ rad patch show e4934b6
+
$ rad patch show 3aa3bbf
╭─────────────────────────────────────────────────────────────────────╮
│ Title    Define power requirements                                  │
-
│ Patch    e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46                   │
+
│ Patch    3aa3bbfbc4162e34ab6787b3508e7ec84166d182                   │
│ Author   bob z6Mkt67…v4N1tRk                                        │
│ Head     27857ec9eb04c69cacab516e8bf4b5fd36090f66                   │
│ Commits  ahead 0, behind 1                                          │
@@ -115,10 +115,10 @@ $ rad patch show e4934b6
│ 3e674d1 Define power requirements                                   │
├─────────────────────────────────────────────────────────────────────┤
│ ● opened by bob z6Mkt67…v4N1tRk (3e674d1) now                       │
-
│ ↑ updated to 773b9aab58b11e9fa83d0ed0baca2bea6ff889c9 (27857ec) now │
-
│ * revised by alice (you) in 9d62420 (f567f69) now                   │
+
│ ↑ updated to 8ea87be8cb7d590f381338348532200b230368af (27857ec) now │
+
│ * revised by alice (you) in 83812f4 (f567f69) now                   │
│   └─ ✓ accepted by alice (you) now                                  │
-
│   └─ ✓ merged by alice (you) at revision 9d62420 (f567f69) now      │
+
│   └─ ✓ merged by alice (you) at revision 83812f4 (f567f69) now      │
╰─────────────────────────────────────────────────────────────────────╯
```

@@ -132,7 +132,7 @@ Finally, we will close the issue that was opened for this
patch, marking it as solved:

```
-
$ rad issue state 9037b7a --solved
-
✓ Issue 9037b7a is now solved
+
$ rad issue state 3b2f7e6 --solved
+
✓ Issue 3b2f7e6 is now solved
✓ Synced with 1 node(s)
```
modified radicle-cli/examples/workflow/6-pulling-contributor.md
@@ -10,7 +10,7 @@ f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
Then, we call `rad sync --fetch` to fetch from the maintainer:
```
$ rad sync --fetch
-
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi@[..]..
+
✓ Fetching rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2 from z6MknSL…StBU8Vi@[..]..
✓ Fetched repository from 1 seed(s)
```

@@ -21,9 +21,9 @@ Your branch is up to date with 'rad/master'.
```
``` (stderr) RAD_SOCKET=/dev/null
$ git pull --all --ff
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2
   f2de534..f567f69  master     -> rad/master
-
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+
From rad://z3W5xAVWJ9Gc4LbN16mE3tjWX92t2/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
   f2de534..f567f69  master     -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

modified radicle-cli/src/commands.rs
@@ -12,6 +12,8 @@ pub mod rad_clone;
pub mod rad_cob;
#[path = "commands/config.rs"]
pub mod rad_config;
+
#[path = "commands/cref.rs"]
+
pub mod rad_cref;
#[path = "commands/debug.rs"]
pub mod rad_debug;
#[path = "commands/diff.rs"]
added radicle-cli/src/commands/cref.rs
@@ -0,0 +1,444 @@
+
use std::ffi::OsString;
+
use std::path::Path;
+

+
use anyhow::{anyhow, Context};
+

+
use nonempty::NonEmpty;
+
use radicle::cob;
+
use radicle::cob::identity::Identity;
+
use radicle::git::canonical::rules;
+
use radicle::git::canonical::rules::Rule;
+
use radicle::git::canonical::RawRule;
+
use radicle::git::canonical::Rules;
+
use radicle::git::Qualified;
+
use radicle::identity::Did;
+
use radicle::identity::IdentityMut;
+
use radicle::identity::RawDoc;
+
use radicle::identity::RepoId;
+
use radicle::prelude::Profile;
+
use radicle::storage::ReadStorage;
+
use radicle::storage::WriteRepository;
+
use radicle_term::Element as _;
+

+
use crate::id;
+
use crate::terminal as term;
+
use crate::terminal::args::{Args, Error, Help};
+
use crate::terminal::json;
+

+
pub const HELP: Help = Help {
+
    name: "cref",
+
    description: "Manage canonical reference rules",
+
    version: env!("RADICLE_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad cref list [<option>...]
+
    rad cref add <refspec> [--allow <did>..] [--threshold <num>] [<option>...]
+
    rad cref edit <refspec> [--allow <did>..] [--threshold <num>] [<option>...]
+
    rad cref remove <refspec> [<option>...]
+
    rad cref match <refname>
+

+
    The *rad cref* command is used to manage the rules for setting
+
    canonical references in the Radicle repository.
+

+
    The *list* command lists all the rules associated with the repository that
+
    match the given *refname*.
+

+
    The *add* command will add the rule for the given *refspec*.
+

+
    The *edit* command will amend the rule for the given *refspec*.
+

+
    The *remove* command will remove the rule for the given *refspec*.
+

+
    The *match* command evaluates all rules against given reference name,
+
    it prints the set of matched rules, in order of precedence, i.e. the
+
    first rule printed is the one that applies to the given reference name.
+

+
Add/Edit options
+

+
   --allow <did>           DID to be used for the canonical reference rule.
+
                           Can be used multiple times to specify more delegates.
+
                           If not specified the default 'delegates' token will be used.
+
   --threshold <num>       The threshold number of votes required to make a reference canonical (default: 1)
+

+
Options
+

+
    --repo <rid>           Repository (defaults to the current repository)
+
    --quiet, -q            Don't print anything
+
    --help                 Print help
+
"#,
+
};
+

+
pub enum Operation {
+
    Add {
+
        title: Option<String>,
+
        description: Option<String>,
+
        pattern: rules::Pattern,
+
        allow: rules::Allowed,
+
        threshold: usize,
+
    },
+
    Edit {
+
        title: Option<String>,
+
        description: Option<String>,
+
        pattern: rules::Pattern,
+
        allow: rules::Allowed,
+
        threshold: usize,
+
    },
+
    List,
+
    Match {
+
        refname: Qualified<'static>,
+
    },
+
    Remove {
+
        title: Option<String>,
+
        description: Option<String>,
+
        pattern: rules::Pattern,
+
    },
+
}
+

+
#[derive(Debug, Default)]
+
pub enum OperationName {
+
    Add,
+
    Edit,
+
    #[default]
+
    List,
+
    Match,
+
    Remove,
+
}
+

+
pub struct Options {
+
    pub op: Operation,
+
    pub rid: Option<RepoId>,
+
    pub quiet: bool,
+
}
+

+
impl Args for Options {
+
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+
        use lexopt::prelude::*;
+

+
        let mut parser = lexopt::Parser::from_args(args);
+
        let mut op: Option<OperationName> = None;
+
        let mut pattern: Option<rules::Pattern> = None;
+
        let mut refname: Option<Qualified<'static>> = None;
+
        let mut allow: Vec<Did> = Vec::new();
+
        let mut threshold: Option<usize> = None;
+
        let mut rid: Option<RepoId> = None;
+
        let mut title: Option<String> = None;
+
        let mut description: Option<String> = None;
+
        let mut quiet = false;
+

+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
+
                    "a" | "add" => op = Some(OperationName::Add),
+
                    "e" | "edit" => op = Some(OperationName::Edit),
+
                    "r" | "remove" => op = Some(OperationName::Remove),
+
                    "l" | "list" => op = Some(OperationName::List),
+
                    "m" | "match" => op = Some(OperationName::Match),
+

+
                    unknown => anyhow::bail!("unknown operation '{}'", unknown),
+
                },
+

+
                Value(val) if matches!(op, Some(OperationName::Match)) => {
+
                    refname = Some(term::args::qualified_refstring(val)?);
+
                }
+

+
                Value(val)
+
                    if matches!(
+
                        op,
+
                        Some(OperationName::Add | OperationName::Edit | OperationName::Remove)
+
                    ) =>
+
                {
+
                    pattern = Some(rules::Pattern::try_from(
+
                        term::args::qualified_pattern_string(val)?,
+
                    )?);
+
                }
+

+
                Long("title")
+
                    if matches!(
+
                        op,
+
                        Some(OperationName::Add | OperationName::Edit | OperationName::Remove)
+
                    ) =>
+
                {
+
                    title = Some(parser.value()?.to_string_lossy().into());
+
                }
+
                Long("description")
+
                    if matches!(
+
                        op,
+
                        Some(OperationName::Add | OperationName::Edit | OperationName::Remove)
+
                    ) =>
+
                {
+
                    description = Some(parser.value()?.to_string_lossy().into());
+
                }
+
                Long("allow") if matches!(op, Some(OperationName::Add | OperationName::Edit)) => {
+
                    let did = term::args::did(&parser.value()?)?;
+
                    allow.push(did);
+
                }
+
                Long("threshold")
+
                    if matches!(op, Some(OperationName::Add | OperationName::Edit)) =>
+
                {
+
                    threshold = Some(parser.value()?.to_string_lossy().parse()?);
+
                }
+
                Long("repo") => {
+
                    let val = parser.value()?;
+
                    let val = term::args::rid(&val)?;
+

+
                    rid = Some(val);
+
                }
+
                Long("quiet") | Short('q') => {
+
                    quiet = true;
+
                }
+
                Long("help") | Short('h') => {
+
                    return Err(Error::Help.into());
+
                }
+
                _ => {
+
                    return Err(anyhow!(arg.unexpected()));
+
                }
+
            }
+
        }
+

+
        let op = match op.unwrap_or_default() {
+
            OperationName::Add => Operation::Add {
+
                title,
+
                description,
+
                pattern: pattern.ok_or_else(|| anyhow!("a refspec must be provided"))?,
+
                allow: NonEmpty::from_vec(allow)
+
                    .map(rules::Allowed::Set)
+
                    .unwrap_or(rules::Allowed::Delegates),
+
                threshold: threshold.unwrap_or(1),
+
            },
+
            OperationName::Edit => Operation::Edit {
+
                title,
+
                description,
+
                pattern: pattern.ok_or_else(|| anyhow!("a refspec must be provided"))?,
+
                allow: NonEmpty::from_vec(allow)
+
                    .map(rules::Allowed::Set)
+
                    .unwrap_or(rules::Allowed::Delegates),
+
                threshold: threshold.unwrap_or(1),
+
            },
+
            OperationName::Remove => Operation::Remove {
+
                title,
+
                description,
+
                pattern: pattern.ok_or_else(|| anyhow!("a refspec must be provided"))?,
+
            },
+
            OperationName::List => Operation::List,
+
            OperationName::Match => Operation::Match {
+
                refname: refname.ok_or_else(|| anyhow!("a refname must be provided"))?,
+
            },
+
        };
+

+
        Ok((Options { op, rid, quiet }, vec![]))
+
    }
+
}
+

+
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let storage = &profile.storage;
+
    let rid = options
+
        .rid
+
        .map(Ok)
+
        .unwrap_or_else(|| radicle::rad::cwd().map(|(_, rid)| rid))?;
+
    let repo = storage
+
        .repository(rid)
+
        .context(anyhow!("repository `{rid}` not found in local storage"))?;
+
    let mut identity = Identity::load_mut(&repo)?;
+
    let current = identity.current().clone();
+

+
    match options.op {
+
        Operation::Add {
+
            title,
+
            description,
+
            pattern,
+
            allow,
+
            threshold,
+
        } => {
+
            let rule = Rule::new(allow, threshold);
+
            let proposal = current.doc.clone().edit();
+
            add(
+
                proposal,
+
                pattern,
+
                rule,
+
                &profile,
+
                &repo,
+
                &mut identity,
+
                title,
+
                description,
+
                options.quiet,
+
            )?;
+
        }
+
        Operation::Edit {
+
            title,
+
            description,
+
            pattern,
+
            allow,
+
            threshold,
+
        } => {
+
            let rule = Rule::new(allow, threshold);
+
            let proposal = current.doc.clone().edit();
+
            edit(
+
                proposal,
+
                pattern,
+
                rule,
+
                &profile,
+
                &repo,
+
                &mut identity,
+
                title,
+
                description,
+
                options.quiet,
+
            )?;
+
        }
+
        Operation::Remove {
+
            title,
+
            description,
+
            pattern,
+
        } => {
+
            let proposal = current.doc.clone().edit();
+
            remove(
+
                proposal,
+
                pattern,
+
                profile,
+
                &repo,
+
                &mut identity,
+
                title,
+
                description,
+
                options.quiet,
+
            )?;
+
        }
+
        Operation::List => {
+
            if !options.quiet {
+
                print_rules(current.rules())?;
+
            }
+
        }
+
        Operation::Match { refname } => {
+
            if !options.quiet {
+
                print_rules(&Rules::from_iter(
+
                    current
+
                        .rules()
+
                        .matches(&refname)
+
                        .map(|(pattern, rule)| (pattern.clone(), rule.clone())),
+
                ))?;
+
            }
+
        }
+
    }
+
    Ok(())
+
}
+

+
fn remove<R>(
+
    mut proposal: RawDoc,
+
    pattern: rules::Pattern,
+
    profile: Profile,
+
    repo: &R,
+
    identity: &mut IdentityMut<R>,
+
    title: Option<String>,
+
    description: Option<String>,
+
    quiet: bool,
+
) -> Result<(), anyhow::Error>
+
where
+
    R: cob::Store + WriteRepository,
+
{
+
    proposal.canonical_refs.rules.remove(&pattern);
+
    let revision = id::propose_changes(&profile, repo, proposal, identity, title, description)?;
+
    match revision {
+
        Some(revision) => {
+
            if !quiet {
+
                term::success!("Rule for {} has been removed", pattern);
+
                term::success!(
+
                    "Identity revision {} created",
+
                    term::format::tertiary(revision.id)
+
                );
+
            }
+
        }
+
        None => {
+
            if !quiet {
+
                term::print(term::format::italic(
+
                    "Nothing to do. The rules are up to date. See `rad cref list`.",
+
                ));
+
            }
+
        }
+
    }
+
    Ok(())
+
}
+

+
fn add<R>(
+
    mut proposal: RawDoc,
+
    pattern: rules::Pattern,
+
    rule: RawRule,
+
    profile: &Profile,
+
    repo: &R,
+
    identity: &mut IdentityMut<R>,
+
    title: Option<String>,
+
    description: Option<String>,
+
    quiet: bool,
+
) -> Result<(), anyhow::Error>
+
where
+
    R: cob::Store + WriteRepository,
+
{
+
    proposal.canonical_refs.rules.insert(pattern.clone(), rule);
+
    let revision = id::propose_changes(profile, repo, proposal, identity, title, description)?;
+
    match revision {
+
        Some(revision) => {
+
            if !quiet {
+
                term::success!("Rule for {pattern} has been added");
+
                term::success!(
+
                    "Identity revision {} created",
+
                    term::format::tertiary(revision.id)
+
                );
+
            }
+
        }
+
        None => {
+
            if !quiet {
+
                term::print(term::format::italic(
+
                    "Nothing to do. The rules are up to date. See `rad cref list`.",
+
                ));
+
            }
+
        }
+
    }
+
    Ok(())
+
}
+

+
fn edit<R>(
+
    mut proposal: RawDoc,
+
    pattern: rules::Pattern,
+
    rule: RawRule,
+
    profile: &Profile,
+
    repo: &R,
+
    identity: &mut IdentityMut<R>,
+
    title: Option<String>,
+
    description: Option<String>,
+
    quiet: bool,
+
) -> Result<(), anyhow::Error>
+
where
+
    R: cob::Store + WriteRepository,
+
{
+
    let changed = proposal.canonical_refs.rules.insert(pattern.clone(), rule);
+
    if changed.is_none() {
+
        term::print(term::format::italic(
+
            "Nothing to do. The rules are up to date. See `rad cref list`.",
+
        ));
+
        return Ok(());
+
    }
+
    let revision = id::propose_changes(profile, repo, proposal, identity, title, description)?;
+
    match revision {
+
        Some(revision) => {
+
            if !quiet {
+
                term::success!("Rule for {pattern} has been modified");
+
                term::success!(
+
                    "Identity revision {} created",
+
                    term::format::tertiary(revision.id)
+
                );
+
            }
+
        }
+
        None => {
+
            if !quiet {
+
                term::print(term::format::italic(
+
                    "Nothing to do. The rules are up to date. See `rad cref list`.",
+
                ));
+
            }
+
        }
+
    }
+
    Ok(())
+
}
+

+
fn print_rules(rules: &rules::Rules) -> anyhow::Result<()> {
+
    json::to_pretty(&rules, Path::new("radicle.json"))?.print();
+
    Ok(())
+
}
modified radicle-cli/src/commands/help.rs
@@ -18,6 +18,7 @@ const COMMANDS: &[Help] = &[
    rad_checkout::HELP,
    rad_clone::HELP,
    rad_config::HELP,
+
    rad_cref::HELP,
    rad_fork::HELP,
    rad_help::HELP,
    rad_id::HELP,
modified radicle-cli/src/commands/id.rs
@@ -4,21 +4,20 @@ use std::{ffi::OsString, io};

use anyhow::{anyhow, Context};

-
use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
-
use radicle::identity::{doc, Doc, Identity, PayloadError, RawDoc, Visibility};
-
use radicle::prelude::{Did, RepoId, Signer};
-
use radicle::storage::refs;
+
use radicle::cob::identity;
+
use radicle::cob::identity::Revision;
+
use radicle::identity::{doc, Doc, Identity, RawDoc, Visibility};
+
use radicle::prelude::{Did, RepoId};
+
use radicle::storage::{refs, HasRepoId};
use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
-
use radicle::{cob, Profile};
-
use radicle_surf::diff::Diff;
+
use radicle::Profile;
use radicle_term::Element;
use serde_json as json;

-
use crate::git::unified_diff::Encode as _;
use crate::git::Rev;
+
use crate::id;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
-
use crate::terminal::patch::Message;
use crate::terminal::Interactive;

pub const HELP: Help = Help {
@@ -31,12 +30,13 @@ Usage
    rad id list [<option>...]
    rad id update [--title <string>] [--description <string>]
                  [--delegate <did>] [--rescind <did>]
-
                  [--threshold <num>] [--visibility <private | public>]
+
                  [--visibility <private | public>]
                  [--allow <did>] [--disallow <did>]
                  [--no-confirm] [--payload <id> <key> <val>...] [--edit] [<option>...]
    rad id edit <revision-id> [--title <string>] [--description <string>] [<option>...]
    rad id show <revision-id> [<option>...]
    rad id <accept | reject | redact> <revision-id> [<option>...]
+
    rad id migrate [--no-confirm] [--title <string>] [--description <string>]

    The *rad id* command is used to manage and propose changes to the
    identity of a Radicle repository.
@@ -53,12 +53,15 @@ Options

#[derive(Clone, Debug, Default)]
pub enum Operation {
+
    Migrate {
+
        title: Option<String>,
+
        description: Option<String>,
+
    },
    Update {
        title: Option<String>,
        description: Option<String>,
        delegate: Vec<Did>,
        rescind: Vec<Did>,
-
        threshold: Option<usize>,
        visibility: Option<EditVisibility>,
        allow: BTreeSet<Did>,
        disallow: BTreeSet<Did>,
@@ -115,6 +118,7 @@ pub enum OperationName {
    Reject,
    Edit,
    Update,
+
    Migrate,
    Show,
    Redact,
    #[default]
@@ -143,7 +147,6 @@ impl Args for Options {
        let mut visibility: Option<EditVisibility> = None;
        let mut allow: BTreeSet<Did> = BTreeSet::new();
        let mut disallow: BTreeSet<Did> = BTreeSet::new();
-
        let mut threshold: Option<usize> = None;
        let mut interactive = Interactive::new(io::stdout());
        let mut payload = Vec::new();
        let mut edit = false;
@@ -176,6 +179,7 @@ impl Args for Options {
                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
                    "e" | "edit" => op = Some(OperationName::Edit),
                    "u" | "update" => op = Some(OperationName::Update),
+
                    "m" | "migrate" => op = Some(OperationName::Migrate),
                    "l" | "list" => op = Some(OperationName::List),
                    "s" | "show" => op = Some(OperationName::Show),
                    "a" | "accept" => op = Some(OperationName::Accept),
@@ -214,9 +218,6 @@ impl Args for Options {

                    visibility = Some(value);
                }
-
                Long("threshold") => {
-
                    threshold = Some(parser.value()?.to_string_lossy().parse()?);
-
                }
                Long("payload") => {
                    let mut values = parser.values()?;
                    let id = values
@@ -275,13 +276,13 @@ impl Args for Options {
                description,
                delegate,
                rescind,
-
                threshold,
                visibility,
                allow,
                disallow,
                payload,
                edit,
            },
+
            OperationName::Migrate => Operation::Migrate { title, description },
        };
        Ok((
            Options {
@@ -371,7 +372,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            if !revision.is_active() {
                anyhow::bail!("revision can no longer be edited");
            }
-
            let Some((title, description)) = edit_title_description(title, description)? else {
+
            let Some((title, description)) = id::edit_title_description(title, description)? else {
                anyhow::bail!("revision title or description missing");
            };
            identity.edit(revision.id, title, description, &signer)?;
@@ -385,7 +386,6 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            description,
            delegate: delegates,
            rescind,
-
            threshold,
            visibility,
            allow,
            disallow,
@@ -394,7 +394,6 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        } => {
            let proposal = {
                let mut proposal = current.doc.clone().edit();
-
                proposal.threshold = threshold.unwrap_or(proposal.threshold);

                if !allow.is_disjoint(&disallow) {
                    let overlap = allow
@@ -438,17 +437,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                    .chain(delegates)
                    .filter(|d| !rescind.contains(d))
                    .collect::<Vec<_>>();
-
                if let Some(errs) = verify_delegates(&proposal, &repo)? {
-
                    term::error(format!("failed to verify delegates for {rid}"));
-
                    term::error(format!(
-
                        "the threshold of {} delegates cannot be met..",
-
                        proposal.threshold
-
                    ));
-
                    for e in errs {
-
                        e.print();
-
                    }
-
                    anyhow::bail!("fatal: refusing to update identity document");
-
                }
+

+
                verify_project_delegates(&proposal, &current, &repo)?;

                for (id, key, val) in payload {
                    if let Some(ref mut payload) = proposal.payload.get_mut(&id) {
@@ -487,34 +477,27 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                proposal
            };

-
            // Verify that the project payload can still be parsed into the `Project` type.
-
            if let Err(PayloadError::Json(e)) = proposal.project() {
-
                anyhow::bail!("failed to verify `xyz.radicle.project`, {e}");
-
            }
-
            let proposal = proposal.verified()?;
-
            if proposal == current.doc {
-
                if !options.quiet {
-
                    term::print(term::format::italic(
-
                        "Nothing to do. The document is up to date. See `rad inspect --identity`.",
-
                    ));
+
            let revision =
+
                id::propose_changes(&profile, &repo, proposal, &mut identity, title, description)?;
+
            match revision {
+
                Some(revision) => {
+
                    if options.quiet {
+
                        term::print(revision.id);
+
                    } else {
+
                        term::success!(
+
                            "Identity revision {} created",
+
                            term::format::tertiary(revision.id)
+
                        );
+
                        id::print(&revision, &identity, &repo, &profile)?;
+
                    }
+
                }
+
                None => {
+
                    if !options.quiet {
+
                        term::print(term::format::italic(
+
                            "Nothing to do. The document is up to date. See `rad inspect --identity`.",
+
                        ));
+
                    }
                }
-
                return Ok(());
-
            }
-
            let signer = term::signer(&profile)?;
-
            let revision = update(title, description, proposal, &mut identity, &signer)?;
-

-
            if revision.is_accepted() && revision.parent == Some(current.id) {
-
                // Update the canonical head to point to the latest accepted revision.
-
                repo.set_identity_head_to(revision.id)?;
-
            }
-
            if options.quiet {
-
                term::print(revision.id);
-
            } else {
-
                term::success!(
-
                    "Identity revision {} created",
-
                    term::format::tertiary(revision.id)
-
                );
-
                print(&revision, &current, &repo, &profile)?;
            }
        }
        Operation::ListRevisions => {
@@ -576,7 +559,42 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                .revision(&previous)
                .ok_or(anyhow!("revision `{previous}` not found"))?;

-
            print(revision, previous, &repo, &profile)?;
+
            id::print(revision, previous, &repo, &profile)?;
+
        }
+
        Operation::Migrate { title, description } => {
+
            let proposal = repo.identity_doc()?.doc;
+
            if options.interactive.confirm(format!(
+
                "Perform migration?\n{}",
+
                term::format::tertiary(serde_json::to_string_pretty(&proposal)?)
+
            )) {
+
                let revision = id::propose_changes(
+
                    &profile,
+
                    &repo,
+
                    proposal.edit(),
+
                    &mut identity,
+
                    title,
+
                    description,
+
                )?;
+
                match revision {
+
                    Some(revision) => {
+
                        if options.quiet {
+
                            term::print(revision.id);
+
                        } else {
+
                            term::success!(
+
                                "Identity revision {} created",
+
                                term::format::tertiary(revision.id)
+
                            );
+
                            id::print(&revision, &identity, &repo, &profile)?;
+
                        }
+
                    }
+
                    None => {
+
                        if !options.quiet {
+
                            let msg = "Nothing to do. The document is up to date. See `rad inspect --identity`.";
+
                            term::print(term::format::italic(msg));
+
                        }
+
                    }
+
                }
+
            }
        }
    }
    Ok(())
@@ -682,107 +700,7 @@ fn print_meta(revision: &Revision, previous: &Doc, profile: &Profile) -> anyhow:
    Ok(())
}

-
fn print(
-
    revision: &identity::Revision,
-
    previous: &identity::Revision,
-
    repo: &radicle::storage::git::Repository,
-
    profile: &Profile,
-
) -> anyhow::Result<()> {
-
    print_meta(revision, previous, profile)?;
-
    println!();
-
    print_diff(revision.parent.as_ref(), &revision.id, repo)?;
-

-
    Ok(())
-
}
-

-
fn edit_title_description(
-
    title: Option<String>,
-
    description: Option<String>,
-
) -> anyhow::Result<Option<(String, String)>> {
-
    const HELP: &str = r#"<!--
-
Please enter a patch message for your changes. An empty
-
message aborts the patch proposal.
-

-
The first line is the patch title. The patch description
-
follows, and must be separated with a blank line, just
-
like a commit message. Markdown is supported in the title
-
and description.
-
-->"#;
-

-
    let result = if let (Some(t), d) = (title.as_ref(), description.as_deref()) {
-
        Some((t.to_owned(), d.unwrap_or_default().to_owned()))
-
    } else {
-
        let result = Message::edit_title_description(title, description, HELP)?;
-
        if let Some((title, description)) = result {
-
            Some((title, description))
-
        } else {
-
            None
-
        }
-
    };
-
    Ok(result)
-
}
-

-
fn update<R: WriteRepository + cob::Store, G: Signer>(
-
    title: Option<String>,
-
    description: Option<String>,
-
    doc: Doc,
-
    current: &mut IdentityMut<R>,
-
    signer: &G,
-
) -> anyhow::Result<Revision> {
-
    if let Some((title, description)) = edit_title_description(title, description)? {
-
        let id = current.update(title, description, &doc, signer)?;
-
        let revision = current
-
            .revision(&id)
-
            .ok_or(anyhow!("update failed: revision {id} is missing"))?;
-

-
        Ok(revision.clone())
-
    } else {
-
        Err(anyhow!("you must provide a revision title and description"))
-
    }
-
}
-

-
fn print_diff(
-
    previous: Option<&RevisionId>,
-
    current: &RevisionId,
-
    repo: &radicle::storage::git::Repository,
-
) -> anyhow::Result<()> {
-
    let previous = if let Some(previous) = previous {
-
        let previous = Doc::load_at(*previous, repo)?;
-
        let previous = serde_json::to_string_pretty(&previous.doc)?;
-

-
        Some(previous)
-
    } else {
-
        None
-
    };
-
    let current = Doc::load_at(*current, repo)?;
-
    let current = serde_json::to_string_pretty(&current.doc)?;
-

-
    let tmp = tempfile::tempdir()?;
-
    let repo = radicle::git::raw::Repository::init_bare(tmp.path())?;
-

-
    let previous = if let Some(previous) = previous {
-
        let tree = radicle::git::write_tree(&doc::PATH, previous.as_bytes(), &repo)?;
-
        Some(tree)
-
    } else {
-
        None
-
    };
-
    let current = radicle::git::write_tree(&doc::PATH, current.as_bytes(), &repo)?;
-
    let mut opts = radicle::git::raw::DiffOptions::new();
-
    opts.context_lines(u32::MAX);
-

-
    let diff = repo.diff_tree_to_tree(previous.as_ref(), Some(&current), Some(&mut opts))?;
-
    let diff = Diff::try_from(diff)?;
-

-
    if let Some(modified) = diff.modified().next() {
-
        let diff = modified.diff.to_unified_string()?;
-
        print!("{diff}");
-
    } else {
-
        term::print(term::format::italic("No changes."));
-
    }
-
    Ok(())
-
}
-

-
#[derive(Clone)]
+
#[derive(Clone, Debug)]
enum VerificationError {
    MissingDefaultBranch {
        branch: radicle::git::RefString,
@@ -811,19 +729,46 @@ impl VerificationError {
    }
}

-
fn verify_delegates<S>(
-
    proposal: &RawDoc,
+
// N.b. if we are modifying a project repository, we want to ensure that we have
+
// rule for the default branch and we can verify the number of delegates for it
+
fn verify_project_delegates<S>(proposal: &RawDoc, current: &Doc, repo: &S) -> anyhow::Result<()>
+
where
+
    S: ReadRepository,
+
{
+
    let Some(threshold) = current.default_branch_threshold()? else {
+
        anyhow::bail!("failed to find threshold of canonical ref rule for default branch");
+
    };
+

+
    if let Some(errs) = verify_delegates(&proposal.delegates, threshold, repo)? {
+
        term::error(format!("failed to verify delegates for {}", repo.rid()));
+
        term::error(format!(
+
            "the threshold of {} delegates cannot be met..",
+
            threshold
+
        ));
+
        for e in errs {
+
            e.print();
+
        }
+
        anyhow::bail!("fatal: refusing to update identity document");
+
    }
+
    Ok(())
+
}
+

+
fn verify_delegates<'a, I, S>(
+
    delegates: I,
+
    threshold: usize,
    repo: &S,
) -> anyhow::Result<Option<Vec<VerificationError>>>
where
+
    I: IntoIterator<Item = &'a Did>,
+
    I::IntoIter: ExactSizeIterator,
    S: ReadRepository,
{
-
    let dids = &proposal.delegates;
-
    let threshold = proposal.threshold;
+
    let delegates = delegates.into_iter();
+
    let n = delegates.len();
    let (canonical, _) = repo.canonical_head()?;
-
    let mut missing = Vec::with_capacity(dids.len());
+
    let mut missing = Vec::with_capacity(n);

-
    for did in dids {
+
    for did in delegates {
        match refs::SignedRefsAt::load((*did).into(), repo)? {
            None => {
                missing.push(VerificationError::MissingDelegate { did: *did });
@@ -839,5 +784,5 @@ where
        }
    }

-
    Ok((dids.len() - missing.len() < threshold).then_some(missing))
+
    Ok((n - missing.len() < threshold).then_some(missing))
}
added radicle-cli/src/id.rs
@@ -0,0 +1,241 @@
+
use anyhow::anyhow;
+

+
use radicle::cob;
+
use radicle::cob::identity::{IdentityMut, Revision, RevisionId};
+
use radicle::crypto::Signer;
+
use radicle::identity::{doc, Doc, RawDoc};
+
use radicle::storage::{ReadRepository, WriteRepository};
+
use radicle::Profile;
+
use radicle_surf::diff::Diff;
+
use radicle_term::Element as _;
+

+
use crate::git::unified_diff::Encode as _;
+
use crate::terminal as term;
+
use crate::terminal::patch::Message;
+

+
pub fn propose_changes<R>(
+
    profile: &Profile,
+
    repo: &R,
+
    proposal: RawDoc,
+
    current: &mut IdentityMut<R>,
+
    title: Option<String>,
+
    description: Option<String>,
+
) -> anyhow::Result<Option<Revision>>
+
where
+
    R: ReadRepository + WriteRepository + cob::Store,
+
{
+
    // Verify that the project payload can still be parsed into the `Project` type.
+
    if let Err(doc::PayloadError::Json(e)) = proposal.project() {
+
        anyhow::bail!("failed to verify `xyz.radicle.project`: {e}");
+
    }
+

+
    let proposal = proposal.verified()?;
+
    if proposal == current.doc {
+
        return Ok(None);
+
    }
+
    let signer = term::signer(profile)?;
+
    // N.b. get the parent OID before updating the identity
+
    let parent = current.current().id;
+
    let revision = update(title, description, proposal, current, &signer)?;
+

+
    if revision.is_accepted() && revision.parent == Some(parent) {
+
        // Update the canonical head to point to the latest accepted revision.
+
        repo.set_identity_head_to(revision.id)?;
+
    }
+
    Ok(Some(revision))
+
}
+

+
pub fn update<R, G>(
+
    title: Option<String>,
+
    description: Option<String>,
+
    doc: Doc,
+
    current: &mut IdentityMut<R>,
+
    signer: &G,
+
) -> anyhow::Result<Revision>
+
where
+
    R: WriteRepository + cob::Store,
+
    G: Signer,
+
{
+
    if let Some((title, description)) = edit_title_description(title, description)? {
+
        let id = current.update(title, description, &doc, signer)?;
+
        let revision = current
+
            .revision(&id)
+
            .ok_or(anyhow!("update failed: revision {id} is missing"))?;
+

+
        Ok(revision.clone())
+
    } else {
+
        Err(anyhow!("you must provide a revision title and description"))
+
    }
+
}
+

+
pub fn edit_title_description(
+
    title: Option<String>,
+
    description: Option<String>,
+
) -> anyhow::Result<Option<(String, String)>> {
+
    const HELP: &str = r#"<!--
+
Please enter a message for your changes. An empty message aborts the proposal.
+

+
The first line is the title. The description follows, and must be separated with
+
a blank line, just like a commit message. Markdown is supported in the title and
+
description.
+
-->"#;
+

+
    let result = if let (Some(t), d) = (title.as_ref(), description.as_deref()) {
+
        Some((t.to_owned(), d.unwrap_or_default().to_owned()))
+
    } else {
+
        let result = Message::edit_title_description(title, description, HELP)?;
+
        if let Some((title, description)) = result {
+
            Some((title, description))
+
        } else {
+
            None
+
        }
+
    };
+
    Ok(result)
+
}
+

+
pub fn print<R>(
+
    revision: &Revision,
+
    previous: &Revision,
+
    repo: &R,
+
    profile: &Profile,
+
) -> anyhow::Result<()>
+
where
+
    R: ReadRepository,
+
{
+
    print_meta(revision, previous, profile)?;
+
    println!();
+
    print_diff(revision.parent.as_ref(), &revision.id, repo)?;
+

+
    Ok(())
+
}
+

+
fn print_meta(revision: &Revision, previous: &Doc, profile: &Profile) -> anyhow::Result<()> {
+
    let mut attrs = term::Table::<2, term::Label>::new(Default::default());
+

+
    attrs.push([
+
        term::format::bold("Title").into(),
+
        term::label(revision.title.to_owned()),
+
    ]);
+
    attrs.push([
+
        term::format::bold("Revision").into(),
+
        term::label(revision.id.to_string()),
+
    ]);
+
    attrs.push([
+
        term::format::bold("Blob").into(),
+
        term::label(revision.blob.to_string()),
+
    ]);
+
    attrs.push([
+
        term::format::bold("Author").into(),
+
        term::label(revision.author.to_string()),
+
    ]);
+
    attrs.push([
+
        term::format::bold("State").into(),
+
        term::label(revision.state.to_string()),
+
    ]);
+
    attrs.push([
+
        term::format::bold("Quorum").into(),
+
        if revision.is_accepted() {
+
            term::format::positive("yes").into()
+
        } else {
+
            term::format::negative("no").into()
+
        },
+
    ]);
+

+
    let mut meta = term::VStack::default()
+
        .border(Some(term::colors::FAINT))
+
        .child(attrs)
+
        .children(if !revision.description.is_empty() {
+
            vec![
+
                term::Label::blank().boxed(),
+
                term::textarea(revision.description.to_owned()).boxed(),
+
            ]
+
        } else {
+
            vec![]
+
        })
+
        .divider();
+

+
    let accepted = revision.accepted().collect::<Vec<_>>();
+
    let rejected = revision.rejected().collect::<Vec<_>>();
+
    let unknown = previous
+
        .delegates()
+
        .iter()
+
        .filter(|id| !accepted.contains(id) && !rejected.contains(id))
+
        .collect::<Vec<_>>();
+
    let mut signatures = term::Table::<4, _>::default();
+

+
    for id in accepted {
+
        let author = term::format::Author::new(&id, profile);
+
        signatures.push([
+
            term::format::positive("✓").into(),
+
            id.to_string().into(),
+
            author.alias().unwrap_or_default(),
+
            author.you().unwrap_or_default(),
+
        ]);
+
    }
+
    for id in rejected {
+
        let author = term::format::Author::new(&id, profile);
+
        signatures.push([
+
            term::format::negative("✗").into(),
+
            id.to_string().into(),
+
            author.alias().unwrap_or_default(),
+
            author.you().unwrap_or_default(),
+
        ]);
+
    }
+
    for id in unknown {
+
        let author = term::format::Author::new(id, profile);
+
        signatures.push([
+
            term::format::dim("?").into(),
+
            id.to_string().into(),
+
            author.alias().unwrap_or_default(),
+
            author.you().unwrap_or_default(),
+
        ]);
+
    }
+
    meta.push(signatures);
+
    meta.print();
+

+
    Ok(())
+
}
+

+
fn print_diff<R>(
+
    previous: Option<&RevisionId>,
+
    current: &RevisionId,
+
    repo: &R,
+
) -> anyhow::Result<()>
+
where
+
    R: ReadRepository,
+
{
+
    let previous = if let Some(previous) = previous {
+
        let previous = Doc::load_at(*previous, repo)?;
+
        let previous = serde_json::to_string_pretty(&previous.doc)?;
+

+
        Some(previous)
+
    } else {
+
        None
+
    };
+
    let current = Doc::load_at(*current, repo)?;
+
    let current = serde_json::to_string_pretty(&current.doc)?;
+

+
    let tmp = tempfile::tempdir()?;
+
    let repo = radicle::git::raw::Repository::init_bare(tmp.path())?;
+

+
    let previous = if let Some(previous) = previous {
+
        let tree = radicle::git::write_tree(&doc::PATH, previous.as_bytes(), &repo)?;
+
        Some(tree)
+
    } else {
+
        None
+
    };
+
    let current = radicle::git::write_tree(&doc::PATH, current.as_bytes(), &repo)?;
+
    let mut opts = radicle::git::raw::DiffOptions::new();
+
    opts.context_lines(u32::MAX);
+

+
    let diff = repo.diff_tree_to_tree(previous.as_ref(), Some(&current), Some(&mut opts))?;
+
    let diff = Diff::try_from(diff)?;
+

+
    if let Some(modified) = diff.modified().next() {
+
        let diff = modified.diff.to_unified_string()?;
+
        print!("{diff}");
+
    } else {
+
        term::print(term::format::italic("No changes."));
+
    }
+
    Ok(())
+
}
modified radicle-cli/src/lib.rs
@@ -3,6 +3,7 @@
#![allow(clippy::too_many_arguments)]
pub mod commands;
pub mod git;
+
pub mod id;
pub mod node;
pub mod pager;
pub mod project;
modified radicle-cli/src/main.rs
@@ -140,6 +140,13 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
                args.to_vec(),
            );
        }
+
        "cref" => {
+
            term::run_command_args::<rad_cref::Options, _>(
+
                rad_cref::HELP,
+
                rad_cref::run,
+
                args.to_vec(),
+
            );
+
        }
        "checkout" => {
            term::run_command_args::<rad_checkout::Options, _>(
                rad_checkout::HELP,
modified radicle-cli/src/terminal/args.rs
@@ -5,9 +5,10 @@ use std::time;

use anyhow::anyhow;

-
use radicle::cob::{self, issue, patch};
+
use radicle::cob;
+
use radicle::cob::{issue, patch};
use radicle::crypto;
-
use radicle::git::{Oid, RefString};
+
use radicle::git::{refspec::QualifiedPattern, Oid, PatternString, RefString};
use radicle::node::{Address, Alias};
use radicle::prelude::{Did, NodeId, RepoId};

@@ -107,6 +108,30 @@ pub fn refstring(flag: &str, value: OsString) -> anyhow::Result<RefString> {
    })
}

+
pub fn qualified_refstring(value: OsString) -> anyhow::Result<radicle::git::Qualified<'static>> {
+
    let refstring = RefString::try_from(
+
        value
+
            .into_string()
+
            .map_err(|_| anyhow!("the value specified is not valid UTF-8"))?,
+
    )
+
    .map_err(|_| anyhow!("the value specified is not a valid ref string",))?;
+
    radicle::git::Qualified::from_refstr(refstring)
+
        .ok_or_else(|| anyhow!("the value specified is not a valid qualified ref string",))
+
}
+

+
pub fn qualified_pattern_string(value: OsString) -> anyhow::Result<QualifiedPattern<'static>> {
+
    let value = value
+
        .into_string()
+
        .map_err(|_| anyhow!("the <refspec> value is not valid UTF-8"))?;
+
    PatternString::try_from(value.clone())
+
        .map_err(|_| anyhow!("'{value}' is not a valid refspec string",))
+
        .and_then(|s| {
+
            let q = QualifiedPattern::from_patternstr(&s)
+
                .ok_or(anyhow!("'{value}' is a not a qualified refspec string"))?;
+
            Ok(q.to_owned())
+
        })
+
}
+

pub fn did(val: &OsString) -> anyhow::Result<Did> {
    let val = val.to_string_lossy();
    let Ok(peer) = Did::from_str(&val) else {
modified radicle-cli/tests/commands.rs
@@ -245,6 +245,20 @@ fn rad_cob_multiset() {
}

#[test]
+
fn rad_cref() {
+
    let mut environment = Environment::new();
+
    let profile = environment.profile(config::profile("alice"));
+
    let home = &profile.home;
+
    let working = environment.tmp().join("working");
+

+
    // Setup a test repository.
+
    fixtures::repository(&working);
+

+
    test("examples/rad-init.md", &working, Some(home), []).unwrap();
+
    test("examples/rad-cref.md", &working, Some(home), []).unwrap();
+
}
+

+
#[test]
fn rad_cob_log() {
    let mut environment = Environment::new();
    let profile = environment.profile(config::profile("alice"));
@@ -480,7 +494,7 @@ fn rad_id() {
    let bob = environment.node(config::node("bob"));
    let working = tempfile::tempdir().unwrap();
    let working = working.path();
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -521,7 +535,11 @@ fn rad_id() {
    .unwrap();
}

+
// FIXME: the test for this would require being able to modify the threshold of
+
// the default branch rule and the delegate set at the same time, but right now
+
// that's not possible via the CLI.
#[test]
+
#[ignore = "not implemented"]
fn rad_id_threshold() {
    let mut environment = Environment::new();
    let alice = environment.node(config::node("alice"));
@@ -529,7 +547,7 @@ fn rad_id_threshold() {
    let seed = environment.node(config::node("seed"));
    let working = tempfile::tempdir().unwrap();
    let working = working.path();
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -586,7 +604,7 @@ fn rad_id_threshold_soft_fork() {
    let bob = environment.node(config::node("bob"));
    let working = tempfile::tempdir().unwrap();
    let working = working.path();
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -664,7 +682,7 @@ fn rad_id_multi_delegate() {
    let eve = environment.node(Config::test(Alias::new("eve")));
    let working = tempfile::tempdir().unwrap();
    let working = working.path();
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -725,7 +743,7 @@ fn rad_id_collaboration() {
    let distrustful = environment.node(config::seed("distrustful"));
    let working = tempfile::tempdir().unwrap();
    let working = working.path();
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -827,7 +845,7 @@ fn rad_id_conflict() {
    let bob = environment.node(Config::test(Alias::new("bob")));
    let working = tempfile::tempdir().unwrap();
    let working = working.path();
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -1072,7 +1090,7 @@ fn rad_patch_checkout_force() {
    let alice = environment.node(Config::test(Alias::new("alice")));
    let bob = environment.node(Config::test(Alias::new("bob")));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -1309,7 +1327,7 @@ fn rad_patch_delete() {
    let bob = environment.node(config::relay("bob"));
    let seed = environment.node(config::relay("seed"));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    // Setup a test repository.
    fixtures::repository(working.join("alice"));
@@ -1369,7 +1387,7 @@ fn rad_clean() {
    let working = environment.tmp().join("working");

    // Setup a test project.
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();
    fixtures::repository(working.join("acme"));
    test(
        "examples/rad-init.md",
@@ -1651,7 +1669,7 @@ fn rad_clone_connect() {
    let alice = environment.node(Config::test(Alias::new("alice")));
    let bob = environment.node(Config::test(Alias::new("bob")));
    let mut eve = environment.node(Config::test(Alias::new("eve")));
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();
    let ua = UserAgent::default();
    let now = localtime::LocalTime::now().into();

@@ -2141,7 +2159,7 @@ fn rad_sync() {
    let alice = environment.node(config::seed("alice"));
    let bob = environment.node(config::seed("bob"));
    let eve = environment.node(config::seed("eve"));
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    fixtures::repository(working.join("acme"));

@@ -2191,7 +2209,7 @@ fn test_replication_via_seed() {
        ..config::relay("seed")
    });
    let working = environment.tmp().join("working");
-
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let rid = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    let mut alice = alice.spawn();
    let mut bob = bob.spawn();
@@ -2278,7 +2296,7 @@ fn rad_remote() {
    let eve = environment.node(config::relay("eve"));
    let working = environment.tmp().join("working");
    let home = alice.home.clone();
-
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let rid = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();
    // Setup a test repository.
    fixtures::repository(working.join("alice"));

@@ -2666,7 +2684,7 @@ fn git_push_diverge() {
    let alice = environment.node(Config::test(Alias::new("alice")));
    let bob = environment.node(Config::test(Alias::new("bob")));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    fixtures::repository(working.join("alice"));

@@ -2710,7 +2728,7 @@ fn git_push_converge() {
    let bob = environment.node(Config::test(Alias::new("bob")));
    let eve = environment.node(Config::test(Alias::new("eve")));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    fixtures::repository(working.join("alice"));

@@ -2771,7 +2789,7 @@ fn git_push_amend() {
    let alice = environment.node(Config::test(Alias::new("alice")));
    let bob = environment.node(Config::test(Alias::new("bob")));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    fixtures::repository(working.join("alice"));

@@ -2812,7 +2830,7 @@ fn git_push_rollback() {
    let alice = environment.node(Config::test(Alias::new("alice")));
    let bob = environment.node(Config::test(Alias::new("bob")));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    fixtures::repository(working.join("alice"));

@@ -2853,7 +2871,7 @@ fn rad_push_and_pull_patches() {
    let alice = environment.node(Config::test(Alias::new("alice")));
    let bob = environment.node(Config::test(Alias::new("bob")));
    let working = environment.tmp().join("working");
-
    let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    let acme = RepoId::from_str("z3W5xAVWJ9Gc4LbN16mE3tjWX92t2").unwrap();

    fixtures::repository(working.join("alice"));

@@ -3062,6 +3080,54 @@ fn git_push_and_fetch() {
}

#[test]
+
fn git_push_canonical_tags() {
+
    let mut environment = Environment::new();
+
    let alice = environment.node(Config::test(Alias::new("alice")));
+
    let bob = environment.node(Config::test(Alias::new("bob")));
+
    let working = environment.tmp().join("working");
+

+
    fixtures::repository(working.join("alice"));
+

+
    test(
+
        "examples/rad-init.md",
+
        working.join("alice"),
+
        Some(&alice.home),
+
        [],
+
    )
+
    .unwrap();
+

+
    let alice = alice.spawn();
+
    let mut bob = bob.spawn();
+

+
    bob.connect(&alice).converge([&alice]);
+

+
    test(
+
        "examples/rad-clone.md",
+
        working.join("bob"),
+
        Some(&bob.home),
+
        [],
+
    )
+
    .unwrap();
+
    formula(
+
        &environment.tmp(),
+
        "examples/git/git-push-canonical-tags.md",
+
    )
+
    .unwrap()
+
    .home(
+
        "alice",
+
        working.join("alice"),
+
        [("RAD_HOME", alice.home.path().display())],
+
    )
+
    .home(
+
        "bob",
+
        working.join("bob"),
+
        [("RAD_HOME", bob.home.path().display())],
+
    )
+
    .run()
+
    .unwrap();
+
}
+

+
#[test]
fn git_tag() {
    let mut environment = Environment::new();
    let alice = environment.node(Config::test(Alias::new("alice")));
modified radicle-cob/Cargo.toml
@@ -22,7 +22,7 @@ fastrand = { version = "2.0.0" }
log = { version = "0.4.17" }
nonempty = { version = "0.9.0", features = ["serialize"] }
once_cell = { version = "1.13" }
-
radicle-git-ext = { version = "0.8.0", features = ["serde"] }
+
radicle-git-ext = { version = "0.8", features = ["serde"] }
serde_json = { version = "1.0" }
thiserror = { version = "1.0" }

modified radicle-crypto/Cargo.toml
@@ -27,7 +27,7 @@ thiserror = { version = "1" }
zeroize = { version = "1.5.7" }

[dependencies.radicle-git-ext]
-
version = "0.8.0"
+
version = "0.8"
default-features = false
optional = true

modified radicle-fetch/Cargo.toml
@@ -18,7 +18,7 @@ gix-protocol = { version = "0.47.0", features = ["blocking-client"] }
gix-transport = { version = "0.44.0", features = ["blocking-client"] }
log = { version = "0.4.17", features = ["std"] }
nonempty = { version = "0.9.0" }
-
radicle-git-ext = { version = "0.8.0", features = ["bstr"] }
+
radicle-git-ext = { version = "0.8", features = ["bstr"] }
thiserror = { version = "1" }

[dependencies.radicle]
modified radicle-fetch/src/state.rs
@@ -32,8 +32,8 @@ pub const DEFAULT_FETCH_DATA_REFS_LIMIT: u64 = 1024 * 1024 * 1024 * 5;
pub mod error {
    use std::io;

-
    use radicle::git::Oid;
    use radicle::prelude::PublicKey;
+
    use radicle::{git::Oid, identity::doc};
    use thiserror::Error;

    use crate::{git, git::repository, handle, sigrefs, stage};
@@ -67,6 +67,8 @@ pub mod error {
        #[error("canonical 'refs/rad/id' is missing")]
        MissingRadId,
        #[error(transparent)]
+
        Payload(#[from] doc::PayloadError),
+
        #[error(transparent)]
        RefdbUpdate(#[from] repository::error::Update),
        #[error(transparent)]
        Resolve(#[from] repository::error::Resolve),
@@ -401,12 +403,15 @@ impl FetchState {

        log::trace!(target: "fetch", "Identity delegates {delegates:?}");

+
        // The threshold is only relevant to the default branch, if there is
+
        // none we set the threshold to 0 so that validation succeeds.
+
        let threshold: usize = anchor.default_branch_threshold()?.unwrap_or(0);
        // The local peer does not need to count towards the threshold
        // since they must be valid already.
        let threshold = if is_delegate {
-
            anchor.threshold() - 1
+
            threshold.saturating_sub(1)
        } else {
-
            anchor.threshold()
+
            threshold
        };
        let signed_refs = self.run_special_refs(
            handle,
modified radicle-node/Cargo.toml
@@ -34,7 +34,7 @@ once_cell = { version = "1.13" }
qcheck = { version = "1", default-features = false, optional = true }
# N.b. this is required to use macros, even though it's re-exported
# through radicle
-
radicle-git-ext = { version = "0.8.0", features = ["serde"] }
+
radicle-git-ext = { version = "0.8", features = ["serde"] }
sqlite = { version = "0.32.0", features = ["bundled"] }
scrypt = { version = "0.11.0", default-features = false }
serde = { version = "1", features = ["derive"] }
modified radicle-node/src/worker/fetch.rs
@@ -9,12 +9,14 @@ use radicle::cob::TypedId;
use radicle::crypto::PublicKey;
use radicle::identity::DocAt;
use radicle::prelude::RepoId;
+
use radicle::storage::git::Repository;
use radicle::storage::refs::RefsAt;
use radicle::storage::{
    ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, RepositoryError,
    WriteRepository as _,
};
use radicle::{cob, git, node, Storage};
+
use radicle_fetch::git::refs::Applied;
use radicle_fetch::{Allowed, BlockList, FetchLimit};

use super::channels::ChannelsFlush;
@@ -88,8 +90,6 @@ impl Handle {
        remote: PublicKey,
        refs_at: Option<Vec<RefsAt>>,
    ) -> Result<FetchResult, error::Fetch> {
-
        use git::canonical::QuorumError::{Diverging, NoCandidates};
-

        let (result, clone, notifs) = match self {
            Self::Clone { mut handle, tmp } => {
                log::debug!(target: "worker", "{} cloning from {remote}", handle.local());
@@ -144,15 +144,19 @@ impl Handle {
                            log::trace!(target: "worker", "Set HEAD to {}", head.new);
                        }
                    }
-
                    Err(RepositoryError::Quorum(Diverging(e))) => {
-
                        log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
+
                    Err(RepositoryError::Quorum(radicle::git::canonical::QuorumError::Git(e))) => {
+
                        return Err(e.into())
                    }
-
                    Err(RepositoryError::Quorum(NoCandidates(e))) => {
+
                    Err(RepositoryError::Quorum(e)) => {
                        log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
                    }
                    Err(e) => return Err(e.into()),
                }

+
                if let Err(e) = set_canonical_refs(&repo, &applied) {
+
                    log::warn!(target: "worker", "Failed to set canonical references: {e}");
+
                }
+

                // Notifications are only posted for pulls, not clones.
                if let Some(mut store) = notifs {
                    // Only create notifications for repos that we have
@@ -376,3 +380,67 @@ where

    Ok(())
}
+

+
fn set_canonical_refs(repo: &Repository, applied: &Applied) -> Result<(), error::Canonical> {
+
    let identity = repo.identity()?;
+
    let rules = identity.rules();
+
    if rules.is_empty() {
+
        return Ok(());
+
    }
+

+
    for update in applied.updated.iter() {
+
        let name = match update {
+
            RefUpdate::Updated { name, .. } | RefUpdate::Created { name, .. } => name,
+
            _ => {
+
                log::trace!(target: "worker", "Skipping update {update}");
+
                continue;
+
            }
+
        };
+
        let Some(name) = name.clone().into_qualified() else {
+
            log::warn!(target: "worker", "Skipping update for canonical reference '{name}' because it is not qualified.");
+
            continue;
+
        };
+
        let Some(name) = name.to_namespaced() else {
+
            log::warn!(target: "worker", "Skipping update for canonical reference '{name}' because it is not namespaced.");
+
            continue;
+
        };
+

+
        let name = name.strip_namespace();
+

+
        let canonical = match identity.rules().canonical(name.clone(), repo) {
+
            Ok(Some(canonical)) => canonical,
+
            Ok(None) => continue,
+
            Err(e) => {
+
                log::warn!(target: "worker", "Failed to get canonical tips for {name}: {e}");
+
                continue;
+
            }
+
        };
+

+
        match canonical.quorum(&repo.backend) {
+
            Err(err) => {
+
                log::warn!(
+
                    target: "worker",
+
                    "Failed to calculate canonical tip: {}",
+
                    err,
+
                );
+
                continue;
+
            }
+
            Ok((refname, oid)) => {
+
                if let Err(e) = repo.backend.reference(
+
                    refname.clone().as_str(),
+
                    *oid,
+
                    true,
+
                    "set-canonical-reference from fetch (radicle)",
+
                ) {
+
                    log::warn!(
+
                        target: "worker",
+
                        "Failed to set canonical tip for {}->{}: {e}",
+
                        refname,
+
                        oid
+
                    );
+
                }
+
            }
+
        }
+
    }
+
    Ok(())
+
}
modified radicle-node/src/worker/fetch/error.rs
@@ -65,3 +65,11 @@ pub enum Handle {
    #[error(transparent)]
    Repository(#[from] radicle::storage::RepositoryError),
}
+

+
#[derive(Debug, Error)]
+
pub enum Canonical {
+
    #[error(transparent)]
+
    Identity(#[from] radicle::storage::RepositoryError),
+
    #[error(transparent)]
+
    Rules(#[from] git::canonical::rules::ValidationError),
+
}
modified radicle-remote-helper/Cargo.toml
@@ -11,7 +11,7 @@ build = "build.rs"
[dependencies]
thiserror = { version = "1" }
log = { version = "0.4.17" }
-
radicle-git-ext = { version = "0.8.0" }
+
radicle-git-ext = { version = "0.8" }

[dependencies.radicle]
path = "../radicle"
modified radicle-remote-helper/src/push.rs
@@ -14,7 +14,6 @@ use radicle::cob::patch::cache::Patches as _;
use radicle::crypto::Signer;
use radicle::explorer::ExplorerResource;
use radicle::git::canonical;
-
use radicle::git::canonical::Canonical;
use radicle::identity::Did;
use radicle::node;
use radicle::node::{Handle, NodeId};
@@ -202,6 +201,10 @@ pub fn run(
        }
    }
    let delegates = stored.delegates()?;
+
    let identity = stored.identity()?;
+
    let project = identity.project()?;
+
    let canonical_ref = git::refs::branch(project.default_branch());
+
    let mut set_canonical_refs: Vec<(git::Qualified, git::Oid)> = Vec::with_capacity(specs.len());

    // For each refspec, push a ref or delete a ref.
    for spec in specs {
@@ -212,7 +215,6 @@ pub fn run(
            Command::Delete(dst) => {
                // Delete refs.
                let refname = nid.to_namespace().join(dst);
-
                let (canonical_ref, _) = &stored.head()?;

                if *dst == canonical_ref.to_ref_string() && delegates.contains(&Did::from(nid)) {
                    return Err(Error::DeleteForbidden(dst.clone()));
@@ -261,8 +263,7 @@ pub fn run(
                        )
                    } else {
                        let identity = stored.identity()?;
-
                        let project = identity.project()?;
-
                        let canonical_ref = git::refs::branch(project.default_branch());
+
                        let rules = identity.rules();
                        let me = Did::from(nid);

                        // If we're trying to update the canonical head, make sure
@@ -271,65 +272,52 @@ pub fn run(
                        //
                        // Note that we *do* allow rolling back to a previous commit on the
                        // canonical branch.
-
                        if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
-
                            let head = working.find_reference(src.as_str())?;
-
                            let head = head.peel_to_commit()?.id();
-

-
                            let mut canonical = Canonical::default_branch(
-
                                stored,
-
                                &project,
-
                                identity.delegates().as_ref(),
-
                            )?;
-
                            let converges = canonical::converges(
-
                                canonical
-
                                    .tips()
-
                                    .filter_map(|(did, tip)| (*did != me).then_some(tip)),
-
                                head.into(),
-
                                &working,
-
                            )?;
-
                            if converges {
-
                                canonical.modify_vote(me, head.into());
-
                            }

-
                            match canonical.quorum(identity.threshold(), &working) {
-
                                Ok(canonical_oid) => {
-
                                    // Canonical head is an ancestor of head.
-
                                    let is_ff = head == *canonical_oid
-
                                        || working.graph_descendant_of(head, *canonical_oid)?;
-

-
                                    if !is_ff && !converges {
-
                                        if hints {
-
                                            hint(
-
                                                "you are attempting to push a commit that would cause \
-
                                                 your upstream to diverge from the canonical head",
-
                                            );
-
                                            hint(
-
                                                "to integrate the remote changes, run `git pull --rebase` \
-
                                                 and try again",
-
                                            );
+
                        if let Some(mut canonical) = rules.canonical(dst.clone(), stored)? {
+
                            if canonical.is_allowed(&me) {
+
                                let head = working.find_reference(src.as_str())?;
+
                                let head = head.peel_to_commit()?.id();
+
                                let converges =
+
                                    canonical.converges(&working, (&me, &head.into()))?;
+

+
                                // If `canonical` is empty then we're creating a new reference.
+
                                // If we're the only delegate then we need to modify our vote.
+
                                if converges || canonical.has_no_tips() || canonical.is_only(&me) {
+
                                    canonical.modify_vote(me, head.into());
+
                                }
+

+
                                match canonical.quorum(&working) {
+
                                    Ok((dst, canonical_oid)) => {
+
                                        // Canonical head is an ancestor of head.
+
                                        let is_ff = head == *canonical_oid
+
                                            || working.graph_descendant_of(head, *canonical_oid)?;
+

+
                                        if !is_ff && !converges {
+
                                            if hints {
+
                                                hint(
+
                                                    format!("you are attempting to push a commit that would cause \
+
                                                    your upstream to diverge from the canonical reference {dst}"),
+
                                                );
+
                                                hint(
+
                                                    "to integrate the remote changes, run `git pull --rebase` \
+
                                                    and try again",
+
                                                );
+
                                            }
+
                                            return Err(Error::HeadsDiverge(
+
                                                head.into(),
+
                                                canonical_oid,
+
                                            ));
                                        }
-
                                        return Err(Error::HeadsDiverge(
-
                                            head.into(),
-
                                            canonical_oid,
-
                                        ));
+
                                        set_canonical_refs
+
                                            .push((dst.clone().to_owned(), canonical_oid));
+
                                    }
+
                                    Err(canonical::QuorumError::Git(e)) => return Err(e.into()),
+
                                    Err(e) => {
+
                                        warn(e.to_string());
+
                                        warn("it is recommended to find a commit to agree upon");
                                    }
                                }
-
                                Err(canonical::QuorumError::Diverging(e)) => {
-
                                    warn(format!(
-
                                        "could not determine canonical tip for `{canonical_ref}`"
-
                                    ));
-
                                    warn(e.to_string());
-
                                    warn("it is recommended to find a commit to agree upon");
-
                                }
-
                                Err(canonical::QuorumError::NoCandidates(e)) => {
-
                                    warn(format!(
-
                                        "could not determine canonical tip for `{canonical_ref}`"
-
                                    ));
-
                                    warn(e.to_string());
-
                                    warn("it is recommended to find a commit to agree upon");
-
                                }
-
                                Err(e) => return Err(e.into()),
-
                            };
+
                            }
                        }
                        push(src, &dst, *force, &nid, &working, stored, patches, &signer)
                    }
@@ -352,16 +340,50 @@ pub fn run(
    if !ok.is_empty() {
        let _ = stored.sign_refs(&signer)?;

-
        // N.b. if an error occurs then there may be no quorum
-
        if let Ok(head) = stored.set_head() {
-
            if head.is_updated() {
+
        for (refname, oid) in &set_canonical_refs {
+
            let print_update = || {
                eprintln!(
-
                    "{} Canonical head updated to {}",
+
                    "{} Canonical head for {} updated to {}",
                    term::format::positive("✓"),
-
                    term::format::secondary(head.new),
-
                );
+
                    term::format::secondary(refname),
+
                    term::format::secondary(oid),
+
                )
+
            };
+

+
            // N.b. special case for handling the canonical ref, since it
+
            // creates a symlink to HEAD
+
            if *refname == canonical_ref
+
                && stored
+
                    .set_head()
+
                    .map(|head| head.is_updated())
+
                    .unwrap_or(false)
+
            {
+
                print_update();
+
                continue;
            }
-
        };
+

+
            match stored.backend.refname_to_id(refname.as_str()) {
+
                Ok(new) if new != **oid => {
+
                    stored.backend.reference(
+
                        refname.as_str(),
+
                        **oid,
+
                        true,
+
                        "set-canonical-reference from git-push (radicle)",
+
                    )?;
+
                    print_update();
+
                }
+
                Err(e) if e.code() == git::raw::ErrorCode::NotFound => {
+
                    stored.backend.reference(
+
                        refname.as_str(),
+
                        **oid,
+
                        true,
+
                        "set-canonical-reference from git-push (radicle)",
+
                    )?;
+
                    print_update();
+
                }
+
                _ => {}
+
            }
+
        }

        if !opts.no_sync {
            if profile.policies()?.is_seeding(&stored.id)? {
modified radicle-tools/Cargo.toml
@@ -9,7 +9,7 @@ edition = "2021"
anyhow = { version = "1" }
# N.b. this is required to use macros, even though it's re-exported
# through radicle
-
radicle-git-ext = { version = "0.8.0", features = ["serde"] }
+
radicle-git-ext = { version = "0.8", features = ["serde"] }

[dependencies.radicle]
version = "0"
modified radicle/Cargo.toml
@@ -29,11 +29,12 @@ once_cell = { version = "1.13" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
siphasher = { version = "1.0.0" }
-
radicle-git-ext = { version = "0.8.0", features = ["serde"] }
+
radicle-git-ext = { version = "0.8", features = ["serde"] }
sqlite = { version = "0.32.0", features = ["bundled"] }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
unicode-normalization = { version = "0.1" }
+
fast-glob = { version = "0.3.2" }

[dependencies.chrono]
version = "0.4.0"
modified radicle/src/cob/identity.rs
@@ -1068,11 +1068,6 @@ mod test {
            .unwrap_err();
        assert_eq!(identity.current, r0);

-
        // Change threshold to `2`, even though there's only one delegate. This should
-
        // fail as it makes the master branch immutable.
-
        doc.threshold = 2;
-
        assert!(doc.clone().verified().is_err());
-

        // Let's add another delegate.
        doc.delegate(bob.public_key().into());
        // The update should go through now.
@@ -1274,13 +1269,16 @@ mod test {
            .unwrap();

        bob.repo.fetch(alice);
-
        let a3 = alice_identity.redact(a2, &alice.signer).unwrap();
+
        let a3 = cob::git::stable::with_advanced_timestamp(|| {
+
            alice_identity.redact(a2, &alice.signer).unwrap()
+
        });
        assert!(alice_identity.revision(&a1).is_some());
        assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);

        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
-
        let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();
-

+
        let b1 = cob::git::stable::with_advanced_timestamp(|| {
+
            bob_identity.accept(&a2, &bob.signer).unwrap()
+
        });
        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
        bob.repo.fetch(alice);
@@ -1442,7 +1440,7 @@ mod test {

        eve.repo.fetch(bob);
        eve_identity.reload().unwrap();
-
        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1, e2]);
+
        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, e1, e2, b1]);

        // Her revision is there, although stale, since another revision was accepted since.
        // However, it wasn't pruned, even though rejecting an accepted revision is an error.
@@ -1568,7 +1566,6 @@ mod test {

        // Add Bob as a delegate, and sign it.
        doc.delegate(bob.public_key().into());
-
        doc.threshold = 2;
        identity
            .update("Add bob", "", &doc.clone().verified().unwrap(), &alice)
            .unwrap();
modified radicle/src/cob/patch.rs
@@ -1078,8 +1078,13 @@ impl Patch {
                        acc
                    },
                );
-
                // Discard revisions that weren't merged by a threshold of delegates.
-
                merges.retain(|_, count| *count >= identity.threshold());
+

+
                {
+
                    if let Some(threshold) = identity.default_branch_threshold()? {
+
                        // Discard revisions that weren't merged by a threshold of delegates.
+
                        merges.retain(|_, count| *count >= threshold);
+
                    }
+
                }

                match merges.into_keys().collect::<Vec<_>>().as_slice() {
                    [] => {
@@ -2814,6 +2819,7 @@ mod test {
    use crate::cob::common::CodeRange;
    use crate::cob::test::Actor;
    use crate::crypto::test::signer::MockSigner;
+
    use crate::git::canonical::rules::RawRules;
    use crate::identity;
    use crate::patch::cache::Patches as _;
    use crate::profile::env;
@@ -3076,6 +3082,7 @@ mod test {
            gen::<Project>(1),
            vec![alice.did()],
            1,
+
            RawRules::default(),
            identity::Visibility::Public,
        )
        .verified()
modified radicle/src/git/canonical.rs
@@ -1,27 +1,32 @@
+
pub mod rules;
+
pub use rules::{RawRule, Rules, ValidRule};
+

use std::collections::BTreeMap;
-
use std::fmt;

-
use nonempty::NonEmpty;
use raw::Repository;
use thiserror::Error;

use crate::prelude::Did;
-
use crate::prelude::Project;
use crate::storage::ReadRepository;

use super::raw;
-
use super::{lit, Oid, Qualified};
+
use super::{Oid, Qualified};

/// A collection of [`Did`]s and their [`Oid`]s that is the tip for a given
/// reference for that [`Did`].
///
/// The general construction of `Canonical` is by using the
-
/// [`Canonical::reference`] constructor. For the default branch of a
-
/// [`Project`], use [`Canonical::default_branch`].
+
/// [`Canonical::new`] constructor.
///
/// `Canonical` can then be used for performing calculations about the
/// canonicity of the reference, most importantly the [`Canonical::quorum`].
-
pub struct Canonical {
+
///
+
/// References to the refname and the matched rule are kept, as they
+
/// are very handy for generating error messages.
+
#[derive(Debug)]
+
pub struct Canonical<'a, 'b> {
+
    refname: Qualified<'a>,
+
    rule: &'b ValidRule,
    tips: BTreeMap<Did, Oid>,
}

@@ -29,134 +34,68 @@ pub struct Canonical {
#[derive(Debug, Error)]
pub enum QuorumError {
    /// Could not determine a quorum [`Oid`], due to diverging tips.
-
    #[error("could not determine canonical reference tip, {0}")]
-
    Diverging(Diverging),
+
    #[error("could not determine tip for canonical reference '{refname}', found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")]
+
    Diverging {
+
        refname: String,
+
        threshold: usize,
+
        base: Oid,
+
        longest: Oid,
+
        head: Oid,
+
    },
    /// Could not determine a base candidate from the given set of delegates.
-
    #[error("could not determine canonical reference tip, {0}")]
-
    NoCandidates(NoCandidates),
+
    #[error("could not determine tip for canonical reference '{refname}', no commit with at least {threshold} vote(s) found (threshold not met)")]
+
    NoCandidates { refname: String, threshold: usize },
    /// An error occurred from [`git2`].
    #[error(transparent)]
    Git(#[from] git2::Error),
}

-
/// No candidates were found for the [`Canonical::quorum`] calculation.
-
///
-
/// The [`fmt::Display`] is used in [`QuorumError`], to provide information on
-
/// the threshold and delegates in the calculation.
-
#[derive(Debug)]
-
pub struct NoCandidates {
-
    threshold: usize,
-
}
-

-
impl fmt::Display for NoCandidates {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        let NoCandidates { threshold } = self;
-
        write!(
-
            f,
-
            "no commit found with at least {threshold} vote(s) (threshold not met)"
-
        )
-
    }
-
}
-

-
/// Diverging commits were found during the [`Canonical::quorum`] calculation.
-
///
-
/// The [`fmt::Display`] is used in [`QuorumError`], to provide information on
-
/// the threshold, base commit, and the two diverging commits, in the
-
/// calculation.
-
#[derive(Debug)]
-
pub struct Diverging {
-
    threshold: usize,
-
    base: Oid,
-
    longest: Oid,
-
    head: Oid,
-
}
-

-
impl fmt::Display for Diverging {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        let Diverging {
-
            threshold,
-
            base,
-
            longest,
-
            head,
-
        } = self;
-
        write!(f, "found diverging commits {longest} and {head}, with base commit {base} and threshold {threshold}")
-
    }
-
}
-

-
impl Canonical {
-
    /// Construct the set of canonical tips of the `Project::default_branch` for
-
    /// the given `delegates`.
-
    pub fn default_branch<S>(
-
        repo: &S,
-
        project: &Project,
-
        delegates: &NonEmpty<Did>,
-
    ) -> Result<Self, raw::Error>
-
    where
-
        S: ReadRepository,
-
    {
-
        Self::reference(
-
            repo,
-
            delegates,
-
            &lit::refs_heads(project.default_branch()).into(),
-
        )
-
    }
-

-
    /// Construct the set of canonical tips given for the given `delegates` and
-
    /// the reference `name`.
-
    pub fn reference<S>(
-
        repo: &S,
-
        delegates: &NonEmpty<Did>,
-
        name: &Qualified,
-
    ) -> Result<Self, raw::Error>
+
impl<'a, 'b> Canonical<'a, 'b> {
+
    /// Construct the set of canonical tips given for the given `rule` and
+
    /// the reference `refname`.
+
    pub fn new<S>(repo: &S, refname: Qualified<'a>, rule: &'b ValidRule) -> Result<Self, raw::Error>
    where
        S: ReadRepository,
    {
        let mut tips = BTreeMap::new();
-
        for delegate in delegates.iter() {
-
            match repo.reference_oid(delegate, name) {
+
        for delegate in rule.allowed().iter() {
+
            match repo.reference_oid(delegate, &refname) {
                Ok(tip) => {
                    tips.insert(*delegate, tip);
                }
                Err(e) if super::ext::is_not_found_err(&e) => {
                    log::warn!(
                        target: "radicle",
-
                        "Missing `refs/namespaces/{}/{name}` while calculating the canonical reference",
-
                        delegate.as_key()
+
                        "Missing `refs/namespaces/{delegate}/{refname}` while calculating the canonical reference",
                    );
                }
                Err(e) => return Err(e),
            }
        }
-
        Ok(Canonical { tips })
+
        Ok(Canonical {
+
            refname,
+
            tips,
+
            rule,
+
        })
    }

    /// Return the set of [`Did`]s and their [`Oid`] tip.
    pub fn tips(&self) -> impl Iterator<Item = (&Did, &Oid)> {
        self.tips.iter()
    }
-
}

-
/// Check that a given `target` converges with any of the provided `tips`.
-
///
-
/// It converges if the `target` is either equal to, ahead of, or behind any of
-
/// the tips.
-
pub fn converges<'a>(
-
    tips: impl Iterator<Item = &'a Oid>,
-
    target: Oid,
-
    repo: &Repository,
-
) -> Result<bool, raw::Error> {
-
    for tip in tips {
-
        match repo.graph_ahead_behind(*target, **tip)? {
-
            (0, 0) => return Ok(true),
-
            (ahead, behind) if ahead > 0 && behind == 0 => return Ok(true),
-
            (ahead, behind) if behind > 0 && ahead == 0 => return Ok(true),
-
            (_, _) => {}
-
        }
+
    /// Returns `true` if there were no tips found for any of the DIDs for
+
    /// the given reference.
+
    ///
+
    /// N.b. this may be the case when a new reference is being created.
+
    pub fn has_no_tips(&self) -> bool {
+
        self.tips.is_empty()
+
    }
+

+
    pub fn refname(&self) -> &Qualified {
+
        &self.refname
    }
-
    Ok(false)
-
}

-
impl Canonical {
    /// In some cases, we allow the vote to be modified. For example, when the
    /// `did` is pushing a new commit, we may want to see if the new commit will
    /// reach a quorum.
@@ -164,6 +103,41 @@ impl Canonical {
        self.tips.insert(did, new);
    }

+
    /// Check that the provided `did` is part of the set of allowed
+
    /// DIDs of the matching rule.
+
    pub fn is_allowed(&self, did: &Did) -> bool {
+
        self.rule.allowed().contains(did)
+
    }
+

+
    /// Check that the provided `did` is the only DID in the set of allowed
+
    /// DIDs of the matching rule.
+
    pub fn is_only(&self, did: &Did) -> bool {
+
        self.rule.allowed().is_only(did)
+
    }
+

+
    /// Checks that setting the given candidate tip would converge with at least
+
    /// one other known tip.
+
    ///
+
    /// It converges if the candidate Oid is either equal to, ahead of, or behind any of
+
    /// the tips.
+
    pub fn converges(
+
        &self,
+
        repo: &Repository,
+
        candidate: (&Did, &Oid),
+
    ) -> Result<bool, raw::Error> {
+
        for tip in self
+
            .tips
+
            .iter()
+
            .filter_map(|(did, tip)| (did != candidate.0).then_some(tip))
+
        {
+
            let (ahead, behind) = repo.graph_ahead_behind(**candidate.1, **tip)?;
+
            if ahead * behind == 0 {
+
                return Ok(true);
+
            }
+
        }
+
        Ok(false)
+
    }
+

    /// Computes the quorum or "canonical" tip based on the tips, of `Canonical`,
    /// and the threshold. This can be described as the latest commit that is
    /// included in at least `threshold` histories. In case there are multiple tips
@@ -171,7 +145,7 @@ impl Canonical {
    ///
    /// Also returns an error if `heads` is empty or `threshold` cannot be
    /// satisified with the number of heads given.
-
    pub fn quorum(&self, threshold: usize, repo: &raw::Repository) -> Result<Oid, QuorumError> {
+
    pub fn quorum(self, repo: &raw::Repository) -> Result<(Qualified<'a>, Oid), QuorumError> {
        let mut candidates = BTreeMap::<_, usize>::new();

        // Build a list of candidate commits and count how many "votes" each of them has.
@@ -196,11 +170,12 @@ impl Canonical {
            }
        }
        // Keep commits which pass the threshold.
-
        candidates.retain(|_, votes| *votes >= threshold);
+
        candidates.retain(|_, votes| *votes >= self.threshold());

-
        let (mut longest, _) = candidates
-
            .pop_first()
-
            .ok_or(QuorumError::NoCandidates(NoCandidates { threshold }))?;
+
        let (mut longest, _) = candidates.pop_first().ok_or(QuorumError::NoCandidates {
+
            refname: self.refname.to_string(),
+
            threshold: self.threshold(),
+
        })?;

        // Now that all scores are calculated, figure out what is the longest branch
        // that passes the threshold. In case of divergence, return an error.
@@ -234,15 +209,20 @@ impl Canonical {
                //            o (base)
                //            |
                //
-
                return Err(QuorumError::Diverging(Diverging {
-
                    threshold,
+
                return Err(QuorumError::Diverging {
+
                    refname: self.refname.to_string(),
+
                    threshold: self.threshold(),
                    base: base.into(),
                    longest,
                    head: *head,
-
                }));
+
                });
            }
        }
-
        Ok((*longest).into())
+
        Ok((self.refname, (*longest).into()))
+
    }
+

+
    fn threshold(&self) -> usize {
+
        (*self.rule.threshold()).into()
    }
}

@@ -263,7 +243,7 @@ mod tests {
        threshold: usize,
        repo: &git::raw::Repository,
    ) -> Result<Oid, QuorumError> {
-
        let tips = heads
+
        let tips: BTreeMap<Did, Oid> = heads
            .iter()
            .enumerate()
            .map(|(i, head)| {
@@ -272,7 +252,25 @@ mod tests {
                (did, (*head).into())
            })
            .collect();
-
        Canonical { tips }.quorum(threshold, repo)
+

+
        let refname =
+
            git::refs::branch(git_ext::ref_format::RefStr::try_from_str("master").unwrap());
+

+
        let rule: RawRule = crate::git::canonical::rules::Rule::new(
+
            crate::git::canonical::rules::Allowed::Delegates,
+
            threshold,
+
        );
+
        let rule = rule
+
            .validate(&mut |_| Ok(crate::identity::doc::Delegates::new(tips.keys().cloned())?))
+
            .unwrap();
+

+
        Canonical {
+
            refname,
+
            tips,
+
            rule: &rule,
+
        }
+
        .quorum(repo)
+
        .map(|(_, oid)| oid)
    }

    #[test]
@@ -334,9 +332,6 @@ mod tests {
        assert_eq!(quorum(&[*c0], 1, &repo).unwrap(), c0);
        assert_eq!(quorum(&[*c1], 1, &repo).unwrap(), c1);
        assert_eq!(quorum(&[*c2], 1, &repo).unwrap(), c2);
-
        assert_eq!(quorum(&[*c0], 0, &repo).unwrap(), c0);
-
        assert_matches!(quorum(&[], 0, &repo), Err(QuorumError::NoCandidates(_)));
-
        assert_matches!(quorum(&[*c0], 2, &repo), Err(QuorumError::NoCandidates(_)));

        //  C1
        //  |
@@ -362,23 +357,23 @@ mod tests {
        //  C0
        assert_matches!(
            quorum(&[*c1, *c2, *b2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*c2, *b2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*b2, *c2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*c2, *b2], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*b2, *c2], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_eq!(quorum(&[*c1, *c2, *b2], 2, &repo).unwrap(), c1);
        assert_eq!(quorum(&[*c1, *c2, *b2], 3, &repo).unwrap(), c1);
@@ -386,7 +381,7 @@ mod tests {
        assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
        assert_matches!(
            quorum(&[*b2, *b2, *c2, *c2], 2, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );

        // B2 C2 C3
@@ -397,15 +392,15 @@ mod tests {
        assert_eq!(quorum(&[*b2, *c2, *c2], 2, &repo).unwrap(), c2);
        assert_matches!(
            quorum(&[*b2, *c2, *c2], 3, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );

        //  B2 C2
@@ -415,19 +410,19 @@ mod tests {
        //   C0
        assert_matches!(
            quorum(&[*c2, *b2, *a1], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*c2, *b2, *a1], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*c2, *b2, *a1], 3, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*c1, *c2, *b2, *a1], 4, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 2, &repo).unwrap(), c1,);
        assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 3, &repo).unwrap(), c1,);
@@ -435,23 +430,23 @@ mod tests {
        assert_eq!(quorum(&[*c0, *c1, *c2, *b2, *a1], 4, &repo).unwrap(), c0,);
        assert_matches!(
            quorum(&[*a1, *a1, *c2, *c2, *c1], 2, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*a1, *a1, *c2, *c2, *c1], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*a1, *a1, *c2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*b2, *b2, *c2, *c2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*b2, *b2, *c2, *c2, *a1], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );

        //    M2  M1
@@ -464,27 +459,27 @@ mod tests {
        assert_eq!(quorum(&[*m1], 1, &repo).unwrap(), m1);
        assert_matches!(
            quorum(&[*m1, *m2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m2, *m1], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m1, *m2], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*m1, *m2, *c2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m1, *a1], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m1, *a1], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_eq!(quorum(&[*m1, *m2, *b2, *c1], 4, &repo).unwrap(), c1);
        assert_eq!(quorum(&[*m1, *m1, *b2], 2, &repo).unwrap(), m1);
@@ -523,11 +518,11 @@ mod tests {
        //      C0
        assert_matches!(
            quorum(&[*m1, *m2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m1, *m2], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );

        let m3 = fixtures::commit("M3", &[*c2, *c1], &repo);
@@ -539,27 +534,27 @@ mod tests {
        //      C0
        assert_matches!(
            quorum(&[*m1, *m3], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m1, *m3], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*m3, *m1], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m3, *m1], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
        assert_matches!(
            quorum(&[*m3, *m2], 1, &repo),
-
            Err(QuorumError::Diverging(_))
+
            Err(QuorumError::Diverging { .. })
        );
        assert_matches!(
            quorum(&[*m3, *m2], 2, &repo),
-
            Err(QuorumError::NoCandidates(_))
+
            Err(QuorumError::NoCandidates { .. })
        );
    }
}
added radicle/src/git/canonical/rules.rs
@@ -0,0 +1,1186 @@
+
//! Implementation of RIP-0004 Canonical References
+
//!
+
//! [`RawRules`] is intended to be deserialized and then validated into a set of
+
//! [`Rules`]. These can then be used to see if a [`Qualified`] reference
+
//! matches any of the rules. Using [`Rules::canonical`] will construct a
+
//! [`Canonical`] with the first matched rule, and this can be used to calculate
+
//! the [`Canonical::quorum`].
+

+
use core::fmt;
+
use std::cmp::Ordering;
+
use std::collections::BTreeMap;
+

+
use nonempty::NonEmpty;
+
use once_cell::sync::Lazy;
+
use serde::{Deserialize, Serialize};
+
use serde_json as json;
+
use thiserror::Error;
+

+
use crate::git;
+
use crate::git::canonical::Canonical;
+
use crate::git::fmt::{refname, RefString};
+
use crate::git::refspec::QualifiedPattern;
+
use crate::git::Qualified;
+
use crate::identity::{doc, Did};
+
use crate::storage::git::Repository;
+

+
const ASTERISK: char = '*';
+

+
static REFS_RAD: Lazy<RefString> = Lazy::new(|| refname!("refs/rad"));
+

+
/// Private trait to ensure that not any `Rule` can be deserialized.
+
/// Implementations are provided for `Allowed` and `usize` so that `RawRule`s
+
/// can be deserialized, while `ValidRule`s cannot – preventing deserialization
+
/// bugs for that type.
+
trait Sealed {}
+
impl Sealed for Allowed {}
+
impl Sealed for usize {}
+

+
/// A `Pattern` is a `QualifiedPattern` reference, however, it disallows any
+
/// references under the `refs/rad` hierarchy.
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(into = "QualifiedPattern", try_from = "QualifiedPattern")]
+
pub struct Pattern(QualifiedPattern<'static>);
+

+
impl fmt::Display for Pattern {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        f.write_str(self.0.as_str())
+
    }
+
}
+

+
impl From<Pattern> for QualifiedPattern<'static> {
+
    fn from(Pattern(pattern): Pattern) -> Self {
+
        pattern
+
    }
+
}
+

+
impl<'a> TryFrom<QualifiedPattern<'a>> for Pattern {
+
    type Error = PatternError;
+

+
    fn try_from(pattern: QualifiedPattern<'a>) -> Result<Self, Self::Error> {
+
        if pattern.starts_with(REFS_RAD.as_str()) {
+
            Err(PatternError::ProtectedRef {
+
                prefix: (*REFS_RAD).clone(),
+
                pattern: pattern.to_owned(),
+
            })
+
        } else {
+
            Ok(Self(pattern.to_owned()))
+
        }
+
    }
+
}
+

+
impl<'a> TryFrom<Qualified<'a>> for Pattern {
+
    type Error = PatternError;
+

+
    fn try_from(name: Qualified<'a>) -> Result<Self, Self::Error> {
+
        Self::try_from(QualifiedPattern::from(name))
+
    }
+
}
+

+
impl Pattern {
+
    /// Check if the `refname` matches the rule's `refspec`.
+
    pub fn matches(&self, refname: &Qualified) -> bool {
+
        // N.b. Git's refspecs do not quite match with glob-star semantics. A
+
        // single `*` in a refspec is expected to match all references under
+
        // that namespace, even if they are further down the hierarchy.
+
        // Thus, the following rules are applied:
+
        //
+
        //   - a trailing `*` changes to `**/*`
+
        //   - a `*` in between path components changes to `**`
+
        let spec = match self.0.as_str().split_once(ASTERISK) {
+
            None => self.0.to_string(),
+
            // Expand `refs/tags/*` to `refs/tags/**/*`
+
            Some((prefix, "")) => {
+
                let mut spec = prefix.to_string();
+
                spec.push_str("**/*");
+
                spec
+
            }
+
            // Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
+
            Some((prefix, suffix)) => {
+
                let mut spec = prefix.to_string();
+
                spec.push_str("**");
+
                spec.push_str(suffix);
+
                spec
+
            }
+
        };
+
        fast_glob::glob_match(&spec, refname.as_str())
+
    }
+
}
+

+
impl AsRef<QualifiedPattern<'static>> for Pattern {
+
    fn as_ref(&self) -> &QualifiedPattern<'static> {
+
        &self.0
+
    }
+
}
+

+
impl PartialOrd for Pattern {
+
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+
        Some(self.cmp(other))
+
    }
+
}
+

+
/// Patterns are ordered by their specificity.
+
///
+
/// This is heavily influenced by the evaluation priority of Rules. For a
+
/// candidate reference name, we want the rule associated with the most specific
+
/// pattern to apply, i.e. to take priority over all other rules with less
+
/// specific patterns.
+
///
+
/// For two patterns `φ` and `ψ`, we say that "`φ` is more specific than `ψ`", denoted
+
/// `φ < ψ` if:
+
///
+
///  1. The number of components in `φ` is larger than the number of components
+
///     in `ψ`. (Note that the number of components is equal to the number of
+
///     occurrences of the symbol '/' in the pattern, plus 1).
+
///     The justification is, that refnames might be interpreted as a hierarchy
+
///     where a match on more components would mean a match at a lower level in
+
///     the hierarchy, thus being more specific.
+
///     Imagine a refname hierarchy that maps to a corporate hierarchy.
+
///     The pattern "department-1" matches all refnames that are administered
+
///     by a particular department, and thus is not very specific.
+
///     To contrast, the pattern "department-1/team-a/project-i/nice-feature"
+
///     is very specific as it matches all refnames that relate to the
+
///     development of a particular feature for a particular project by a
+
///     particular team.
+
///     Note that this would also apply when the connection between the `φ` and `ψ`
+
///     is not as obvious, e.g. also `a/b/c/d/* < */x`.
+
///
+
/// (Note that for the following items, one may assume that `φ` and `ψ` have the
+
/// same number of components.)
+
///
+
///  2. If path component i of `φ`, denoted `φ[i]`, is more specific than path
+
///     component i of `ψ`, denoted `ψ[i]`. This is the case if:
+
///      a. `φ[i]` does not contain an asterisk and `ψ[i]` contains an asterisk,
+
///         i.e. the symbol `*`, e.g. `a < * and abc < a*`.
+
///         Note that this is important to capture specificity accross
+
///         components, i.e. to conclude that `a/b/* < a/*/c`.
+
///      b. Both `φ[i]` and `ψ[i]` contain an asterisk.
+
///          A. The asterisk in `φ[i]` is further right than the asterisk in `φ[i]`,
+
///             e.g. `aa* < a*`.
+
///          B. The asterisk in `φ[i]` and `ψ[i]` is equally far to the right,
+
///             and `φ[i]` is longer than `ψ[i]`, e.g. `a*b < a*`.
+
///
+
///  3. Otherwise, fall back to a lexicographic ordering.
+
///
+
/// Some examples (justification in parentheses):
+
///
+
/// ```text, no_run
+
/// refs/tags/release/candidates/* <(1.)   refs/tags/release/* <(1.) refs/tags/*
+
/// refs/tags/v1.0                 <(2.a.) refs/tags/*
+
/// refs/heads/*                   <(3.)   refs/tags/*
+
/// refs/heads/main                <(3.)   refs/tags/v1.0
+
/// ```
+
impl Ord for Pattern {
+
    fn cmp(&self, other: &Self) -> Ordering {
+
        #[derive(Debug, Clone, Copy)]
+
        #[repr(i8)]
+
        enum ComponentOrdering {
+
            MatchLength(Ordering),
+
            Lexicographic(Ordering),
+
        }
+

+
        impl ComponentOrdering {
+
            fn merge(&mut self, other: Self) {
+
                *self = match (*self, other) {
+
                    (Self::Lexicographic(Ordering::Equal), Self::Lexicographic(other)) => {
+
                        Self::Lexicographic(other)
+
                    }
+
                    (Self::Lexicographic(_), Self::MatchLength(other)) => Self::MatchLength(other),
+
                    (Self::MatchLength(Ordering::Equal), Self::MatchLength(other)) => {
+
                        Self::MatchLength(other)
+
                    }
+
                    (clone, _) => clone,
+
                }
+
            }
+
        }
+

+
        impl From<ComponentOrdering> for Ordering {
+
            fn from(value: ComponentOrdering) -> Self {
+
                match value {
+
                    ComponentOrdering::MatchLength(ordering) => ordering,
+
                    ComponentOrdering::Lexicographic(ordering) => ordering,
+
                }
+
            }
+
        }
+

+
        impl Default for ComponentOrdering {
+
            /// The weakest value of Self, which will be absorbed by any
+
            /// other in [`ComponentOrdering::merge`].
+
            fn default() -> Self {
+
                Self::Lexicographic(Ordering::Equal)
+
            }
+
        }
+

+
        use git::refspec::Component;
+

+
        fn cmp_component(lhs: Component<'_>, rhs: Component<'_>) -> ComponentOrdering {
+
            let (l, r) = (lhs.as_str(), rhs.as_str());
+
            match (l.find(ASTERISK), r.find(ASTERISK)) {
+
                // (2.a.)
+
                (Some(_), None) => ComponentOrdering::MatchLength(Ordering::Greater),
+
                // (2.a.)
+
                (None, Some(_)) => ComponentOrdering::MatchLength(Ordering::Less),
+
                (Some(li), Some(ri)) => {
+
                    if li != ri {
+
                        // (2.b.A)
+
                        ComponentOrdering::MatchLength(li.cmp(&ri).reverse())
+
                    } else if l.len() != r.len() {
+
                        // (2.b.B)
+
                        ComponentOrdering::MatchLength(l.len().cmp(&r.len()).reverse())
+
                    } else {
+
                        // (3.)
+
                        ComponentOrdering::Lexicographic(l.cmp(r))
+
                    }
+
                }
+
                // (3.)
+
                (None, None) => ComponentOrdering::Lexicographic(l.cmp(r)),
+
            }
+
        }
+

+
        let mut result = ComponentOrdering::default();
+
        let mut lhs = self.0.components();
+
        let mut rhs = other.0.components();
+
        loop {
+
            match (lhs.next(), rhs.next()) {
+
                (None, Some(_)) => return Ordering::Greater, // (1.)
+
                (Some(_), None) => return Ordering::Less,    // (1.)
+
                (Some(lhs), Some(rhs)) => {
+
                    result.merge(cmp_component(lhs, rhs));
+
                }
+
                (None, None) => return result.into(),
+
            }
+
        }
+
    }
+
}
+

+
/// A [`Rule`] that can be serialized and deserialized safely.
+
///
+
/// Should be converted to a [`ValidRule`] via [`Rule::validate`].
+
pub type RawRule = Rule<Allowed, usize>;
+

+
impl RawRule {
+
    /// Validate the `Rule` into a form that can be used for calculating
+
    /// canonical references.
+
    ///
+
    /// The `resolve` function is used to get the set of DIDs by inspecting the
+
    /// [`Allowed`] value. In most cases, if it is [`Allowed::Delegates`] then
+
    /// the closure will resolve the DIDs from the identity document, and if it
+
    /// is [`Allowed::Set`] it will validate the set.
+
    pub fn validate<R>(self, resolve: &mut R) -> Result<ValidRule, ValidationError>
+
    where
+
        R: Fn(Allowed) -> Result<doc::Delegates, ValidationError>,
+
    {
+
        let Self {
+
            allow: delegates,
+
            threshold,
+
            ..
+
        } = self;
+
        let allow = match &delegates {
+
            Allowed::Delegates => ResolvedDelegates::Delegates(resolve(delegates)?),
+
            Allowed::Set(_) => ResolvedDelegates::Set(resolve(delegates)?),
+
        };
+
        let threshold = doc::Threshold::new(threshold, &allow)?;
+
        Ok(Rule {
+
            allow,
+
            threshold,
+
            extensions: self.extensions,
+
        })
+
    }
+
}
+

+
/// A set of `RawRule`s that can be serialized and deserialized.
+
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct RawRules {
+
    /// The reference pattern that this rule applies to.
+
    ///
+
    /// Note that this can be a fully-qualified pattern, e.g. `refs/heads/qa`,
+
    /// as well as a wild-card pattern, e.g. `refs/tags/*`.
+
    #[serde(flatten)]
+
    pub rules: BTreeMap<Pattern, RawRule>,
+
}
+

+
impl RawRules {
+
    /// Returns an iterator over the [`Pattern`] and [`RawRule`] in the set of
+
    /// rules.
+
    pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &RawRule)> {
+
        self.rules.iter()
+
    }
+

+
    /// Add a new [`RawRule`] to the set of rules.
+
    ///
+
    /// Returns the replaced rule, if it existed.
+
    pub fn insert(&mut self, pattern: Pattern, rule: RawRule) -> Option<RawRule> {
+
        self.rules.insert(pattern, rule)
+
    }
+

+
    /// Remove the rule that matches the `pattern` parameter.
+
    ///
+
    /// Returns the rule if it existed.
+
    pub fn remove(&mut self, pattern: &Pattern) -> Option<RawRule> {
+
        self.rules.remove(pattern)
+
    }
+

+
    /// Check to see if there is an exact match for `refname` in the rules.
+
    pub fn exact_match(&self, refname: &Qualified) -> bool {
+
        let refname = refname.as_str();
+
        self.rules
+
            .iter()
+
            .any(|(pattern, _)| pattern.0.as_str() == refname)
+
    }
+

+
    /// Check if the `refname` matches any existing rules, including glob
+
    /// matches.
+
    pub fn matches(&self, refname: &Qualified) -> bool {
+
        self.rules
+
            .iter()
+
            .any(|(pattern, _)| pattern.matches(refname))
+
    }
+
}
+

+
impl Extend<(Pattern, RawRule)> for RawRules {
+
    fn extend<T: IntoIterator<Item = (Pattern, RawRule)>>(&mut self, iter: T) {
+
        self.rules.extend(iter)
+
    }
+
}
+

+
impl From<BTreeMap<Pattern, RawRule>> for RawRules {
+
    fn from(rules: BTreeMap<Pattern, RawRule>) -> Self {
+
        RawRules { rules }
+
    }
+
}
+

+
impl FromIterator<(Pattern, RawRule)> for RawRules {
+
    fn from_iter<T: IntoIterator<Item = (Pattern, RawRule)>>(iter: T) -> Self {
+
        iter.into_iter().collect::<BTreeMap<_, _>>().into()
+
    }
+
}
+

+
impl IntoIterator for RawRules {
+
    type Item = (Pattern, RawRule);
+
    type IntoIter = std::collections::btree_map::IntoIter<Pattern, RawRule>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.rules.into_iter()
+
    }
+
}
+

+
/// A [`Rule`] that has been validated. See [`Rules`] and [`Rules::matches`] for
+
/// its main usage.
+
///
+
/// N.b. a `ValidRule` can be serialized, however, it cannot be deserialized.
+
/// This is due to the fact that the `allow` field may have a value of
+
/// `delegates`. In those cases the value needs to be looked up via the identity
+
/// document and validated.
+
pub type ValidRule = Rule<ResolvedDelegates, doc::Threshold>;
+

+
impl ValidRule {
+
    /// Initialize a `ValidRule` for the default branch, given by `name`. The
+
    /// rule will contain the single `did` as the allowed DID, and use a
+
    /// threshold of `1`.
+
    ///
+
    /// Note that the serialization of the rule will use the `delegates` token
+
    /// for the rule. E.g.
+
    /// ```json, no_run
+
    /// {
+
    ///   "pattern": "refs/heads/main",
+
    ///   "allow": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"],
+
    ///   "threshold": 1
+
    /// }
+
    /// ```
+
    ///
+
    /// # Errors
+
    ///
+
    /// If the `name` reference begins with `refs/rad`.
+
    pub fn default_branch(did: Did, name: &git::RefStr) -> Result<(Pattern, Self), PatternError> {
+
        let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
+
        let rule = Self {
+
            allow: ResolvedDelegates::Delegates(doc::Delegates::from(did)),
+
            // N.B. this needs to be the minimum since we only have one
+
            // delegate.
+
            threshold: doc::Threshold::MIN,
+
            extensions: json::Map::new(),
+
        };
+
        Ok((pattern, rule))
+
    }
+
}
+

+
impl From<ValidRule> for RawRule {
+
    fn from(rule: ValidRule) -> Self {
+
        let Rule {
+
            allow,
+
            threshold,
+
            extensions,
+
        } = rule;
+
        Self {
+
            allow: allow.into(),
+
            threshold: threshold.into(),
+
            extensions,
+
        }
+
    }
+
}
+

+
/// A representation of a set of allowed DIDs.
+
///
+
/// `Allowed` is used in a `RawRule`.
+
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+
pub enum Allowed {
+
    /// Pointer to the identity document's set of delegates.
+
    #[serde(rename = "delegates")]
+
    #[default]
+
    Delegates,
+
    /// Explicit set of allowed DIDs.
+
    ///
+
    /// # Validation
+
    ///
+
    /// The set of allowed DIDs must be:
+
    ///   - Unique
+
    ///   - `1 <= delegates.len() <= 255`
+
    #[serde(untagged)]
+
    Set(NonEmpty<Did>),
+
}
+

+
impl From<NonEmpty<Did>> for Allowed {
+
    fn from(dids: NonEmpty<Did>) -> Self {
+
        Self::Set(dids)
+
    }
+
}
+

+
impl From<Did> for Allowed {
+
    fn from(did: Did) -> Self {
+
        Self::Set(NonEmpty::new(did))
+
    }
+
}
+

+
/// A marker `enum` that is used in a [`ValidRule`].
+
///
+
/// It ensures that a rule that has been deserialized, resolving the `delegates`
+
/// token to a set of DIDs, is still serialized back to the `delegates` token –
+
/// as opposed to serializing it to the set of DIDs.
+
///
+
/// The variants mirror the [`Allowed::Delegates`] and [`Allowed::Set`]
+
/// variants.
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+
#[serde(into = "Allowed")]
+
pub enum ResolvedDelegates {
+
    Delegates(doc::Delegates),
+
    Set(doc::Delegates),
+
}
+

+
impl From<ResolvedDelegates> for Allowed {
+
    fn from(ds: ResolvedDelegates) -> Self {
+
        match ds {
+
            ResolvedDelegates::Delegates(_) => Self::Delegates,
+
            ResolvedDelegates::Set(ds) => Self::Set(ds.into()),
+
        }
+
    }
+
}
+

+
impl std::ops::Deref for ResolvedDelegates {
+
    type Target = doc::Delegates;
+

+
    fn deref(&self) -> &Self::Target {
+
        match self {
+
            ResolvedDelegates::Delegates(ds) => ds,
+
            ResolvedDelegates::Set(ds) => ds,
+
        }
+
    }
+
}
+

+
/// A set of valid [`Rule`]s, where the set of DIDs and threshold are fully
+
/// resolved and valid. Since the rules are constructed via a `BTreeMap`, they
+
/// cannot be duplicated.
+
///
+
/// To construct the set of rules, use [`Rules::from_raw`], which validates a
+
/// set of [`RawRule`]s, and their [`Pattern`] references, into a set of
+
/// [`ValidRule`]s.
+
///
+
/// The `Rules` can then be used to construct a [`Canonical`] by providing a
+
/// [`Qualified`] reference to [`Rules::canonical`], returning the [`Canonical`]
+
/// for the first matched rule.
+
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
+
pub struct Rules {
+
    #[serde(flatten)]
+
    rules: BTreeMap<Pattern, ValidRule>,
+
}
+

+
impl FromIterator<(Pattern, ValidRule)> for Rules {
+
    fn from_iter<T: IntoIterator<Item = (Pattern, ValidRule)>>(iter: T) -> Self {
+
        Self {
+
            rules: iter.into_iter().collect(),
+
        }
+
    }
+
}
+

+
impl<'a> IntoIterator for &'a Rules {
+
    type Item = (&'a Pattern, &'a ValidRule);
+
    type IntoIter = std::collections::btree_map::Iter<'a, Pattern, ValidRule>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.rules.iter()
+
    }
+
}
+

+
impl IntoIterator for Rules {
+
    type Item = (Pattern, ValidRule);
+
    type IntoIter = std::collections::btree_map::IntoIter<Pattern, ValidRule>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.rules.into_iter()
+
    }
+
}
+

+
impl From<Rules> for RawRules {
+
    fn from(Rules { rules }: Rules) -> Self {
+
        Self {
+
            rules: rules
+
                .into_iter()
+
                .map(|(pattern, rule)| (pattern, rule.into()))
+
                .collect(),
+
        }
+
    }
+
}
+

+
impl Rules {
+
    /// Returns an iterator over the [`Pattern`] and [`ValidRule`] in the set of
+
    /// rules.
+
    pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &ValidRule)> {
+
        self.rules.iter()
+
    }
+

+
    /// Returns `true` is the set of rules is empty.
+
    pub fn is_empty(&self) -> bool {
+
        self.rules.is_empty()
+
    }
+

+
    /// Construct a set of `Rules` given a set of `RawRule`s.
+
    pub fn from_raw<R>(
+
        rules: impl IntoIterator<Item = (Pattern, RawRule)>,
+
        resolve: &mut R,
+
    ) -> Result<Self, ValidationError>
+
    where
+
        R: Fn(Allowed) -> Result<doc::Delegates, ValidationError>,
+
    {
+
        let valid = rules
+
            .into_iter()
+
            .map(|(pattern, rule)| rule.validate(resolve).map(|rule| (pattern, rule)))
+
            .collect::<Result<_, _>>()?;
+
        Ok(Self { rules: valid })
+
    }
+

+
    /// Return the matching rules for the given `refname`.
+
    pub fn matches<'a>(
+
        &self,
+
        refname: &Qualified<'a>,
+
    ) -> impl Iterator<Item = (&Pattern, &ValidRule)> + use<'a, '_> {
+
        let refname_cloned = refname.clone();
+
        self.rules
+
            .iter()
+
            .filter(move |(pattern, _)| pattern.matches(&refname_cloned))
+
    }
+

+
    /// Match given refname, take the most specific rule, and prepare evaluation
+
    /// as [`Canonical`]
+
    ///
+
    /// N.b. it will find the first rule that is most specific for the given
+
    /// `refname`.
+
    pub fn canonical<'a, 'b>(
+
        &'a self,
+
        refname: Qualified<'b>,
+
        repo: &Repository,
+
    ) -> Result<Option<Canonical<'b, 'a>>, git::raw::Error> {
+
        if let Some((_, rule)) = self.matches(&refname).next() {
+
            Ok(Some(Canonical::new(repo, refname, rule)?))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+
}
+

+
/// A `Rule` defines how a reference or set of references can be made canonical,
+
/// i.e. have a top-level `refs/*` entry – see [`Pattern`].
+
///
+
/// The [`Rule::allowed`] type is generic to allow for [`Allowed`] to be used
+
/// for serialization and deserialization, however, the use of
+
/// [`Rule::validate`] should be used to get a valid rule.
+
///
+
/// The [`Rule::threshold`], similarly, allows for [`doc::Threshold`] to be used, and
+
/// [`Rule::validate`] should be used to get a valid rule.
+
// N.b. it's safe to derive `Serialize` since we only allow constructing a
+
// `Rule` via `Rule::validate`, and we seal `Deserialize` by ensuring that only
+
// `RawRule` can be deserialized.
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(bound(deserialize = "D: Sealed + Deserialize<'de>, T: Sealed + Deserialize<'de>"))]
+
pub struct Rule<D, T> {
+
    /// The set of delegates that are considered for voting for this rule.
+
    allow: D,
+
    /// The threshold the votes must pass for the reference(s) to be considered
+
    /// canonical.
+
    threshold: T,
+

+
    /// Optional extensions in rules. This is intended to preserve backwards and
+
    /// forward-compatibility
+
    #[serde(skip_serializing_if = "json::Map::is_empty")]
+
    #[serde(flatten)]
+
    extensions: json::Map<String, json::Value>,
+
}
+

+
impl<D, T> Rule<D, T> {
+
    /// Construct a new `Rule` with the given `refspec`, `delegates`, and
+
    /// `threshold`.
+
    pub fn new(allow: D, threshold: T) -> Self {
+
        Self {
+
            allow,
+
            threshold,
+
            extensions: json::Map::new(),
+
        }
+
    }
+

+
    /// Get the set of DIDs this `Rule` was created with.
+
    pub fn allowed(&self) -> &D {
+
        &self.allow
+
    }
+

+
    /// Get the set of threshold this `Rule` was created with.
+
    pub fn threshold(&self) -> &T {
+
        &self.threshold
+
    }
+

+
    /// Get the extensions that may have been added to this `Rule`.
+
    pub fn extensions(&self) -> &json::Map<String, json::Value> {
+
        &self.extensions
+
    }
+

+
    /// If the [`Rule::extensions`] is not set, the provided `extensions` will
+
    /// be used.
+
    ///
+
    /// Otherwise, it expects that the JSON value is a `Map` and the
+
    /// `extensions` are merged. If the existing value is any other kind of JSON
+
    /// value, this is a no-op.
+
    pub fn add_extensions(&mut self, extensions: impl Into<json::Map<String, json::Value>>) {
+
        self.extensions.extend(extensions.into());
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum PatternError {
+
    #[error("cannot create rule for '{pattern}' since references under '{prefix}' are protected")]
+
    ProtectedRef {
+
        prefix: RefString,
+
        pattern: QualifiedPattern<'static>,
+
    },
+
}
+

+
#[derive(Debug, Error)]
+
pub enum ValidationError {
+
    #[error(transparent)]
+
    Threshold(#[from] doc::ThresholdError),
+
    #[error(transparent)]
+
    Delegates(#[from] doc::DelegatesError),
+
    #[error("cannot create rule for reserved `rad` references '{pattern}'")]
+
    RadRef { pattern: QualifiedPattern<'static> },
+
}
+

+
#[derive(Debug, Error)]
+
pub enum CanonicalError {
+
    #[error(transparent)]
+
    Git(#[from] git::raw::Error),
+
    #[error(transparent)]
+
    References(#[from] git::ext::Error),
+
}
+

+
#[cfg(test)]
+
#[allow(clippy::unwrap_used)]
+
mod tests {
+
    use std::collections::BTreeMap;
+

+
    use nonempty::nonempty;
+

+
    use crate::crypto::{test::signer::MockSigner, Signer};
+
    use crate::git;
+
    use crate::git::refspec::qualified_pattern;
+
    use crate::git::RefString;
+
    use crate::identity::doc::Doc;
+
    use crate::identity::Visibility;
+
    use crate::rad;
+
    use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH};
+
    use crate::storage::{git::transport, ReadStorage};
+
    use crate::test::{arbitrary, fixtures};
+
    use crate::Storage;
+

+
    use super::*;
+

+
    fn roundtrip(rule: &Rule<Allowed, usize>) {
+
        let json = serde_json::to_string(rule).unwrap();
+
        assert_eq!(
+
            *rule,
+
            serde_json::from_str(&json).unwrap(),
+
            "failed to roundtrip: {json}"
+
        )
+
    }
+

+
    fn did(s: &str) -> Did {
+
        s.parse().unwrap()
+
    }
+

+
    fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
+
        Pattern::try_from(qp).unwrap()
+
    }
+

+
    fn resolve_from_doc(delegate: Allowed, doc: &Doc) -> Result<doc::Delegates, ValidationError> {
+
        match delegate {
+
            Allowed::Delegates => Ok(doc.delegates().clone()),
+
            Allowed::Set(delegates) => {
+
                doc::Delegates::new(delegates).map_err(ValidationError::from)
+
            }
+
        }
+
    }
+

+
    fn tag(name: RefString, head: git2::Oid, repo: &git2::Repository) -> git::Oid {
+
        let commit = fixtures::commit(name.as_str(), &[head], repo);
+
        let target = repo.find_object(*commit, None).unwrap();
+
        let tagger = repo.signature().unwrap();
+
        repo.tag(name.as_str(), &target, &tagger, name.as_str(), false)
+
            .unwrap()
+
            .into()
+
    }
+

+
    #[test]
+
    fn test_roundtrip() {
+
        let rule1 = Rule::new(Allowed::Delegates, 1);
+
        let rule2 = Rule::new(Allowed::Delegates, 1);
+
        let rule3 = Rule::new(Allowed::Delegates, 1);
+
        let mut rule4 = Rule::new(
+
            Allowed::Set(nonempty![
+
                did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
+
                did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
+
            ]),
+
            2,
+
        );
+
        rule4.add_extensions(
+
            serde_json::json!({
+
                "foo": "bar",
+
                "quux": 5,
+
            })
+
            .as_object()
+
            .cloned()
+
            .unwrap(),
+
        );
+
        roundtrip(&rule1);
+
        roundtrip(&rule2);
+
        roundtrip(&rule3);
+
        roundtrip(&rule4);
+
    }
+

+
    #[test]
+
    fn test_deserialization() {
+
        let examples = r#"
+
{
+
  "refs/heads/main": {
+
    "threshold": 2,
+
    "allow": [
+
      "did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
+
      "did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
+
    ]
+
  },
+
  "refs/tags/releases/*": {
+
    "threshold": 2,
+
    "allow": [
+
      "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56",
+
      "did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP",
+
      "did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax"
+
    ]
+
  },
+
  "refs/heads/development": {
+
    "threshold": 1,
+
    "allow": [
+
      "did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
+
    ]
+
  },
+
  "refs/heads/release/*": {
+
    "threshold": 1,
+
    "allow": "delegates"
+
  }
+
}
+
 "#;
+
        let expected = [
+
            (
+
                pattern(qualified_pattern!("refs/heads/main")),
+
                Rule::new(
+
                    Allowed::Set(nonempty![
+
                        did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
+
                        did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
+
                    ]),
+
                    2,
+
                ),
+
            ),
+
            (
+
                pattern(qualified_pattern!("refs/tags/releases/*")),
+
                Rule::new(
+
                    Allowed::Set(nonempty![
+
                        did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
+
                        did("did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP"),
+
                        did("did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax")
+
                    ]),
+
                    2,
+
                ),
+
            ),
+
            (
+
                pattern(qualified_pattern!("refs/heads/development")),
+
                Rule::new(
+
                    Allowed::Set(nonempty![did(
+
                        "did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
+
                    )]),
+
                    1,
+
                ),
+
            ),
+
            (
+
                pattern(qualified_pattern!("refs/heads/release/*")),
+
                Rule::new(Allowed::Delegates, 1),
+
            ),
+
        ]
+
        .into_iter()
+
        .collect::<RawRules>();
+
        let rules = serde_json::from_str::<BTreeMap<Pattern, RawRule>>(examples)
+
            .unwrap()
+
            .into();
+
        eprintln!(
+
            "RULES: {}",
+
            serde_json::to_string_pretty(&expected).unwrap()
+
        );
+
        assert_eq!(expected, rules)
+
    }
+

+
    #[test]
+
    fn test_order() {
+
        assert!(
+
            pattern(qualified_pattern!("a/b/c/d/*")) < pattern(qualified_pattern!("*/x")),
+
            "example 1"
+
        );
+
        assert!(
+
            pattern(qualified_pattern!("a")) < pattern(qualified_pattern!("*")),
+
            "example 2.a"
+
        );
+
        assert!(
+
            pattern(qualified_pattern!("abc")) < pattern(qualified_pattern!("a*")),
+
            "example 2.a"
+
        );
+
        assert!(
+
            pattern(qualified_pattern!("a/b/*")) < pattern(qualified_pattern!("a/*/c")),
+
            "example 2.a"
+
        );
+
        assert!(
+
            pattern(qualified_pattern!("aa*")) < pattern(qualified_pattern!("a*")),
+
            "example 2.b.A"
+
        );
+
        assert!(
+
            pattern(qualified_pattern!("a*b")) < pattern(qualified_pattern!("a*")),
+
            "example 2.b.B"
+
        );
+

+
        let pattern01 = pattern(qualified_pattern!("refs/tags/*"));
+
        let pattern02 = pattern(qualified_pattern!("refs/tags/v1"));
+
        let pattern04 = pattern(qualified_pattern!("refs/tags/v1.0.0"));
+
        let pattern05 = pattern(qualified_pattern!("refs/tags/release/v1.0.0"));
+
        let pattern03 = pattern(qualified_pattern!("refs/heads/main"));
+
        let pattern06 = pattern(qualified_pattern!("refs/tags/*/v1.0.0"));
+

+
        let pattern07 = pattern(qualified_pattern!("refs/tags/x*"));
+
        let pattern08 = pattern(qualified_pattern!("refs/tags/xx*"));
+

+
        let pattern09 = pattern(qualified_pattern!("refs/foos/*"));
+

+
        let pattern10 = pattern(qualified_pattern!("a"));
+
        let pattern11 = pattern(qualified_pattern!("b"));
+

+
        let pattern12 = pattern(qualified_pattern!("a/*"));
+
        let pattern13 = pattern(qualified_pattern!("b/*"));
+

+
        let pattern14 = pattern(qualified_pattern!("a/*/ab"));
+
        let pattern15 = pattern(qualified_pattern!("a/*/a"));
+

+
        let pattern16 = pattern(qualified_pattern!("a/*/b"));
+
        let pattern17 = pattern(qualified_pattern!("a/*/a"));
+

+
        // Test priority for path specificity
+
        assert!(
+
            pattern06 < pattern02,
+
            "match for 06 is always more specific since it has more components"
+
        );
+
        assert!(pattern02 < pattern01, "match for 02 is also match for 01");
+
        assert!(pattern08 < pattern07, "match for 08 is also match for 07");
+
        // Test equality
+
        assert!(pattern02 == pattern02);
+
        // Test lexicographical fallback when paths are equally specific
+
        assert!(pattern02 < pattern04);
+
        assert!(pattern03 < pattern01);
+
        assert!(pattern09 < pattern01);
+
        assert!(pattern10 < pattern11);
+
        assert!(pattern12 < pattern13);
+
        assert!(pattern15 < pattern14);
+
        assert!(
+
            pattern17 < pattern16,
+
            "matches have same length, but lexicographically, 'a' < 'b'"
+
        );
+

+
        // Test example from docs
+
        let pattern18 = pattern(qualified_pattern!("refs/tags/release/candidates/*"));
+
        let pattern19 = pattern(qualified_pattern!("refs/tags/release/*"));
+
        let pattern20 = pattern(qualified_pattern!("refs/tags/*"));
+

+
        assert!(pattern18 < pattern19);
+
        assert!(pattern19 < pattern20);
+

+
        let pattern21 = pattern(qualified_pattern!("refs/heads/dev"));
+

+
        assert!(pattern21 < pattern03);
+

+
        let mut patterns = [
+
            pattern01.clone(),
+
            pattern02.clone(),
+
            pattern03.clone(),
+
            pattern04.clone(),
+
            pattern05.clone(),
+
            pattern06.clone(),
+
        ];
+
        patterns.sort();
+

+
        assert_eq!(
+
            patterns,
+
            [pattern05, pattern06, pattern03, pattern02, pattern04, pattern01]
+
        );
+
    }
+

+
    #[test]
+
    fn test_deserialize_extensions() {
+
        let example = r#"
+
{
+
  "threshold": 2,
+
  "allow": [
+
    "did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
+
    "did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
+
  ],
+
  "foo": "bar",
+
  "quux": 5
+
}
+
"#;
+
        let rule = serde_json::from_str::<Rule<Allowed, usize>>(example).unwrap();
+
        assert!(!rule.extensions().is_empty());
+
        let extensions = rule.extensions();
+
        assert_eq!(
+
            extensions.get("foo"),
+
            Some(serde_json::Value::String("bar".to_string())).as_ref()
+
        );
+
        assert_eq!(
+
            extensions.get("quux"),
+
            Some(serde_json::Value::Number(5.into())).as_ref()
+
        );
+
    }
+

+
    #[test]
+
    fn test_rule_validate_success() {
+
        let doc = arbitrary::gen::<Doc>(1);
+
        let delegates = Allowed::Set(doc.delegates().as_ref().clone());
+
        let threshold = doc.majority();
+

+
        let rule = Rule::new(delegates, threshold);
+
        let result = rule.validate(&mut |delegate| resolve_from_doc(delegate, &doc));
+
        assert!(result.is_ok(), "failed to validate doc: {result:?}");
+

+
        let rule = Rule::new(Allowed::Delegates, 1);
+
        let result = rule.validate(&mut |delegate| resolve_from_doc(delegate, &doc));
+
        assert!(result.is_ok(), "failed to validate doc: {result:?}");
+
    }
+

+
    #[test]
+
    fn test_rule_validate_failures() {
+
        let doc = arbitrary::gen::<Doc>(1);
+
        let pattern = pattern(qualified_pattern!("refs/heads/main"));
+

+
        assert!(matches!(
+
            Rule::new(Allowed::Delegates, 256)
+
                .validate(&mut |delegate| resolve_from_doc(delegate, &doc)),
+
            Err(ValidationError::Threshold(_))
+
        ));
+

+
        let threshold = doc.delegates().len().saturating_add(1);
+
        assert!(matches!(
+
            Rule::new(Allowed::Delegates, threshold)
+
                .validate(&mut |delegate| resolve_from_doc(delegate, &doc)),
+
            Err(ValidationError::Threshold(_))
+
        ));
+

+
        let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
+
        assert!(matches!(
+
            Rule::new(delegates.into(), 1)
+
                .validate(&mut |delegate| resolve_from_doc(delegate, &doc)),
+
            Err(ValidationError::Delegates(_))
+
        ));
+

+
        let delegates = nonempty![
+
            did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
+
            did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
+
        ];
+
        let expected = Rule {
+
            allow: ResolvedDelegates::Set(
+
                doc::Delegates::new(nonempty![did(
+
                    "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
+
                )])
+
                .unwrap(),
+
            ),
+
            threshold: doc::Threshold::MIN,
+
            extensions: json::Map::new(),
+
        };
+
        assert_eq!(
+
            Rule::new(delegates.into(), 1)
+
                .validate(&mut |delegate| resolve_from_doc(delegate, &doc))
+
                .unwrap(),
+
            expected,
+
        );
+

+
        // Duplicate rules are overwritten
+
        let rules = vec![
+
            (pattern.clone(), Rule::new(Allowed::Delegates, 1)),
+
            (
+
                pattern.clone(),
+
                Rule::new(doc.delegates().as_ref().clone().into(), 1),
+
            ),
+
        ];
+
        let expected = [(
+
            pattern,
+
            Rule::new(
+
                ResolvedDelegates::Set(doc.delegates().clone()),
+
                doc::Threshold::MIN,
+
            ),
+
        )]
+
        .into_iter()
+
        .collect::<Rules>();
+
        assert_eq!(
+
            Rules::from_raw(rules, &mut |delegate| resolve_from_doc(delegate, &doc)).unwrap(),
+
            expected
+
        );
+
    }
+

+
    #[test]
+
    fn test_canonical() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
+

+
        transport::local::register(storage.clone());
+

+
        let delegate = MockSigner::from_seed([0xff; 32]);
+
        let contributor = MockSigner::from_seed([0xfe; 32]);
+
        let (repo, head) = fixtures::repository(tempdir.path().join("working"));
+
        let (rid, doc, _) = rad::init(
+
            &repo,
+
            "heartwood".try_into().unwrap(),
+
            "Radicle Heartwood Protocol & Stack",
+
            git::refname!("master"),
+
            Visibility::default(),
+
            &delegate,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        let mut doc = doc.edit();
+
        // Ensure there is a second delegate for testing overlapping rules
+
        doc.delegate(contributor.public_key().into());
+

+
        // Create tags and keep track of their OIDs
+
        //
+
        // follows the `refs/tags/release/candidates/*` rule
+
        let failing_tag = git::refname!("release/candidates/v1.0");
+
        let tags = [
+
            // follows the `refs/tags/*` rule
+
            git::refname!("v1.0"),
+
            // follows the `refs/tags/release/*` rule
+
            git::refname!("release/v1.0"),
+
            failing_tag.clone(),
+
            // follows the `refs/tags/*` rule
+
            git::refname!("qa/v1.0"),
+
        ]
+
        .into_iter()
+
        .map(|name| {
+
            (
+
                git::lit::refs_tags(name.clone()).into(),
+
                tag(name, head, &repo),
+
            )
+
        })
+
        .collect::<BTreeMap<Qualified, _>>();
+

+
        git::push(
+
            &repo,
+
            &rad::REMOTE_NAME,
+
            [
+
                (
+
                    &git::qualified!("refs/tags/v1.0"),
+
                    &git::qualified!("refs/tags/v1.0"),
+
                ),
+
                (
+
                    &git::qualified!("refs/tags/release/v1.0"),
+
                    &git::qualified!("refs/tags/release/v1.0"),
+
                ),
+
                (
+
                    &git::qualified!("refs/tags/release/candidates/v1.0"),
+
                    &git::qualified!("refs/tags/release/candidates/v1.0"),
+
                ),
+
                (
+
                    &git::qualified!("refs/tags/qa/v1.0"),
+
                    &git::qualified!("refs/tags/qa/v1.0"),
+
                ),
+
            ],
+
        )
+
        .unwrap();
+

+
        let rules = Rules::from_raw(
+
            [
+
                (
+
                    pattern(qualified_pattern!("refs/tags/*")),
+
                    Rule::new(Allowed::Delegates, 1),
+
                ),
+
                (
+
                    pattern(qualified_pattern!("refs/tags/release/*")),
+
                    Rule::new(Allowed::Delegates, 1),
+
                ),
+
                // Ensure that none of the other rules apply by ensuring we need
+
                // both delegates to get the quorum of the
+
                // `refs/tags/release/candidates/v1.0` reference
+
                (
+
                    pattern(qualified_pattern!("refs/tags/release/candidates/*")),
+
                    Rule::new(Allowed::Delegates, 2),
+
                ),
+
            ],
+
            &mut |delegate| resolve_from_doc(delegate, &doc.clone().verified().unwrap()),
+
        )
+
        .unwrap();
+

+
        // All tags should succeed at getting their canonical tip other than the
+
        // candidates tag.
+
        let stored = storage.repository(rid).unwrap();
+
        let failing = git::Qualified::from(git::lit::refs_tags(failing_tag));
+
        for (refname, oid) in tags.into_iter() {
+
            let canonical = rules
+
                .canonical(refname.clone(), &stored)
+
                .unwrap()
+
                .unwrap_or_else(|| {
+
                    panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
+
                });
+
            if refname == failing {
+
                assert!(canonical.quorum(&repo).is_err());
+
            } else {
+
                assert_eq!(
+
                    canonical
+
                        .quorum(&repo)
+
                        .unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
+
                    (refname, oid),
+
                )
+
            }
+
        }
+
    }
+

+
    #[test]
+
    fn test_special_branches() {
+
        assert!(Pattern::try_from((*IDENTITY_BRANCH).clone()).is_err());
+
        assert!(Pattern::try_from((*SIGREFS_BRANCH).clone()).is_err());
+
        assert!(Pattern::try_from((*IDENTITY_ROOT).clone()).is_err());
+
    }
+
}
modified radicle/src/identity.rs
@@ -5,7 +5,9 @@ pub mod project;

pub use crypto::PublicKey;
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, VersionedRawDoc, Visibility,
+
};
pub use project::Project;

pub use crate::cob::identity::{Action, Error, Identity, IdentityMut, TYPENAME};
modified radicle/src/identity/doc.rs
@@ -1,8 +1,13 @@
mod id;
+
mod v1;
+
mod version;
+

+
use version::VersionTwo;
+
use version::KNOWN_VERSIONS;

use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
-
use std::num::{NonZeroU32, NonZeroUsize};
+
use std::num::NonZeroUsize;
use std::ops::{Deref, Not};
use std::path::Path;
use std::str::FromStr;
@@ -19,6 +24,9 @@ use crate::cob::identity;
use crate::crypto;
use crate::crypto::Signature;
use crate::git;
+
use crate::git::canonical::rules;
+
use crate::git::canonical::rules::{RawRules, Rule};
+
use crate::git::canonical::Rules;
use crate::identity::{project::Project, Did};
use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};
@@ -32,12 +40,13 @@ pub static PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
pub const MAX_STRING_LENGTH: usize = 255;
/// Maximum number of a delegates in the identity document.
pub const MAX_DELEGATES: usize = 255;
-
/// The current, most recent version of the identity document.
-
// SAFETY: identity version should never be 0, so we can use `unsafe` here
-
pub const IDENTITY_VERSION: Version = Version(unsafe { NonZeroU32::new_unchecked(1) });

#[derive(Error, Debug)]
pub enum DocError {
+
    #[error(transparent)]
+
    Pattern(#[from] rules::PatternError),
+
    #[error(transparent)]
+
    CanonicalRefs(#[from] rules::ValidationError),
    #[error("json: {0}")]
    Json(#[from] serde_json::Error),
    #[error(transparent)]
@@ -50,6 +59,8 @@ pub enum DocError {
    Git(#[from] git2::Error),
    #[error("missing identity document")]
    Missing,
+
    #[error("migration: {0}")]
+
    MigrationError(#[from] MigrationError),
}

#[derive(Debug, Error)]
@@ -72,120 +83,6 @@ impl DocError {
    }
}

-
/// The version number of the identity document.
-
///
-
/// It is used to ensure compatibility when parsing identity documents.
-
///
-
/// If an invalid version is found – either the `0` version, or an unrecognized
-
/// future version – the parsing of a version will fail.
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-
pub struct Version(NonZeroU32);
-

-
impl Version {
-
    /// Construct a [`Version`].
-
    ///
-
    /// # Errors
-
    ///
-
    ///   - `n` is 0
-
    ///   - `n` is greater than the latest version, specified by
-
    ///     [`IDENTITY_VERSION`].
-
    pub fn new(n: u32) -> Result<Version, VersionError> {
-
        match NonZeroU32::new(n) {
-
            None => Err(VersionError::ZeroVersion),
-
            Some(n) if n > IDENTITY_VERSION.into() => Err(VersionError::UnkownVersion(n)),
-
            Some(n) => Ok(Version(n)),
-
        }
-
    }
-

-
    /// Return the underlying [`NonZeroU32`] number of the `Version`.
-
    pub fn number(&self) -> NonZeroU32 {
-
        self.0
-
    }
-

-
    /// Check if the provided version is part of the set of accepted versions.
-
    pub fn is_valid_version(v: &u32) -> bool {
-
        0 < *v && *v <= IDENTITY_VERSION.into()
-
    }
-

-
    /// Helper for skipping the serialization of the version if `version <= 1`.
-
    ///
-
    /// Note that we shouldn't allow `version: 0`, but there is no harm in
-
    /// skipping it anyway.
-
    fn skip_serializing(&self) -> bool {
-
        u32::from(*self) <= 1
-
    }
-
}
-

-
impl From<Version> for NonZeroU32 {
-
    fn from(Version(n): Version) -> Self {
-
        n
-
    }
-
}
-

-
impl From<Version> for u32 {
-
    fn from(Version(n): Version) -> Self {
-
        n.into()
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum VersionError {
-
    #[error("the version 0 is not supported")]
-
    ZeroVersion,
-
    #[error("unknown identity document version {0}, only version {IDENTITY_VERSION} is supported")]
-
    UnkownVersion(NonZeroU32),
-
}
-

-
impl VersionError {
-
    /// Provide a verbose error.
-
    ///
-
    /// This will give a user more information on how to upgrade to a newer
-
    /// version of an identity document, if there is one.
-
    pub fn verbose(&self) -> String {
-
        const UNKOWN_VERSION_ERROR: &str = r#"
-
Perhaps a new version of the identity document is released which is not supported by the current client.
-
See https://radicle.xyz for the latest versions of Radicle.
-
The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;
-

-
        match self {
-
            err @ Self::ZeroVersion => err.to_string(),
-
            err @ Self::UnkownVersion(_) => format!("{err}{UNKOWN_VERSION_ERROR}"),
-
        }
-
    }
-
}
-

-
impl TryFrom<u32> for Version {
-
    type Error = VersionError;
-

-
    fn try_from(n: u32) -> Result<Self, Self::Error> {
-
        Version::new(n)
-
    }
-
}
-

-
impl fmt::Display for Version {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.0)
-
    }
-
}
-

-
impl<'de> Deserialize<'de> for Version {
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: serde::Deserializer<'de>,
-
    {
-
        u32::deserialize(deserializer)
-
            .and_then(|v| Version::new(v).map_err(|e| de::Error::custom(e.to_string())))
-
    }
-
}
-

-
/// Used for [`Deserialize`] of a [`Version`] in [`RawDoc`], so that
-
/// deserializing a missing version results in `Version(1)`.
-
fn missing_version() -> Version {
-
    // N.B. the default version is `1` which is non-zero so unsafe is fine here
-
    unsafe { Version(NonZeroU32::new_unchecked(1)) }
-
}
-

/// Identifies an identity document payload type.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
@@ -224,6 +121,15 @@ pub enum PayloadError {
    NotFound(PayloadId),
}

+
impl PayloadError {
+
    pub fn is_not_found(&self) -> bool {
+
        match self {
+
            PayloadError::Json(_) => false,
+
            PayloadError::NotFound(_) => true,
+
        }
+
    }
+
}
+

/// A `Payload` is a free-form JSON value that can be associated with an
/// identity's [`Doc`].
/// The payload is identified in the [`Doc`] by its corresponding [`PayloadId`].
@@ -346,22 +252,115 @@ impl Visibility {
/// serializing an unverified document, while also making sure that any document
/// that is deserialized is verified.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
-
#[serde(rename_all = "camelCase")]
+
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct RawDoc {
-
    /// Version of the identity document.
-
    #[serde(default = "missing_version")]
-
    version: Version,
+
    version: VersionTwo,
    /// The payload section.
    pub payload: BTreeMap<PayloadId, Payload>,
    /// The delegates section.
    pub delegates: Vec<Did>,
-
    /// The signature threshold.
-
    pub threshold: usize,
+
    /// The canonical reference rules.
+
    #[serde(default)]
+
    pub canonical_refs: RawCanonicalRefs,
    /// Repository visibility.
    #[serde(default)]
    pub visibility: Visibility,
}

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub enum VersionedRawDoc {
+
    V2(RawDoc),
+
    V1(v1::RawDoc),
+
}
+

+
impl<'de> Deserialize<'de> for VersionedRawDoc {
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: de::Deserializer<'de>,
+
    {
+
        use serde::de::Error;
+

+
        // We could use the `serde-value` crate and its type
+
        // [`serde_value::Value`] to achieve the same, without
+
        // depending on a particular format, such as JSON.
+
        // However, we do depend on `serde_json` already,
+
        // and it is the actual format we use, so this is fine.
+
        use serde_json::{from_value, Value};
+

+
        fn default_version() -> u64 {
+
            (*KNOWN_VERSIONS.start()).into()
+
        }
+

+
        /// The derived implementation of [`Deserialize`]
+
        /// for this helper struct allows us to peek for a version
+
        /// field in the contents we are deserializing via [`Helper::version`],
+
        /// and at the same time collects all other fields in [`Helper::value`].
+
        #[derive(Debug, Deserialize)]
+
        struct Helper {
+
            /// This is of type [`u64`] and not [`std::num::NonZeroU64`] or
+
            /// [`version::Version`] so that we can handle zero with an explicit
+
            /// error below.
+
            #[serde(default = "default_version")]
+
            version: u64,
+
            #[serde(flatten)]
+
            value: Value,
+
        }
+

+
        let Helper { version, mut value } = Helper::deserialize(deserializer)?;
+

+
        if let Value::Object(ref mut map) = value {
+
            const VERSION: &str = "version";
+
            debug_assert!(!map.contains_key(&VERSION.to_string()));
+
            // Since [`Helper`] has its own field [`Helper::version`],
+
            // we have to copy it to [`Helper::value`].
+
            map.insert(VERSION.to_string(), version.into());
+
        }
+

+
        match version {
+
            1 => Ok(Self::V1(from_value(value).map_err(Error::custom)?)),
+
            2 => Ok(Self::V2(from_value(value).map_err(Error::custom)?)),
+
            v => Err(Error::custom(format!(
+
                "invalid value for field version: {v}, expected a positive non-zero integer value in interval [{},{}]",
+
                KNOWN_VERSIONS.start(),
+
                KNOWN_VERSIONS.end(),
+
            ))),
+
        }
+
    }
+
}
+

+
impl TryFrom<VersionedRawDoc> for Doc {
+
    type Error = DocError;
+

+
    fn try_from(value: VersionedRawDoc) -> Result<Doc, Self::Error> {
+
        match value {
+
            VersionedRawDoc::V1(raw) => {
+
                let doc = raw.verified()?;
+
                Ok(Doc::migrate_from(doc)?)
+
            }
+
            VersionedRawDoc::V2(raw) => raw.verified(),
+
        }
+
    }
+
}
+

+
/// Configuration for canonical references and their rules.
+
///
+
/// `RawCanonicalRefs` is used in [`RawDoc`], and is verified into
+
/// [`CanonicalRefs`].
+
///
+
/// Note that it must implement `Default` for `Deserialize` for compatibility –
+
/// any fields being added must be able to implement default too.
+
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct RawCanonicalRefs {
+
    pub rules: RawRules,
+
}
+

+
impl RawCanonicalRefs {
+
    pub fn new(rules: RawRules) -> Self {
+
        Self { rules }
+
    }
+
}
+

impl TryFrom<RawDoc> for Doc {
    type Error = DocError;

@@ -377,26 +376,21 @@ impl RawDoc {
    pub fn new(
        project: Project,
        delegates: Vec<Did>,
-
        threshold: usize,
+
        _threshold: usize,
+
        rules: RawRules,
        visibility: Visibility,
    ) -> Self {
        let project =
            serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
-

        Self {
-
            version: IDENTITY_VERSION,
+
            version: VersionTwo,
            payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
            delegates,
-
            threshold,
+
            canonical_refs: RawCanonicalRefs::new(rules),
            visibility,
        }
    }

-
    /// Get the version of the document.
-
    pub fn version(&self) -> &Version {
-
        &self.version
-
    }
-

    /// Get the project payload, if it exists and is valid, out of this document.
    pub fn project(&self) -> Result<Project, PayloadError> {
        let value = self
@@ -442,22 +436,29 @@ impl RawDoc {
    ///  - [`RawDoc::delegates`]: any duplicates are removed, and for the
    ///    remaining set ensure that it is non-empty and does not exceed a
    ///    length of [`MAX_DELEGATES`].
-
    ///  - [`RawDoc::threshold`]: ensure that it is in the range `[1, delegates.len()]`.
    pub fn verified(self) -> Result<Doc, DocError> {
        let RawDoc {
            version,
            payload,
            delegates,
-
            threshold,
+
            canonical_refs,
            visibility,
+
            ..
        } = self;
+

        let delegates = Delegates::new(delegates)?;
-
        let threshold = Threshold::new(threshold, &delegates)?;
+
        let rules = Rules::from_raw(canonical_refs.rules, &mut |ds| match ds {
+
            rules::Allowed::Delegates => Ok(delegates.clone()),
+
            rules::Allowed::Set(delegates) => {
+
                Delegates::new(delegates).map_err(rules::ValidationError::from)
+
            }
+
        })?;
+

        Ok(Doc {
            version,
            payload,
            delegates,
-
            threshold,
+
            canonical_refs: CanonicalRefs { rules },
            visibility,
        })
    }
@@ -476,6 +477,12 @@ impl AsRef<NonEmpty<Did>> for Delegates {
    }
}

+
impl From<Did> for Delegates {
+
    fn from(did: Did) -> Self {
+
        Self(NonEmpty::new(did))
+
    }
+
}
+

impl TryFrom<Vec<Did>> for Delegates {
    type Error = DelegatesError;

@@ -529,6 +536,11 @@ impl Delegates {
        self.0.contains(did)
    }

+
    /// Check if the `did` is the only delegate of the repository.
+
    pub fn is_only(&self, did: &Did) -> bool {
+
        self.0.tail.is_empty() && &self.0.head == did
+
    }
+

    /// Get the number of delegates in the set.
    pub fn len(&self) -> usize {
        self.0.len()
@@ -606,6 +618,17 @@ impl Threshold {
    }
}

+
#[derive(Debug, Error)]
+
pub enum MigrationError {
+
    #[error("failed to migrate from v1: {0}")]
+
    V1(#[from] v1::MigrationError),
+
    #[error("failed to parse previous version {version}: {err}")]
+
    Unknown {
+
        version: u32,
+
        err: serde_json::Error,
+
    },
+
}
+

/// `Doc` is a valid identity document.
///
/// To ensure that only valid documents are used, this type is restricted to be
@@ -616,45 +639,84 @@ impl Threshold {
///   1. [`Doc::initial`]: a safe way to construct the initial document for an identity.
///   2. [`RawDoc::verified`]: validates a [`RawDoc`]'s fields and converts it
///      into a `Doc`
-
///   3. [`Deserialize`]: will deserialize a `Doc` by first deserializing a
-
///      [`RawDoc`] and use [`RawDoc::verified`] to construct the `Doc`.
-
///   4. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
+
///   3. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
///      its contents.
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
-
#[serde(try_from = "RawDoc")]
pub struct Doc {
-
    #[serde(skip_serializing_if = "Version::skip_serializing")]
-
    version: Version,
+
    version: VersionTwo,
    payload: BTreeMap<PayloadId, Payload>,
    delegates: Delegates,
-
    threshold: Threshold,
+
    #[serde(skip_serializing_if = "CanonicalRefs::is_empty")]
+
    canonical_refs: CanonicalRefs,
    #[serde(default, skip_serializing_if = "Visibility::is_public")]
    visibility: Visibility,
}

+
/// Configuration for canonical references and their [`Rules`].
+
///
+
/// `CanonicalRefs` is used in [`Doc`], and the [`Rules`] are accessed via
+
/// [`Doc::rules`].
+
// Note that it must implement [`CanonicalRefs::is_empty`] for skipping
+
// serialization, for compatibility – any fields being added must be accounted
+
// for in this method.
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct CanonicalRefs {
+
    rules: Rules,
+
}
+

+
impl CanonicalRefs {
+
    pub fn new(rules: Rules) -> Self {
+
        CanonicalRefs { rules }
+
    }
+

+
    /// Check if the data structure empty. Used in the [`Serialize`]
+
    /// implementation.
+
    fn is_empty(&self) -> bool {
+
        // N.b. account for any new fields when adding them.
+
        self.rules.is_empty()
+
    }
+
}
+

+
impl From<CanonicalRefs> for RawCanonicalRefs {
+
    fn from(crefs: CanonicalRefs) -> Self {
+
        Self {
+
            rules: crefs.rules.into(),
+
        }
+
    }
+
}
+

impl Doc {
    /// Construct the initial [`Doc`] for an identity.
    ///
    /// It will begin with the provided `project` in the [`Doc::payload`], the
    /// `delegate` as the sole delegate, a threshold of 1, and the given
    /// `visibility`.
-
    pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
+
    pub fn initial(
+
        project: Project,
+
        delegate: Did,
+
        visibility: Visibility,
+
    ) -> Result<Self, rules::PatternError> {
+
        let rules = [Rule::default_branch(delegate, project.default_branch())?]
+
            .into_iter()
+
            .collect();
        let project =
            serde_json::to_value(project).expect("Doc::initial: payload must be serializable");

-
        Self {
-
            version: IDENTITY_VERSION,
+
        Ok(Self {
+
            version: VersionTwo,
            payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
            delegates: Delegates(NonEmpty::new(delegate)),
-
            threshold: Threshold(NonZeroUsize::MIN),
+
            canonical_refs: CanonicalRefs::new(rules),
            visibility,
-
        }
+
        })
    }

    /// Construct a [`Doc`] contained in the provided Git blob.
    pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
-
        RawDoc::from_json(blob.content())?.verified()
+
        let raw = serde_json::from_slice::<VersionedRawDoc>(blob.content())?;
+
        Self::try_from(raw)
    }

    /// Convert the [`Doc`] into a [`RawDoc`] for changing the field values and
@@ -664,14 +726,14 @@ impl Doc {
            version,
            payload,
            delegates,
-
            threshold,
+
            canonical_refs,
            visibility,
        } = self;
        RawDoc {
            version,
            payload,
            delegates: delegates.into(),
-
            threshold: threshold.into(),
+
            canonical_refs: canonical_refs.into(),
            visibility,
        }
    }
@@ -687,11 +749,6 @@ impl Doc {
        raw.verified()
    }

-
    /// Get the version of the document.
-
    pub fn version(&self) -> &Version {
-
        &self.version
-
    }
-

    /// Return the associated payloads for this [`Doc`].
    pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
        &self.payload
@@ -723,16 +780,6 @@ impl Doc {
        self.visibility.is_private()
    }

-
    /// Return the associated threshold of this document.
-
    pub fn threshold(&self) -> usize {
-
        self.threshold.into()
-
    }
-

-
    /// Return the associated threshold of this document in its non-zero format.
-
    pub fn threshold_nonzero(&self) -> &NonZeroUsize {
-
        &self.threshold.0
-
    }
-

    /// Return the associated delegates of this document.
    pub fn delegates(&self) -> &Delegates {
        &self.delegates
@@ -743,6 +790,28 @@ impl Doc {
        self.delegates.contains(did)
    }

+
    /// Return the canonical reference rules of this document.
+
    pub fn rules(&self) -> &Rules {
+
        &self.canonical_refs.rules
+
    }
+

+
    pub fn default_branch_threshold(&self) -> Result<Option<usize>, PayloadError> {
+
        let Some(refname) = match self.project() {
+
            Ok(project) => Ok(Some(git::refs::branch(project.default_branch()))),
+
            Err(e) if e.is_not_found() => Ok(None),
+
            Err(e) => Err(e),
+
        }?
+
        else {
+
            return Ok(None);
+
        };
+

+
        Ok(self
+
            .rules()
+
            .matches(&refname)
+
            .next()
+
            .map(|(_, rule)| (*rule.threshold()).into()))
+
    }
+

    /// Check whether this document and the associated repository is visible to
    /// the given peer.
    pub fn is_visible_to(&self, did: &Did) -> bool {
@@ -833,6 +902,16 @@ impl Doc {
        })
    }

+
    /// Load the identity document as raw JSON from the provided `commit`.
+
    pub fn load_json<R: ReadRepository>(
+
        commit: Oid,
+
        repo: &R,
+
    ) -> Result<serde_json::Value, DocError> {
+
        let blob = Self::blob_at(commit, repo)?;
+
        let value = serde_json::from_slice::<serde_json::Value>(blob.content())?;
+
        Ok(value)
+
    }
+

    /// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
    /// document.
    pub fn init<G: crypto::Signer>(
@@ -857,6 +936,10 @@ impl Doc {

        Ok(*cob.id)
    }
+

+
    pub fn migrate_from(doc: v1::Doc) -> Result<Self, MigrationError> {
+
        doc.migrate().map_err(MigrationError::from)
+
    }
}

#[cfg(test)]
@@ -882,7 +965,13 @@ mod test {
    fn test_duplicate_dids() {
        let delegate = MockSigner::from_seed([0xff; 32]);
        let did = Did::from(delegate.public_key());
-
        let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
+
        let mut doc = RawDoc::new(
+
            gen::<Project>(1),
+
            vec![did],
+
            1,
+
            RawRules::default(),
+
            Visibility::Public,
+
        );
        doc.delegate(did);
        let doc = doc.verified().unwrap();
        assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
@@ -899,44 +988,74 @@ mod test {
            gen::<Project>(1),
            delegates[0..MAX_DELEGATES].into(),
            1,
+
            RawRules::default(),
            Visibility::Public,
        );
        assert_matches!(doc.verified(), Ok(_));

        // A document that exceeds max delegates should fail
-
        let doc = RawDoc::new(gen::<Project>(1), delegates, 1, Visibility::Public);
+
        let doc = RawDoc::new(
+
            gen::<Project>(1),
+
            delegates,
+
            1,
+
            RawRules::default(),
+
            Visibility::Public,
+
        );
        assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
    }

    #[test]
-
    fn test_is_valid_version() {
-
        // 0 is not a valid version
-
        assert!(!Version::is_valid_version(&0));
-

-
        // Ensures that the latest version is always valid
-
        let current = IDENTITY_VERSION.number();
-
        assert!(Version::is_valid_version(&current.into()));
-

-
        // Ensures that the next version is not valid because we have not
-
        // defined it yet
-
        let next = current.checked_add(1).unwrap();
-
        assert!(!Version::is_valid_version(&next.into()));
-
    }
-

-
    #[test]
-
    fn test_future_version_error() {
-
        let v = Version(NonZeroU32::MAX).to_string();
-
        assert_eq!(
-
            serde_json::from_str::<Version>(&v)
-
                .expect_err("should fail to deserialize")
-
                .to_string(),
-
            VersionError::UnkownVersion(NonZeroU32::MAX).to_string(),
-
        )
+
    fn test_version_out_of_range() {
+
        let zero = json!({
+
            "version": 0,
+
            "payload": {
+
                "xyz.radicle.project": {
+
                    "defaultBranch": "main",
+
                    "description": "Example project",
+
                    "name": "example"
+
                }
+
            },
+
            "delegates": [
+
                "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
+
            ],
+
        });
+
        assert_matches!(serde_json::from_value::<VersionedRawDoc>(zero), Err(_));
+

+
        let next = json!({
+
            "version": Into::<u64>::into(*KNOWN_VERSIONS.end()).saturating_add(1),
+
            "payload": {
+
                "xyz.radicle.project": {
+
                    "defaultBranch": "main",
+
                    "description": "Example project",
+
                    "name": "example"
+
                }
+
            },
+
            "delegates": [
+
                "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
+
            ],
+
        });
+
        assert_matches!(serde_json::from_value::<VersionedRawDoc>(next), Err(_));
+

+
        let max = json!({
+
            "version": u64::MAX,
+
            "payload": {
+
                "xyz.radicle.project": {
+
                    "defaultBranch": "main",
+
                    "description": "Example project",
+
                    "name": "example"
+
                }
+
            },
+
            "delegates": [
+
                "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
+
            ],
+
        });
+
        assert_matches!(serde_json::from_value::<VersionedRawDoc>(max), Err(_));
    }

    #[test]
-
    fn test_parse_version() {
-
        // Original document before introducing the version field
+
    fn test_migrate_v1() {
+
        // Harcoded version 1 of the identity document. We expect that parsing
+
        // this into the latest document should pass.
        let v1 = json!(
            {
                "payload": {
@@ -955,9 +1074,6 @@ mod test {
            }
        );

-
        // Deserializing the `RawDoc` should not fail and should include the
-
        // `IDENTITY_VERSION`.
-
        let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
        let payload = [(
            PayloadId::project(),
            Payload {
@@ -981,31 +1097,51 @@ mod test {
                .parse::<Did>()
                .unwrap(),
        ];
-
        // And this is the expected outcome of the deserialization
+

+
        let doc = serde_json::from_str::<v1::RawDoc>(&v1.to_string()).unwrap();
+

        assert_eq!(
            doc,
-
            RawDoc {
-
                version: IDENTITY_VERSION,
+
            v1::RawDoc {
+
                version: version::VersionOne,
                payload: payload.clone(),
                delegates: delegates.clone(),
-
                threshold: 1,
                visibility: Visibility::Public,
+
                threshold: 1,
            }
        );

-
        // Deserializing into `Doc` should also succeed.
-
        let verified = serde_json::from_str::<Doc>(&v1.to_string()).unwrap();
-
        let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
+
        // Verifying into `Doc` should also succeed.
+
        let verified = doc.verified().unwrap();
+

+
        let delegates = NonEmpty::from_vec(delegates).unwrap();
+
        // Note that v1 -> v2 upgrade introduces adding a rule for the default
+
        // branch when it comes to project repositories.
+
        let rules = Rules::from_raw(
+
            [(
+
                rules::Pattern::try_from(git::refspec::qualified_pattern!("refs/heads/master"))
+
                    .unwrap(),
+
                Rule::new(rules::Allowed::Delegates, 1),
+
            )],
+
            &mut |ds| match ds {
+
                rules::Allowed::Delegates => Ok(Delegates(delegates.clone())),
+
                rules::Allowed::Set(_) => Ok(Delegates(delegates.clone())),
+
            },
+
        )
+
        .unwrap();
+

+
        // Test that we can migrate from the raw JSON value into a verified
+
        // latest version of the document
        assert_eq!(
-
            verified,
+
            Doc::migrate_from(verified).unwrap(),
            Doc {
-
                version: IDENTITY_VERSION,
-
                threshold: Threshold::new(1, &delegates).unwrap(),
-
                payload: payload.clone(),
-
                delegates,
+
                version: VersionTwo,
+
                payload,
+
                delegates: Delegates(delegates),
+
                canonical_refs: CanonicalRefs::new(rules),
                visibility: Visibility::Public,
            }
-
        );
+
        )
    }

    #[test]
@@ -1034,9 +1170,9 @@ mod test {
        );
        assert_eq!(
            (*id).to_string(),
-
            "d96f425412c9f8ad5d9a9a05c9831d0728e2338d"
+
            "b38d81ee99d880461a3b7b3502e5d1556e440ef3"
        );
-
        assert_eq!(id.urn(), String::from("rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji"));
+
        assert_eq!(id.urn(), String::from("rad:z3W5xAVWJ9Gc4LbN16mE3tjWX92t2"));
    }

    #[test]
added radicle/src/identity/doc/v1.rs
@@ -0,0 +1,266 @@
+
//! We keep track of the previous versions of the identity documents for testing
+
//! and migrating.
+
//!
+
//! This [`Doc`] is the Version 1 of the identity document.
+

+
use std::collections::BTreeMap;
+

+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::git;
+
use crate::git::canonical::rules;
+
use crate::git::canonical::rules::{RawRules, Rule, Rules, ValidationError};
+
use crate::prelude::{Did, Project};
+

+
use super::{
+
    version::VersionOne, CanonicalRefs, Delegates, DocError, Payload, PayloadError, PayloadId,
+
    Threshold, Visibility,
+
};
+

+
/// `RawDoc` is similar to the [`Doc`] type, however, it can be edited and may
+
/// not be valid.
+
///
+
/// It is expected that any changes to a [`Doc`] are made via [`RawDoc`], and
+
/// then verified by using [`RawDoc::verified`].
+
///
+
/// Note that `RawDoc` only implements [`Deserialize`]. This prevents us from
+
/// serializing an unverified document, while also making sure that any document
+
/// that is deserialized is verified.
+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+
#[serde(deny_unknown_fields, rename_all = "camelCase")]
+
pub struct RawDoc {
+
    #[serde(default)]
+
    pub version: VersionOne,
+
    /// The payload section.
+
    pub payload: BTreeMap<PayloadId, Payload>,
+
    /// The delegates section.
+
    pub delegates: Vec<Did>,
+
    /// The signature threshold.
+
    pub threshold: usize,
+
    /// Repository visibility.
+
    #[serde(default)]
+
    pub visibility: Visibility,
+
}
+

+
impl RawDoc {
+
    /// Verify the `RawDoc`'s values, converting it into a valid [`Doc`].
+
    ///
+
    /// The verifications are as follows:
+
    ///
+
    ///  - [`RawDoc::delegates`]: any duplicates are removed, and for the
+
    ///    remaining set ensure that it is non-empty and does not exceed a
+
    ///    length of [`MAX_DELEGATES`].
+
    ///  - [`RawDoc::threshold`]: ensure that it is in the range `[1, delegates.len()]`.
+
    pub fn verified(self) -> Result<Doc, DocError> {
+
        let RawDoc {
+
            payload,
+
            delegates,
+
            threshold,
+
            visibility,
+
            version,
+
        } = self;
+
        let delegates = Delegates::new(delegates)?;
+
        let threshold = Threshold::new(threshold, &delegates)?;
+
        Ok(Doc {
+
            payload,
+
            delegates,
+
            threshold,
+
            visibility,
+
            version,
+
        })
+
    }
+
}
+

+
/// `Doc` is a valid identity document.
+
///
+
/// To ensure that only valid documents are used, this type is restricted to be
+
/// read-only. For mutating the document use [`Doc::edit`].
+
///
+
/// A valid `Doc` can be constructed in four ways:
+
///
+
///   1. [`Doc::initial`]: a safe way to construct the initial document for an identity.
+
///   2. [`RawDoc::verified`]: validates a [`RawDoc`]'s fields and converts it
+
///      into a `Doc`
+
///   3. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
+
///      its contents.
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct Doc {
+
    #[serde(skip_serializing)]
+
    version: VersionOne,
+
    payload: BTreeMap<PayloadId, Payload>,
+
    delegates: Delegates,
+
    threshold: Threshold,
+
    #[serde(default, skip_serializing_if = "Visibility::is_public")]
+
    visibility: Visibility,
+
}
+

+
impl TryFrom<RawDoc> for Doc {
+
    type Error = DocError;
+

+
    fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
+
        doc.verified()
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum MigrationError {
+
    #[error(transparent)]
+
    Pattern(#[from] rules::PatternError),
+
    #[error(transparent)]
+
    Payload(#[from] PayloadError),
+
    #[error(transparent)]
+
    ValidationError(#[from] ValidationError),
+
}
+

+
impl Doc {
+
    /// Automatically migrate the `v1` `Doc` to the latest [`super::Doc`] version.
+
    ///
+
    /// This can be used to get the latest version in a verified state for
+
    /// proposing as identity document change.
+
    ///
+
    /// This migrations handles:
+
    ///
+
    ///  * v1 -> v2:
+
    ///    - Add a canonical reference rule for the default branch.
+
    ///    - Add the [`Version`] field
+
    ///    - Removes the `threshold`, but uses it in the above rule.
+
    pub fn migrate(self) -> Result<super::Doc, MigrationError> {
+
        let project = self.project()?;
+
        let Doc {
+
            version: _version,
+
            payload,
+
            delegates,
+
            threshold,
+
            visibility,
+
        } = self;
+

+
        let rules = match project {
+
            Some(project) => {
+
                let refname = git::refspec::QualifiedPattern::from(git::refs::branch(
+
                    project.default_branch(),
+
                ))
+
                .to_owned();
+
                let rules = [(
+
                    rules::Pattern::try_from(refname)?,
+
                    Rule::new(rules::Allowed::Delegates, threshold.into()),
+
                )]
+
                .into_iter()
+
                .collect::<RawRules>();
+
                // N.b. we always return the `delegates`, since we know we're
+
                // using the `Identity` marker.
+
                Rules::from_raw(rules, &mut |_| Ok(Delegates::new(delegates.clone())?))?
+
            }
+
            None => Rules::default(),
+
        };
+

+
        Ok(super::Doc {
+
            version: super::VersionTwo,
+
            payload,
+
            delegates,
+
            canonical_refs: CanonicalRefs::new(rules),
+
            visibility,
+
        })
+
    }
+

+
    /// Get the project payload, if it exists and is valid, out of this document.
+
    fn project(&self) -> Result<Option<Project>, PayloadError> {
+
        match self.payload.get(&PayloadId::project()) {
+
            Some(value) => serde_json::from_value((**value).clone())
+
                .map_err(PayloadError::from)
+
                .map(Some),
+
            None => Ok(None),
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
#[allow(clippy::unwrap_used)]
+
mod test {
+
    use crate::identity::VersionedRawDoc;
+

+
    use super::*;
+

+
    use nonempty::NonEmpty;
+
    use serde_json::json;
+

+
    #[test]
+
    fn test_parse_version() {
+
        // Harcoded version 1 of the identity document. We expect that parsing
+
        // this will include the version.
+
        let v1 = json!(
+
            {
+
                "payload": {
+
                    "xyz.radicle.project": {
+
                        "defaultBranch": "master",
+
                        "description": "Radicle Heartwood Protocol & Stack",
+
                        "name": "heartwood"
+
                    }
+
                },
+
                "delegates": [
+
                    "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
+
                    "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
+
                    "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
+
                ],
+
                "threshold": 1
+
            }
+
        );
+

+
        // Deserializing the v1 document to the current version of the mutable
+
        // document should not fail.
+
        let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
+
        let payload = [(
+
            PayloadId::project(),
+
            Payload {
+
                value: json!({
+
                    "name": "heartwood",
+
                    "description": "Radicle Heartwood Protocol & Stack",
+
                    "defaultBranch": "master",
+
                }),
+
            },
+
        )]
+
        .into_iter()
+
        .collect::<BTreeMap<_, _>>();
+
        let delegates = vec![
+
            "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
+
                .parse::<Did>()
+
                .unwrap(),
+
            "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW"
+
                .parse::<Did>()
+
                .unwrap(),
+
            "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
+
                .parse::<Did>()
+
                .unwrap(),
+
        ];
+

+
        let expected_doc = RawDoc {
+
            version: VersionOne,
+
            payload: payload.clone(),
+
            delegates: delegates.clone(),
+
            threshold: 1,
+
            visibility: Visibility::Public,
+
        };
+

+
        // And this is the expected outcome of the deserialization
+
        assert_eq!(doc, expected_doc);
+

+
        // Deserializing into the verified document should also succeed.
+
        let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
+
        let verified = doc.verified().unwrap();
+
        let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
+
        assert_eq!(
+
            verified,
+
            Doc {
+
                version: VersionOne,
+
                threshold: Threshold::new(1, &delegates).unwrap(),
+
                payload: payload.clone(),
+
                delegates,
+
                visibility: Visibility::Public,
+
            }
+
        );
+

+
        let versioned = serde_json::from_str::<VersionedRawDoc>(&v1.to_string()).unwrap();
+
        assert_eq!(versioned, VersionedRawDoc::V1(expected_doc));
+
    }
+
}
added radicle/src/identity/doc/version.rs
@@ -0,0 +1,109 @@
+
use std::num::NonZeroU64;
+
use std::ops::RangeInclusive;
+

+
use serde::{Deserialize, Serialize};
+
use std::fmt::Display;
+
use thiserror::Error;
+

+
/// A [`RangeInclusive`] of all valid identity document versions, known to this
+
/// release of the protocol.
+
pub const KNOWN_VERSIONS: RangeInclusive<Version> = Version::MIN..=Version::LATEST;
+

+
/// The version number of the repository identity documents.
+
///
+
/// The number cannot be zero and will range from `1` to the maximum known
+
/// version number for this given release. This range can be retrieved from the
+
/// [`KNOWN`] range.
+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Deserialize, Serialize)]
+
pub struct Version(NonZeroU64);
+

+
impl Version {
+
    /// The minimum `Version` is version `1`.
+
    pub const MIN: Version = Version(unsafe { NonZeroU64::new_unchecked(1) });
+

+
    /// The current latest `Version` is version `2`.
+
    pub const LATEST: Version = Version(unsafe { NonZeroU64::new_unchecked(2) });
+

+
    /// Get the latest known `Version`.
+
    pub const fn latest() -> Version {
+
        Self::LATEST
+
    }
+
}
+

+
impl Display for Version {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
+

+
impl From<Version> for u64 {
+
    fn from(version: Version) -> Self {
+
        version.0.into()
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
pub enum VersionError {
+
    #[error("encountered unexpected identity document version {actual}, expected {expected}")]
+
    Unexpected { expected: Version, actual: Version },
+
}
+

+
impl VersionError {
+
    /// Provide a verbose error.
+
    ///
+
    /// This will give a user more information on how to upgrade to a newer
+
    /// version of an identity document, if there is one.
+
    pub fn verbose(&self) -> String {
+
        const UNKOWN_VERSION_ERROR: &str = r#"
+
Perhaps a new version of the identity document is released which is not supported by the current client.
+
See https://radicle.xyz for the latest versions of Radicle.
+
The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;
+

+
        format!("{self}{UNKOWN_VERSION_ERROR}")
+
    }
+
}
+

+
macro_rules! version {
+
    ($def: expr, $name: ident) => {
+
        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
        #[serde(try_from = "Version", into = "Version")]
+
        pub struct $name;
+

+
        impl TryFrom<Version> for $name {
+
            type Error = VersionError;
+

+
            fn try_from(value: Version) -> Result<Self, Self::Error> {
+
                if value == $def {
+
                    Ok(Self)
+
                } else {
+
                    Err(VersionError::Unexpected {
+
                        expected: $def,
+
                        actual: value,
+
                    })
+
                }
+
            }
+
        }
+

+
        impl From<$name> for Version {
+
            fn from(_: $name) -> Self {
+
                $def
+
            }
+
        }
+

+
        impl std::fmt::Display for $name {
+
            fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
                $def.fmt(formatter)
+
            }
+
        }
+
    };
+
}
+

+
version!(*KNOWN_VERSIONS.start(), VersionOne);
+
version!(*KNOWN_VERSIONS.end(), VersionTwo);
+

+
impl Default for VersionOne {
+
    fn default() -> Self {
+
        debug_assert_eq!(Into::<Version>::into(Self), *KNOWN_VERSIONS.start());
+
        Self
+
    }
+
}
modified radicle/src/rad.rs
@@ -9,6 +9,7 @@ use thiserror::Error;
use crate::cob::ObjectId;
use crate::crypto::{Signer, Verified};
use crate::git;
+
use crate::git::canonical::rules;
use crate::identity::doc;
use crate::identity::doc::{DocError, RepoId, Visibility};
use crate::identity::project::{Project, ProjectName};
@@ -35,6 +36,8 @@ pub enum InitError {
    BareRepository { path: PathBuf },
    #[error("doc: {0}")]
    Doc(#[from] DocError),
+
    #[error("rule pattern: {0}")]
+
    Pattern(#[from] rules::PatternError),
    #[error("repository: {0}")]
    Repository(#[from] RepositoryError),
    #[error("project payload: {0}")]
@@ -72,7 +75,7 @@ pub fn init<G: Signer, S: WriteStorage>(
                .join(", "),
        )
    })?;
-
    let doc = identity::Doc::initial(proj, delegate, visibility);
+
    let doc = identity::Doc::initial(proj, delegate, visibility)?;
    let (project, identity) = Repository::init(&doc, &storage, signer)?;
    let url = git::Url::from(project.id);

modified radicle/src/storage.rs
@@ -119,6 +119,8 @@ pub enum RepositoryError {
    Quorum(#[from] canonical::QuorumError),
    #[error(transparent)]
    Refs(#[from] refs::Error),
+
    #[error("missing canonical reference rule for default branch")]
+
    MissingBranchRule,
}

impl RepositoryError {
modified radicle/src/storage/git.rs
@@ -11,7 +11,6 @@ use crypto::{Signer, Verified};
use once_cell::sync::Lazy;
use tempfile::TempDir;

-
use crate::git::canonical::Canonical;
use crate::identity::doc::DocError;
use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
@@ -744,12 +743,12 @@ impl ReadRepository for Repository {

    fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> {
        let doc = self.identity_doc()?;
-
        let project = doc.project()?;
-
        let branch_ref = git::refs::branch(project.default_branch());
-
        let raw = self.raw();
-
        let oid = Canonical::default_branch(self, &project, doc.delegates().into())?
-
            .quorum(doc.threshold(), raw)?;
-
        Ok((branch_ref, oid))
+
        let refname = git::refs::branch(doc.project()?.default_branch());
+
        Ok(doc
+
            .rules()
+
            .canonical(refname, self)?
+
            .ok_or(RepositoryError::MissingBranchRule)?
+
            .quorum(self.raw())?)
    }

    fn identity_head(&self) -> Result<Oid, RepositoryError> {
modified radicle/src/test/arbitrary.rs
@@ -11,10 +11,11 @@ use cyphernet::EcPk;
use qcheck::Arbitrary;

use crate::collections::RandomMap;
+
use crate::git::canonical::rules::RawRules;
use crate::identity::doc::Visibility;
use crate::identity::project::ProjectName;
use crate::identity::{
-
    doc::{Doc, DocAt, RawDoc, RepoId},
+
    doc::{Doc, DocAt, RawDoc, RepoId, VersionedRawDoc},
    project::Project,
    Did,
};
@@ -138,13 +139,19 @@ impl Arbitrary for Visibility {
    }
}

-
impl Arbitrary for RawDoc {
+
impl Arbitrary for VersionedRawDoc {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        let proj = Project::arbitrary(g);
        let delegate = Did::arbitrary(g);
        let visibility = Visibility::arbitrary(g);

-
        Self::new(proj, vec![delegate], 1, visibility)
+
        VersionedRawDoc::V2(RawDoc::new(
+
            proj,
+
            vec![delegate],
+
            1,
+
            RawRules::default(),
+
            visibility,
+
        ))
    }
}

@@ -157,8 +164,13 @@ impl Arbitrary for Doc {
            .collect::<Vec<_>>();
        let threshold = delegates.len() / 2 + 1;
        let visibility = Visibility::arbitrary(g);
-
        let doc = RawDoc::new(project, delegates, threshold, visibility);
-

+
        let doc = RawDoc::new(
+
            project,
+
            delegates,
+
            threshold,
+
            RawRules::default(),
+
            visibility,
+
        );
        doc.verified().unwrap()
    }
}