From e991bd7476d9038eca540218846a0782dfd6abbd Mon Sep 17 00:00:00 2001 From: Adrian Duke Date: Thu, 7 May 2026 16:42:42 +0100 Subject: [PATCH] cli/examples: Introduce a suite of merge and revert tests for patch.target --- ...d-patch-merge-into-canonical-ref-branch.md | 96 ++++++++++++ .../rad-patch-merge-unauthorized-branch.md | 97 ++++++++++++ .../examples/rad-patch-merge-wrong-branch.md | 82 ++++++++++ .../rad-patch-revert-custom-branch.md | 146 ++++++++++++++++++ crates/radicle-cli/tests/commands/patch.rs | 57 +++++++ 5 files changed, 478 insertions(+) create mode 100644 crates/radicle-cli/examples/rad-patch-merge-into-canonical-ref-branch.md create mode 100644 crates/radicle-cli/examples/rad-patch-merge-unauthorized-branch.md create mode 100644 crates/radicle-cli/examples/rad-patch-merge-wrong-branch.md create mode 100644 crates/radicle-cli/examples/rad-patch-revert-custom-branch.md diff --git a/crates/radicle-cli/examples/rad-patch-merge-into-canonical-ref-branch.md b/crates/radicle-cli/examples/rad-patch-merge-into-canonical-ref-branch.md new file mode 100644 index 00000000..26180d82 --- /dev/null +++ b/crates/radicle-cli/examples/rad-patch-merge-into-canonical-ref-branch.md @@ -0,0 +1,96 @@ +# Merging patches into non-default canonical branches + +First, we update the identity document to add a canonical reference rule for a new `accepted` branch, allowing delegates to merge into it. + +``` +$ rad id update --title "Add accepted branch" --payload xyz.radicle.crefs rules '{ "refs/heads/accepted": { "threshold": 1, "allow": "delegates" } }' -q +[..] +``` + +Now, let's create the `accepted` branch and push it to the repository so it becomes a tracked canonical reference: + +``` (stderr) +$ git checkout -b accepted +Switched to a new branch 'accepted' +``` + +``` +$ git commit --allow-empty -m "Initialize accepted branch" +[accepted [..]] Initialize accepted branch +``` + +``` (stderr) +$ git push rad accepted +✓ Canonical reference refs/heads/accepted updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new branch] accepted -> accepted +``` + +Next, we create a feature branch and open a patch. We use the `patch.target` push option to explicitly state that this patch is intended for `refs/heads/accepted` rather than the default branch (`master`). + +``` (stderr) +$ git checkout -b feature/1 +Switched to a new branch 'feature/1' +$ touch FEATURE.md +$ git add FEATURE.md +``` + +``` +$ git commit -m "Add new feature" +[feature/1 [..]] Add new feature + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` + +``` (stderr) +$ git push -o patch.message="Add new feature" -o patch.target="refs/heads/accepted" rad HEAD:refs/patches +✓ Patch [..] opened +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +We can verify the patch is open: + +``` +$ rad patch +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ● [.. ] Add new feature alice (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` + +Now, Alice merges the feature into the `accepted` branch and pushes it. Because `accepted` is a valid canonical reference and Alice is a delegate, the remote helper should detect the merge and update the patch status. + +``` (stderr) +$ git checkout accepted +Switched to branch 'accepted' +``` + +``` +$ git merge feature/1 +Updating [..] +Fast-forward + FEATURE.md | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` + +``` (stderr) +$ git push rad accepted +✓ Patch [..] merged +✓ Canonical reference refs/heads/accepted updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + [..]..[..] accepted -> accepted +``` + +Finally, we verify that the patch has been successfully marked as merged, even though it wasn't merged into the default branch. + +``` +$ rad patch list --merged +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ✓ [.. ] Add new feature alice (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` diff --git a/crates/radicle-cli/examples/rad-patch-merge-unauthorized-branch.md b/crates/radicle-cli/examples/rad-patch-merge-unauthorized-branch.md new file mode 100644 index 00000000..a8641acc --- /dev/null +++ b/crates/radicle-cli/examples/rad-patch-merge-unauthorized-branch.md @@ -0,0 +1,97 @@ +# Merging patches into an unauthorized branch + +First, we update the identity document to add a canonical reference rule for a new `accepted` branch, but we only allow Alice to merge into it. + +``` ~alice +$ rad id update --title "Add accepted branch" --payload xyz.radicle.crefs rules '{ "refs/heads/accepted": { "threshold": 1, "allow": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"] } }' -q +[..] +``` + +Now, let's create the `accepted` branch and push it: + +``` ~alice (stderr) +$ git checkout -b accepted +Switched to a new branch 'accepted' +``` +``` ~alice +$ git commit --allow-empty -m "Initialize accepted branch" +[accepted [..]] Initialize accepted branch +``` +``` ~alice (stderr) +$ git push rad accepted +✓ Canonical reference refs/heads/accepted updated to target commit [..] +✓ Synced with 1 seed(s) +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new branch] accepted -> accepted +``` + +Next, Bob clones the repository and opens a patch targeting `accepted`. + +``` ~bob +$ rad clone rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji +✓ Creating checkout in ./heartwood.. +✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added +✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi +✓ Repository successfully cloned under [..] +╭────────────────────────────────────╮ +│ heartwood │ +│ Radicle Heartwood Protocol & Stack │ +│ 0 issues · 0 patches │ +╰────────────────────────────────────╯ +Run `cd ./heartwood` to go to the repository directory. +``` +``` ~bob (stderr) +$ cd heartwood +$ git checkout -b feature/1 +Switched to a new branch 'feature/1' +$ touch FEATURE.md +$ git add FEATURE.md +``` +``` ~bob +$ git commit -m "Add new feature" +[feature/1 [..]] Add new feature + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` +``` ~bob (stderr) +$ git push -o patch.message="Add new feature" -o patch.target="refs/heads/accepted" rad HEAD:refs/patches +✓ Patch [..] opened +✓ Synced with 1 seed(s) +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk + * [new reference] HEAD -> refs/patches +``` + +Now, Bob tries to merge the feature into the `accepted` branch and push it. + +``` ~bob (stderr) +$ git checkout -t rad/accepted +Switched to a new branch 'accepted' +``` +``` ~bob +$ git merge feature/1 +Merge made by the 'ort' strategy. + FEATURE.md | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` +``` ~bob (stderr) +$ git push rad accepted +✓ Synced with 1 seed(s) +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk + * [new branch] accepted -> accepted +``` + +Notice that the canonical reference was *not* updated because Bob is not authorized. The patch should remain open. + +``` ~bob +$ rad patch list --merged +Nothing to show. +``` +``` ~bob +$ rad patch list --open +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ● [.. ] Add new feature bob (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` diff --git a/crates/radicle-cli/examples/rad-patch-merge-wrong-branch.md b/crates/radicle-cli/examples/rad-patch-merge-wrong-branch.md new file mode 100644 index 00000000..72b09f08 --- /dev/null +++ b/crates/radicle-cli/examples/rad-patch-merge-wrong-branch.md @@ -0,0 +1,82 @@ +# Merging patches into the wrong branch + +First, we update the identity document to add a canonical reference rule for a new `accepted` branch. + +``` +$ rad id update --title "Add accepted branch" --payload xyz.radicle.crefs rules '{ "refs/heads/accepted": { "threshold": 1, "allow": "delegates" } }' -q +[..] +``` + +Now, let's create the `accepted` branch and push it: + +``` (stderr) +$ git checkout -b accepted +Switched to a new branch 'accepted' +``` +``` +$ git commit --allow-empty -m "Initialize accepted branch" +[accepted [..]] Initialize accepted branch +``` +``` (stderr) +$ git push rad accepted +✓ Canonical reference refs/heads/accepted updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new branch] accepted -> accepted +``` + +Next, we create a feature branch and open a patch targeting `accepted`. + +``` (stderr) +$ git checkout -b feature/1 +Switched to a new branch 'feature/1' +$ touch FEATURE.md +$ git add FEATURE.md +``` +``` +$ git commit -m "Add new feature" +[feature/1 [..]] Add new feature + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` +``` (stderr) +$ git push -o patch.message="Add new feature" -o patch.target="refs/heads/accepted" rad HEAD:refs/patches +✓ Patch [..] opened +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +Now, Alice accidentally merges the feature into the `master` branch instead of `accepted`. + +``` (stderr) +$ git checkout master +Switched to branch 'master' +``` +``` +$ git merge feature/1 +Updating [..] +Fast-forward + FEATURE.md | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` +``` (stderr) +$ git push rad master +✓ Canonical reference refs/heads/master updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + [..]..[..] master -> master +``` + +Because the patch was explicitly targeted for `accepted`, it should remain open. + +``` +$ rad patch list --merged +Nothing to show. +``` +``` +$ rad patch list --open +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ● [.. ] Add new feature alice (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` diff --git a/crates/radicle-cli/examples/rad-patch-revert-custom-branch.md b/crates/radicle-cli/examples/rad-patch-revert-custom-branch.md new file mode 100644 index 00000000..63cf5ce9 --- /dev/null +++ b/crates/radicle-cli/examples/rad-patch-revert-custom-branch.md @@ -0,0 +1,146 @@ +# Reverting a patch on a custom canonical branch + +First, we update the identity document to add a canonical reference rule for a new `accepted` branch. + +``` +$ rad id update --title "Add accepted branch" --payload xyz.radicle.crefs rules '{ "refs/heads/accepted": { "threshold": 1, "allow": "delegates" } }' -q +[..] +``` + +Now, let's create the `accepted` branch and push it: + +``` (stderr) +$ git checkout -b accepted +Switched to a new branch 'accepted' +``` +``` +$ git commit --allow-empty -m "Initialize accepted branch" +[accepted [..]] Initialize accepted branch +``` +``` (stderr) +$ git push rad accepted +✓ Canonical reference refs/heads/accepted updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new branch] accepted -> accepted +``` + +Next, we create a feature branch and open a patch targeting `accepted`. + +``` (stderr) +$ git checkout -b feature/1 +Switched to a new branch 'feature/1' +$ touch FEATURE.md +$ git add FEATURE.md +``` +``` +$ git commit -m "Add new feature" +[feature/1 [..]] Add new feature + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` +``` (stderr) +$ git push -o patch.message="Add new feature" -o patch.target="refs/heads/accepted" rad HEAD:refs/patches +✓ Patch [..] opened +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + * [new reference] HEAD -> refs/patches +``` + +Alice merges the feature into the `accepted` branch and pushes it. + +``` (stderr) +$ git checkout accepted +Switched to branch 'accepted' +``` +``` +$ git merge feature/1 +Updating [..] +Fast-forward + FEATURE.md | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + create mode 100644 FEATURE.md +``` +``` (stderr) +$ git push rad accepted +✓ Patch [..] merged +✓ Canonical reference refs/heads/accepted updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + [..]..[..] accepted -> accepted +``` + +We verify the patch is merged. + +``` +$ rad patch list --merged +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ✓ [.. ] Add new feature alice (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` + +## Revert isolation (Force-pushing the wrong branch) + +Now, Alice switches to `master`, resets it to drop a commit, and force pushes. This simulates a scenario where commits are +dropped from a branch *other* than the patch's destination. + +``` (stderr) +$ git checkout master +Switched to branch 'master' +``` +``` +$ git reset --hard HEAD~1 +HEAD is now at [..] Initial commit +``` +``` (stderr) +$ git push rad master --force +✓ Canonical reference refs/heads/master updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + + f2de534...08c788d master -> master (forced update) +``` + +Because the patch was merged into `accepted`, dropping commits on `master` should not revert the patch. It should remain +merged. + +``` +$ rad patch list --merged +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ✓ [.. ] Add new feature alice (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` + +## Reverting the patch (Force-pushing the correct branch) + +Now, Alice realizes she made a mistake and resets the `accepted` branch, dropping the merge commit, and force pushes. + +``` (stderr) +$ git checkout accepted +Switched to branch 'accepted' +``` +``` +$ git reset --hard HEAD~1 +HEAD is now at [..] Initialize accepted branch +``` +``` (stderr) +$ git push rad accepted --force +! Patch [..] reverted at revision [..] +✓ Canonical reference refs/heads/accepted updated to target commit [..] +To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + + 27acd3b...f9a3b89 accepted -> accepted (forced update) +``` + +The patch should now be open again. + +``` +$ rad patch list --merged +Nothing to show. +``` +``` +$ rad patch list --open +╭───────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Reviews Head + - Updated Labels │ +├───────────────────────────────────────────────────────────────────────────────────────┤ +│ ● [.. ] Add new feature alice (you) - [.. ] +0 -0 now │ +╰───────────────────────────────────────────────────────────────────────────────────────╯ +``` diff --git a/crates/radicle-cli/tests/commands/patch.rs b/crates/radicle-cli/tests/commands/patch.rs index 165a3a49..d908f461 100644 --- a/crates/radicle-cli/tests/commands/patch.rs +++ b/crates/radicle-cli/tests/commands/patch.rs @@ -436,3 +436,60 @@ fn rad_patch_merge_on_first_push() { fn rad_patch_magic_push() { Environment::alice(["rad-init", "rad-patch-magic-push"]); } + +#[test] +fn rad_patch_merge_into_canonical_ref_branch() { + Environment::alice(["rad-init", "rad-patch-merge-into-canonical-ref-branch"]); +} + +#[test] +fn rad_patch_merge_wrong_branch() { + Environment::alice(["rad-init", "rad-patch-merge-wrong-branch"]); +} + +#[test] +fn rad_patch_merge_unauthorized_branch() { + let mut environment = Environment::new(); + let alice = environment.node("alice"); + let bob = environment.node("bob"); + let acme = RepoId::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap(); + + environment.repository(&alice); + + test( + "examples/rad-init.md", + environment.work(&alice), + Some(&alice.home), + [], + ) + .unwrap(); + + let mut alice = alice.spawn(); + let mut bob = bob.spawn(); + + bob.handle.seed(acme, Scope::All).unwrap(); + alice.connect(&bob).converge([&bob]); + + formula( + &environment.tempdir(), + "examples/rad-patch-merge-unauthorized-branch.md", + ) + .unwrap() + .home( + "alice", + environment.work(&alice), + [("RAD_HOME", alice.home.path().display())], + ) + .home( + "bob", + environment.work(&bob), + [("RAD_HOME", bob.home.path().display())], + ) + .run() + .unwrap(); +} + +#[test] +fn rad_patch_revert_custom_branch() { + Environment::alice(["rad-init", "rad-patch-revert-custom-branch"]); +}