rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood0a3f13044a33bc9b4c13d87e1c872a907936be72
{
"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": "c80616590af912dd08bc6608b609f8456d7f00ac",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"title": "cli: test delegate convergence",
"state": {
"status": "archived",
"conflicts": []
},
"before": "a6e33ec19a662e23e998347ec2dd2d3c90bdb7ee",
"after": "0a3f13044a33bc9b4c13d87e1c872a907936be72",
"commits": [
"0a3f13044a33bc9b4c13d87e1c872a907936be72"
],
"target": "3fc92f83fe089263c470b328414d923227cf0e4a",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "c80616590af912dd08bc6608b609f8456d7f00ac",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "If a delegate is in a diverged state from the canonical head, ensure\nthat they can easily reset to another delegate's head to converge\nagain.",
"base": "ab532be0330358ab7582b8a40909d5dc1a5dbe95",
"oid": "2386780819d78b65063509242f1cb4cfc765929b",
"timestamp": 1717754376
},
{
"id": "972157a5e98e790a2e9909db9f74b93f4bb582fb",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Test has 3 peers diverge where Alice resets to one of theirs to\n converge",
"base": "a6e33ec19a662e23e998347ec2dd2d3c90bdb7ee",
"oid": "0a3f13044a33bc9b4c13d87e1c872a907936be72",
"timestamp": 1718185701
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "dfa87c50-1fd5-4a74-88a7-30dd10599210"
},
"info_url": "https://cci.rad.levitte.org//dfa87c50-1fd5-4a74-88a7-30dd10599210.html"
}
Started at: 2026-09-08 03:37:42.896412+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/dfa87c50-1fd5-4a74-88a7-30dd10599210/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 38 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout c80616590af912dd08bc6608b609f8456d7f00ac
✓ Switched to branch patch/c806165 at revision 972157a
✓ Branch patch/c806165 setup to track rad/patches/c80616590af912dd08bc6608b609f8456d7f00ac
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 0a3f13044a33bc9b4c13d87e1c872a907936be72
HEAD is now at 0a3f1304 cli: test delegate convergence
Exit code: 0
$ rad patch show c80616590af912dd08bc6608b609f8456d7f00ac -p
╭────────────────────────────────────────────────────────────────────────────────╮
│ Title cli: test delegate convergence │
│ Patch c80616590af912dd08bc6608b609f8456d7f00ac │
│ Author fintohaps z6Mkire…SQZ3voM │
│ Head 0a3f13044a33bc9b4c13d87e1c872a907936be72 │
│ Base a6e33ec19a662e23e998347ec2dd2d3c90bdb7ee │
│ Target master │
│ Branches patch/c806165 │
│ Commits ahead 1, behind 1309 │
│ Status archived │
│ │
│ If a delegate is in a diverged state from the canonical head, ensure │
│ that they can easily reset to another delegate's head to converge │
│ again. │
├────────────────────────────────────────────────────────────────────────────────┤
│ 0a3f130 cli: test delegate convergence │
├────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision c806165 @ ab532be..2386780 by fintohaps z6Mkire…SQZ3voM 2 years ago │
│ ↑ Revision 972157a @ a6e33ec..0a3f130 by fintohaps z6Mkire…SQZ3voM 2 years ago │
╰────────────────────────────────────────────────────────────────────────────────╯
commit 0a3f13044a33bc9b4c13d87e1c872a907936be72
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Fri Jun 7 11:51:45 2024 +0200
cli: test delegate convergence
If a delegate is in a diverged state from the canonical head, ensure
that they can easily reset to another delegate's head to converge
again.
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
diff --git a/radicle-cli/examples/git/git-push-converge.md b/radicle-cli/examples/git/git-push-converge.md
new file mode 100644
index 000000000..9ae27f7f1
--- /dev/null
+++ b/radicle-cli/examples/git/git-push-converge.md
@@ -0,0 +1,111 @@
+In this scenario we check that we can easily reset our canonical head to the
+head of another delegate when there is divergence between the 3 delegates.
+
+First we add our new delegates, Bob & Eve, to our repo, while also setting the
+`threshold` to `2`:
+
+``` ~alice
+$ rad id update --title "Add Bob & Eve" --description "" --delegate did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --delegate did:key:z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z --threshold 2 --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji -q
+73bd6ea3f88eb0687afd13ee13bfb31a9eb0ccd2
+```
+
+Bob and Eve will fetch the changes to ensure they hear about their delegate
+responsibilities:
+
+``` ~bob
+$ rad sync --fetch
+✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
+✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z..
+✓ Fetched repository from 2 seed(s)
+```
+
+``` ~eve
+$ rad sync --fetch
+✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
+✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
+✓ Fetched repository from 2 seed(s)
+```
+
+To demonstrate the divergence, Alice, Bob, and Eve will all create a new change,
+pushing to their `rad` remote -- but they won't sync to the network just yet:
+
+``` ~alice
+$ git commit -m "Alice's commit" --allow-empty -q
+$ git push rad -o no-sync
+$ git ls-remote rad
+f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
+```
+
+``` ~bob
+$ git commit -m "Bob's commit" --allow-empty -q
+$ git push rad -o no-sync
+$ git ls-remote rad
+f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
+```
+
+``` ~eve
+$ git commit -m "Eve's commit" --allow-empty -q
+$ git push rad -o no-sync
+$ git ls-remote rad
+f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
+```
+
+Alice adds Bob and Eve as remotes and starts to notice that the `no quorum was
+found` error is showing up:
+
+``` ~alice
+$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk --name bob
+✓ Follow policy updated for z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (bob)
+✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkt67…v4N1tRk..
+✗ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z.. error: no quorum was found
+✓ 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 z6Mkt67…v4N1tRk.. error: no quorum was found
+✗ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z.. error: no quorum was found
+✓ Remote eve added
+✓ Remote-tracking branch eve/master created for z6Mkux1…nVhib7Z
+```
+
+Alice does indeed have Bob and Eve's references, however, a new canonical
+`refs/heads/master` cannot be decided while they're out of quorum. This can be
+remedied by the delegates agreeing upon which way to move forward. In this case,
+Alice resets her `master` to `bob/master`:
+
+``` ~alice
+$ git reset bob/master --hard
+HEAD is now at 0801f02 Bob's commit
+```
+
+She can then force push to update the canonical head to the new agreed upon
+commit:
+
+``` ~alice (stderr)
+$ git push rad -f
+✓ Canonical head updated to 0801f020f11a08ec7a96d1710ec24e5e005499d1
+✓ Synced with 2 node(s)
+To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
+ + d09e634...0801f02 master -> master (forced update)
+```
+
+We can convince ourselves that the canonical branch has indeed changed by using
+`git ls-remote` for each of the delegates:
+
+``` ~alice
+$ git ls-remote rad
+0801f020f11a08ec7a96d1710ec24e5e005499d1 refs/heads/master
+```
+
+``` ~bob
+$ git ls-remote rad
+0801f020f11a08ec7a96d1710ec24e5e005499d1 refs/heads/master
+```
+
+``` ~eve
+$ git ls-remote rad
+0801f020f11a08ec7a96d1710ec24e5e005499d1 refs/heads/master
+```
+
+
+
diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs
index dbf44beaf..d4de41bb7 100644
--- a/radicle-cli/tests/commands.rs
+++ b/radicle-cli/tests/commands.rs
@@ -2363,6 +2363,57 @@ fn git_push_diverge() {
.unwrap();
}
+#[test]
+fn git_push_converge() {
+ let mut environment = Environment::new();
+ let alice = environment.node(Config::test(Alias::new("alice")));
+ 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();
+
+ 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();
+ let mut eve = eve.spawn();
+
+ bob.connect(&alice).connect(&eve).converge([&alice]);
+ eve.connect(&alice).converge([&alice]);
+ bob.fork(acme, working.join("bob")).unwrap();
+ eve.fork(acme, working.join("eve")).unwrap();
+ alice.has_remote_of(&acme, &bob.id);
+ alice.has_remote_of(&acme, &eve.id);
+
+ formula(&environment.tmp(), "examples/git/git-push-converge.md")
+ .unwrap()
+ .home(
+ "alice",
+ working.join("alice"),
+ [("RAD_HOME", alice.home.path().display())],
+ )
+ .home(
+ "bob",
+ working.join("bob").join("heartwood"),
+ [("RAD_HOME", bob.home.path().display())],
+ )
+ .home(
+ "eve",
+ working.join("eve").join("heartwood"),
+ [("RAD_HOME", eve.home.path().display())],
+ )
+ .run()
+ .unwrap();
+}
+
#[test]
fn rad_push_and_pull_patches() {
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 dfa87c50-1fd5-4a74-88a7-30dd10599210 -v /opt/radcis/ci.rad.levitte.org/cci/state/dfa87c50-1fd5-4a74-88a7-30dd10599210/s:/dfa87c50-1fd5-4a74-88a7-30dd10599210/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/dfa87c50-1fd5-4a74-88a7-30dd10599210/w:/dfa87c50-1fd5-4a74-88a7-30dd10599210/w -w /dfa87c50-1fd5-4a74-88a7-30dd10599210/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /dfa87c50-1fd5-4a74-88a7-30dd10599210/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"
}