Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: test canonical tags
Archived fintohaps opened 10 months ago

Add a test to exercise adding canonical reference rules and pushing new canonical tags.

A change in rad id was required to allow adding a new payload to the document, i.e. one that does not already exist.

Adding rules is also documented in the man page for rad id.

Stacked on Patch:

  • ID: bea09df15505cfcebc72ad40f629747d2e82f670
  • Base: c7d8494bba0989cd30af9649a1914dcca80509ec
4 files changed +313 -2 c7d8494b 7dab01cd
added crates/radicle-cli/examples/git/git-push-canonical-tags.md
@@ -0,0 +1,217 @@
+
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.crefs`. 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 id update --title "Add canonical reference rules" --payload xyz.radicle.crefs rules '{ "refs/heads/master": { "threshold": 1, "allow": "delegates" }, "refs/tags/*": { "threshold": 1, "allow": "delegates" }, "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" }}'
+
✓ Identity revision 0f7073156153070c3f4e8e2ad0320dc91ed7f2b5 created
+
╭────────────────────────────────────────────────────────────────────────╮
+
│ Title    Add canonical reference rules                                 │
+
│ Revision 0f7073156153070c3f4e8e2ad0320dc91ed7f2b5                      │
+
│ Blob     39891b4f4365fdc8849c179ad0b780964dd1aafd                      │
+
│ Author   did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi      │
+
│ State    accepted                                                      │
+
│ Quorum   yes                                                           │
+
├────────────────────────────────────────────────────────────────────────┤
+
│ ✓ did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi alice (you) │
+
╰────────────────────────────────────────────────────────────────────────╯
+

+
@@ -1,13 +1,29 @@
+
 {
+
   "payload": {
+
+    "xyz.radicle.crefs": {
+
+      "rules": {
+
+        "refs/heads/master": {
+
+          "allow": "delegates",
+
+          "threshold": 1
+
+        },
+
+        "refs/tags/*": {
+
+          "allow": "delegates",
+
+          "threshold": 1
+
+        },
+
+        "refs/tags/qa/*": {
+
+          "allow": "delegates",
+
+          "threshold": 1
+
+        }
+
+      }
+
+    },
+
     "xyz.radicle.project": {
+
       "defaultBranch": "master",
+
       "description": "Radicle Heartwood Protocol & Stack",
+
       "name": "heartwood"
+
     }
+
   },
+
   "delegates": [
+
     "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
+
   ],
+
   "threshold": 1
+
 }
+
```
+

+
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 seed(s)
+
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/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://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
+
 * [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:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
+
✓ Target met: 1 seed(s)
+
✓ 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:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
+
✓ Target met: 1 seed(s)
+
✓ 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
+
45e23a57373e917d88596fc9d19619e2f1066e8b
+
$ rad id update --title "Update canonical reference rules" --payload xyz.radicle.crefs rules '{ "refs/heads/master": { "threshold": 1, "allow": "delegates" }, "refs/tags/*": { "threshold": 2, "allow": "delegates" }, "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" } }' -q
+
456a105638c704ec9694d38f8cede99178eea04a
+
```
+

+
**Note:** here we have to specify all the rules again to update the `threshold`.
+
In reality, you can use `rad id update --edit` and edit the payload in your
+
editor instead.
+

+
``` ~bob
+
$ rad sync -f
+
Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 1 potential seed(s).
+
✓ Target met: 1 seed(s)
+
🌱 Fetched from z6MknSL…StBU8Vi
+
$ rad id accept 456a105638c704ec9694d38f8cede99178eea04a -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 seed(s)
+
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/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://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/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 seed(s)
+
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/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 seed(s)
+
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+
 * [new tag]         qa/v2.1 -> qa/v2.1
+
```
modified crates/radicle-cli/src/commands/id.rs
@@ -5,6 +5,7 @@ use std::{ffi::OsString, io};
use anyhow::{anyhow, Context};

use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
+
use radicle::identity::crefs::GetCanonicalRefs as _;
use radicle::identity::{doc, Doc, Identity, PayloadError, RawDoc, Visibility};
use radicle::node::device::Device;
use radicle::node::NodeId;
@@ -464,7 +465,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                            anyhow::bail!("payload `{id}` is not a map");
                        }
                    } else {
-
                        anyhow::bail!("payload `{id}` not found in identity document");
+
                        proposal
+
                            .payload
+
                            .insert(id, serde_json::json!({ key: val }).into());
                    }
                }
                proposal
@@ -494,6 +497,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                anyhow::bail!("failed to verify `xyz.radicle.project`, {e}");
            }
            let proposal = proposal.verified()?;
+
            if let Err(PayloadError::Json(e)) = proposal.canonical_refs() {
+
                anyhow::bail!("failed to verify `xyz.radicle.crefs`, {e}");
+
            }
            if proposal == current.doc {
                if !options.quiet {
                    term::print(term::format::italic(
modified crates/radicle-cli/tests/commands.rs
@@ -3101,6 +3101,48 @@ fn git_tag() {
}

#[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");
+

+
    let rid = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
+
    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]);
+
    bob.clone(rid, working.join("bob")).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 rad_workflow() {
    let mut environment = Environment::new();
    let alice = environment.node(Config::test(Alias::new("alice")));
modified rad-id.1.adoc
@@ -18,7 +18,7 @@ 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>...] +

== Description

@@ -176,3 +176,49 @@ To remove a delegate and update the threshold, use the *--rescind* option:
As with adding a delegate, this change will require approval from the remaining
delegates. Make sure you set an appropriate new threshold when removing
delegates!
+

+
=== Adding Canonical References Rules
+

+
To update the canonical reference rules of the project, use the `--payload
+
xyz.radicle.crefs` option while updating, `rules` as the key, and the rules
+
object as the value. Here is an example below:
+

+
    $ rad id update --title "Update canonical reference rules" \
+
        --payload xyz.radicle.crefs rules '{
+
              "refs/heads/master": { "threshold": 1, "allow": "delegates" },
+
              "refs/tags/*": { "threshold": 2, "allow": "delegates" },
+
              "refs/tags/qa/*": { "threshold": 1, "allow": "delegates" }
+
          }'
+

+
Alternatively, you can use the `--edit` option for the `update` command and edit
+
the payload directly. Here is an example of what that may look like:
+

+
    {
+
      "payload": {
+
        "xyz.radicle.crefs": {
+
          "rules": {
+
            "refs/heads/master": {
+
              "allow": "delegates",
+
              "threshold": 1
+
            },
+
            "refs/tags/*": {
+
              "allow": "delegates",
+
              "threshold": 1
+
            },
+
            "refs/tags/qa/*": {
+
              "allow": "delegates",
+
              "threshold": 1
+
            }
+
          }
+
        },
+
        "xyz.radicle.project": {
+
          "defaultBranch": "master",
+
          "description": "Radicle Heartwood Protocol & Stack",
+
          "name": "heartwood"
+
        }
+
      },
+
      "delegates": [
+
        "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
+
      ],
+
      "threshold": 1
+
    }