rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodf91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "ed97c94fce693c7e7b8fe5f324c22d0f51d2fe9d",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"title": "cli: improve rad id allow list UX",
"state": {
"status": "merged",
"conflicts": []
},
"before": "6f91f2fb8fc29d8ecac9653411918ab48a6fc44e",
"after": "f91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36",
"commits": [
"f91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36"
],
"target": "3fc92f83fe089263c470b328414d923227cf0e4a",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "ed97c94fce693c7e7b8fe5f324c22d0f51d2fe9d",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "The previous implementation of using the `--allow` option in `rad id\nupdate` would be an absolute list, rather than additive. That is, if\nthere were existing DIDs not passed in via `--allow` then they would\nbe remove.\n\nChange this behaviour to check the existing value of the document's\nallow list, and add to it if `--allow` is specified.\n\nTo enable the user to also remove a DID, the `--disallow` option is\nalso added.\n\nThe correct usage of these options is improved by checking the\nvalidity of the `--visibility` option being used, the current state of\nthe repository's privacy, and the use of the `--allow` and\n`--disallow` options.",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "448c6b01f19c2452b03a8fe94f9b4d53580e78aa",
"timestamp": 1716221692
},
{
"id": "f91d92977f4d8f14e44c75184f1e524c03e0750e",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Error if disallow and allow are not disjoint\n- Improve test to show off use of multiple `--allow` and `--disallow`",
"base": "6f91f2fb8fc29d8ecac9653411918ab48a6fc44e",
"oid": "f91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36",
"timestamp": 1718895425
},
{
"id": "8ef265ea642f1c6a6d045fbe6ead85f6753a018f",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Small CLI output edits.",
"base": "6f91f2fb8fc29d8ecac9653411918ab48a6fc44e",
"oid": "f26be552c20e8db05ce9e0aad9d61ab6e4e2bf48",
"timestamp": 1718896967
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "3e2f89a6-d6af-4f55-9496-76477ee3a5dd"
},
"info_url": "https://cci.rad.levitte.org//3e2f89a6-d6af-4f55-9496-76477ee3a5dd.html"
}
Started at: 2026-09-08 05:21:54.677090+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/3e2f89a6-d6af-4f55-9496-76477ee3a5dd/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 38 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout ed97c94fce693c7e7b8fe5f324c22d0f51d2fe9d
✓ Switched to branch patch/ed97c94 at revision f91d929
✓ Branch patch/ed97c94 setup to track rad/patches/ed97c94fce693c7e7b8fe5f324c22d0f51d2fe9d
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout f91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36
HEAD is now at f91db85c cli: improve rad id allow list UX
Exit code: 0
$ rad patch show ed97c94fce693c7e7b8fe5f324c22d0f51d2fe9d -p
╭────────────────────────────────────────────────────────────────────────────────╮
│ Title cli: improve rad id allow list UX │
│ Patch ed97c94fce693c7e7b8fe5f324c22d0f51d2fe9d │
│ Author fintohaps z6Mkire…SQZ3voM │
│ Head f91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36 │
│ Base 6f91f2fb8fc29d8ecac9653411918ab48a6fc44e │
│ Target master │
│ Branches patch/ed97c94 │
│ Commits ahead 1, behind 1284 │
│ Status merged │
│ │
│ The previous implementation of using the `--allow` option in `rad id │
│ update` would be an absolute list, rather than additive. That is, if │
│ there were existing DIDs not passed in via `--allow` then they would │
│ be remove. │
│ │
│ Change this behaviour to check the existing value of the document's │
│ allow list, and add to it if `--allow` is specified. │
│ │
│ To enable the user to also remove a DID, the `--disallow` option is │
│ also added. │
│ │
│ The correct usage of these options is improved by checking the │
│ validity of the `--visibility` option being used, the current state of │
│ the repository's privacy, and the use of the `--allow` and │
│ `--disallow` options. │
├────────────────────────────────────────────────────────────────────────────────┤
│ f91db85 cli: improve rad id allow list UX │
├────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision ed97c94 @ 064ece3..448c6b0 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision f91d929 @ 6f91f2f..f91db85 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 8ef265e @ 6f91f2f..f26be55 by cloudhead z6MksFq…bS9wzpT 2 years ago │
│ └─ ✓ merged by cloudhead z6MksFq…bS9wzpT │
╰────────────────────────────────────────────────────────────────────────────────╯
commit f91db85cd9cf53cc9478ac7a3a8bfe27acbd7b36
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Mon May 20 17:10:43 2024 +0100
cli: improve rad id allow list UX
The previous implementation of using the `--allow` option in `rad id
update` would be an absolute list, rather than additive. That is, if
there were existing DIDs not passed in via `--allow` then they would
be remove.
Change this behaviour to check the existing value of the document's
allow list, and add to it if `--allow` is specified.
To enable the user to also remove a DID, the `--disallow` option is
also added.
The correct usage of these options is improved by checking the
validity of the `--visibility` option being used, the current state of
the repository's privacy, and the use of the `--allow` and
`--disallow` options.
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
diff --git a/radicle-cli/examples/rad-id-private.md b/radicle-cli/examples/rad-id-private.md
new file mode 100644
index 000000000..63083927a
--- /dev/null
+++ b/radicle-cli/examples/rad-id-private.md
@@ -0,0 +1,101 @@
+When we are working with a private repository, we can modify the list of peers
+we allow by using the `rad id` command with its `--allow` and `--disallow`
+options. Both options can be specified multiple times in the same command line call:
+
+Here we will add Bob and Eve's DIDs to the `allow`list:
+
+```
+$ rad id update --title "Allow Bob & Eve" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --allow did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -q
+...
+$ rad inspect --identity
+{
+ "payload": {
+ "xyz.radicle.project": {
+ "defaultBranch": "master",
+ "description": "radicle heartwood protocol & stack",
+ "name": "heartwood"
+ }
+ },
+ "delegates": [
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
+ ],
+ "threshold": 1,
+ "visibility": {
+ "type": "private",
+ "allow": [
+ "did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk",
+ "did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z"
+ ]
+ }
+}
+```
+
+To remove a peer's DID, we can use the `--disallow` option. Let's remove both of them again:
+
+```
+$ rad id update --title "Remove allow list" --description "" --disallow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --disallow did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z
+...
+$ rad inspect --identity
+{
+ "payload": {
+ "xyz.radicle.project": {
+ "defaultBranch": "master",
+ "description": "radicle heartwood protocol & stack",
+ "name": "heartwood"
+ }
+ },
+ "delegates": [
+ "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
+ ],
+ "threshold": 1,
+ "visibility": {
+ "type": "private"
+ }
+}
+```
+
+Note that using both `--disallow` and `--allow` with the same DID will result in
+an error:
+
+``` (fails)
+$ rad id update --title "Remove allow list" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --disallow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+✗ Error: --allow and --disallow must have different DIDs: ["did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"]
+```
+
+Allowing or disallowing the same peer twice will result in an error the second
+call, since there is no update specified:
+
+```
+$ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
+...
+```
+``` (fails)
+$ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q
+✗ Error: no update specified
+✗ Hint: an update to the identity must be specified, run `rad id update -h` to see the available options
+```
+
+If we attempt to change the list while also changing the repository to `public`,
+then the command will fail since there is no longer an allow list to work with:
+
+``` (fails)
+$ rad id update --visibility public --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+✗ Error: --allow and --disallow cannot be used with `--visibility public`
+```
+
+Let's change the repository to `public`:
+
+```
+$ rad id update --title "IPO" --description "" --visibility public -q
+...
+```
+
+Now, if we attempt to change the `allow` list we also get an error with a
+helpful hint:
+
+``` (fails)
+$ rad id update --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
+✗ Error: --allow and --disallow should only be used for private repositories
+✗ Hint: use `--visibility private` to make the repository private, or perhaps you meant to use `--delegate`/`--rescind`
+```
+
diff --git a/radicle-cli/src/commands/id.rs b/radicle-cli/src/commands/id.rs
index 77729bfef..2b23d091d 100644
--- a/radicle-cli/src/commands/id.rs
+++ b/radicle-cli/src/commands/id.rs
@@ -1,3 +1,5 @@
+use std::collections::BTreeSet;
+use std::str::FromStr;
use std::{ffi::OsString, io};
use anyhow::{anyhow, Context};
@@ -32,7 +34,8 @@ Usage
rad id update [--title <string>] [--description <string>]
[--delegate <did>] [--rescind <did>]
[--threshold <num>] [--visibility <private | public>]
- [--allow <did>] [--no-confirm] [--payload <id> <key> <val>...]
+ [--allow <did>] [--disallow <did>]
+ [--no-confirm] [--payload <id> <key> <val>...]
[<option>...]
rad id edit <revision-id> [--title <string>] [--description <string>] [<option>...]
rad id show <revision-id> [<option>...]
@@ -59,7 +62,9 @@ pub enum Operation {
delegate: Vec<Did>,
rescind: Vec<Did>,
threshold: Option<usize>,
- visibility: Option<Visibility>,
+ visibility: Option<EditVisibility>,
+ allow: BTreeSet<Did>,
+ disallow: BTreeSet<Did>,
payload: Vec<(doc::PayloadId, String, json::Value)>,
},
AcceptRevision {
@@ -83,6 +88,29 @@ pub enum Operation {
ListRevisions,
}
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum EditVisibility {
+ #[default]
+ Public,
+ Private,
+}
+
+#[derive(thiserror::Error, Debug)]
+#[error("'{0}' is not a valid visibility type")]
+pub struct EditVisibilityParseError(String);
+
+impl FromStr for EditVisibility {
+ type Err = EditVisibilityParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "public" => Ok(EditVisibility::Public),
+ "private" => Ok(EditVisibility::Private),
+ _ => Err(EditVisibilityParseError(s.to_owned())),
+ }
+ }
+}
+
#[derive(Default, PartialEq, Eq)]
pub enum OperationName {
Accept,
@@ -114,7 +142,9 @@ impl Args for Options {
let mut description: Option<String> = None;
let mut delegate: Vec<Did> = Vec::new();
let mut rescind: Vec<Did> = Vec::new();
- let mut visibility: Option<Visibility> = None;
+ 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();
@@ -172,11 +202,12 @@ impl Args for Options {
Long("allow") => {
let value = parser.value()?;
let did = term::args::did(&value)?;
- if let Some(Visibility::Private { allow }) = &mut visibility {
- allow.insert(did);
- } else {
- visibility = Some(Visibility::private([did]));
- }
+ allow.insert(did);
+ }
+ Long("disallow") => {
+ let value = parser.value()?;
+ let did = term::args::did(&value)?;
+ disallow.insert(did);
}
Long("visibility") => {
let value = parser.value()?;
@@ -244,6 +275,8 @@ impl Args for Options {
rescind,
threshold,
visibility,
+ allow,
+ disallow,
payload,
},
};
@@ -349,12 +382,51 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
rescind,
threshold,
visibility,
+ allow,
+ disallow,
payload,
} => {
let proposal = {
let mut proposal = current.doc.clone();
proposal.threshold = threshold.unwrap_or(proposal.threshold);
- proposal.visibility = visibility.unwrap_or(proposal.visibility);
+
+ if !allow.is_disjoint(&disallow) {
+ let overlap = allow
+ .intersection(&disallow)
+ .map(Did::to_string)
+ .collect::<Vec<_>>();
+ anyhow::bail!("--allow and --disallow must have different DIDs: {overlap:?}")
+ }
+
+ match (&mut proposal.visibility, visibility) {
+ (Visibility::Public, None | Some(EditVisibility::Public)) if !allow.is_empty() || !disallow.is_empty() => {
+ return Err(Error::WithHint {
+ err:
+ anyhow!("--allow and --disallow should only be used for private repositories"),
+ hint: "use `--visibility private` to make the repository private, or perhaps you meant to use `--delegate`/`--rescind`",
+ }.into())
+ }
+ (Visibility::Public, None | Some(EditVisibility::Public)) => { /* no-op */ },
+ (Visibility::Private { allow: existing }, None | Some(EditVisibility::Private)) => {
+ for did in allow {
+ existing.insert(did);
+ }
+ for did in disallow {
+ existing.remove(&did);
+ }
+ }
+ (Visibility::Public, Some(EditVisibility::Private)) => {
+ // We ignore disallow since only allowing matters and
+ // the sets are disjoint
+ proposal.visibility = Visibility::Private { allow };
+ }
+ (Visibility::Private { .. }, Some(EditVisibility::Public)) if !allow.is_empty() || !disallow.is_empty() => {
+ anyhow::bail!("--allow and --disallow cannot be used with `--visibility public`")
+ }
+ (Visibility::Private { .. }, Some(EditVisibility::Public)) => {
+ proposal.visibility = Visibility::Public;
+ }
+ }
proposal.delegates = NonEmpty::from_vec(
proposal
.delegates
diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs
index e9a555e88..61ee44d5d 100644
--- a/radicle-cli/tests/commands.rs
+++ b/radicle-cli/tests/commands.rs
@@ -716,6 +716,30 @@ fn rad_id_unknown_field() {
.unwrap();
}
+#[test]
+fn rad_id_private() {
+ let mut environment = Environment::new();
+ let alice = environment.node(Config::test(Alias::new("alice")));
+ let working = environment.tmp().join("working");
+
+ fixtures::repository(working.join("alice"));
+
+ test(
+ "examples/rad-init-private.md",
+ working.join("alice"),
+ Some(&alice.home),
+ [],
+ )
+ .unwrap();
+ test(
+ "examples/rad-id-private.md",
+ working.join("alice"),
+ Some(&alice.home),
+ [],
+ )
+ .unwrap();
+}
+
#[test]
fn rad_node_connect() {
let mut environment = Environment::new();
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 3e2f89a6-d6af-4f55-9496-76477ee3a5dd -v /opt/radcis/ci.rad.levitte.org/cci/state/3e2f89a6-d6af-4f55-9496-76477ee3a5dd/s:/3e2f89a6-d6af-4f55-9496-76477ee3a5dd/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/3e2f89a6-d6af-4f55-9496-76477ee3a5dd/w:/3e2f89a6-d6af-4f55-9496-76477ee3a5dd/w -w /3e2f89a6-d6af-4f55-9496-76477ee3a5dd/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /3e2f89a6-d6af-4f55-9496-76477ee3a5dd/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}