remote-helper: Introduce magic push ref 'refs/for/'

Introduces support for Gerrit-style magic push references via
`refs/for/<branch>`. Pushing to this ref automatically extracts the
target branch and opens a patch against it, bypassing the need for the
push option `patch.target`. Example:

```
$ git push rad HEAD:refs/for/accepted
```

Will open a patch with its `patch.target` set to `refs/heads/accepted`.
This commit is contained in:
Adrian Duke 2026-05-19 15:51:32 +01:00 committed by Fintan Halpenny
parent 06e201c927
commit eb2dded0ed
4 changed files with 260 additions and 13 deletions

View File

@ -76,6 +76,11 @@ COB type names and payload IDs remain unchanged for backwards compatibility.
Furthermore, strict merge and revert isolation is now enforced: patches are
only marked as merged or reverted if the commits are pushed to the target
branch of the patch explicitly.
- Additionally a magic push reference has been introduced to shortcut the usage
of the aforementioned push option `patch.target`. `refs/for/<branch>` can be
used to set the `patch.target` when used as a push target e.g.
`git push rad HEAD:refs/for/backport`. This will use the `refs/heads/backport`
canonical reference as its `patch.target` in place of using the push option.
- Teach `rad patch show` to show the full commit range for each revision.
Previously, it would only show the head of the range, but not the base.
It now shows `<base>..<head>`, where the shortened OID is used when not

View File

@ -0,0 +1,213 @@
# Magic Push Reference
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
```
We can then use the magic push reference `refs/for/<branch>` to open a patch targeting a specific branch without needing to use push options.
```
$ git checkout -b feature/1 -q
$ git commit -m "Add new feature" --allow-empty -q
```
Pushing to the magic reference:
``` (stderr)
$ git push rad HEAD:refs/for/accepted
✓ Patch [..] opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/for/accepted
```
We can see the patch is open:
```
$ rad patch show 01dad54
╭──────────────────────────────────────────────────────────╮
│ Title Initialize accepted branch │
│ Patch 01dad54873ada4efa61541e7d90702266d5ced89 │
│ Author alice (you) │
│ Head 46fd342edc149468ec08b0b25291083aa05d4449 │
│ Base [.. ] │
│ Branches feature/1 │
│ Commits ahead 1, behind 0 │
│ Status open │
│ │
│ Add new feature │
├──────────────────────────────────────────────────────────┤
│ 46fd342 Add new feature │
│ f9a3b89 Initialize accepted branch │
├──────────────────────────────────────────────────────────┤
│ ● Revision 01dad54 @ f2de534..46fd342 by alice (you) now │
╰──────────────────────────────────────────────────────────╯
$ rad patch list --open
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Reviews Head + - Updated Labels │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● [.. ] Initialize accepted branch alice (you) - [.. ] +0 -0 now │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
```
Now we merge the feature into the `accepted` branch:
``` (stderr)
$ git checkout accepted
Switched to branch 'accepted'
```
```
$ git merge feature/1
Updating [..]
Fast-forward
```
``` (stderr)
$ git push rad accepted
✓ Patch [..] merged
✓ Canonical reference refs/heads/accepted updated to target commit [..]
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
[..]..[..] accepted -> accepted
```
We can now verify that the patch has been successfully marked as merged:
```
$ rad patch show 01dad54
╭──────────────────────────────────────────────────────────╮
│ Title Initialize accepted branch │
│ Patch 01dad54873ada4efa61541e7d90702266d5ced89 │
│ Author alice (you) │
│ Head 46fd342edc149468ec08b0b25291083aa05d4449 │
│ Base [.. ] │
│ Branches accepted, feature/1 │
│ Commits up to date │
│ Status merged │
│ │
│ Add new feature │
├──────────────────────────────────────────────────────────┤
│ 46fd342 Add new feature │
│ f9a3b89 Initialize accepted branch │
├──────────────────────────────────────────────────────────┤
│ ● Revision 01dad54 @ f2de534..46fd342 by alice (you) now │
│ └─ ✓ merged by alice (you) │
╰──────────────────────────────────────────────────────────╯
$ rad patch list --merged
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Reviews Head + - Updated Labels │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ✓ [.. ] Initialize accepted branch alice (you) - [.. ] +0 -0 now │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
```
Alternative, attempting to provide conflicting targets fails:
``` (fail) (stderr)
$ git push -o patch.target=master rad HEAD:refs/for/accepted
error: conflicting merge targets: push option 'refs/heads/master' and magic ref 'refs/heads/accepted' specified
error: failed to push some refs to 'rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi'
```
However, if the push option and the magic ref match, it succeeds:
```
$ git checkout -b feature/2 -q
$ git commit -m "Add another feature" --allow-empty -q
```
``` (stderr)
$ git push -o patch.target=accepted rad HEAD:refs/for/accepted
✓ Patch [..] opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/for/accepted
```
```
$ rad patch show 965855f
╭──────────────────────────────────────────────────────────╮
│ Title Initialize accepted branch │
│ Patch 965855f69bc5a86f7e1787ed80cd065013b6504c │
│ Author alice (you) │
│ Head 7e6814e99fe7084a213faeb69ca1eacc26935e3c │
│ Base f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
│ Branches feature/2 │
│ Commits ahead 1, behind 0 │
│ Status open │
│ │
│ Add new feature │
│ │
│ │
│ Add another feature │
├──────────────────────────────────────────────────────────┤
│ 7e6814e Add another feature │
│ 46fd342 Add new feature │
│ f9a3b89 Initialize accepted branch │
├──────────────────────────────────────────────────────────┤
│ ● Revision 965855f @ f2de534..7e6814e by alice (you) now │
╰──────────────────────────────────────────────────────────╯
```
Finally, we can also use a fully qualified branch name in the magic reference:
```
$ git checkout -b feature/3 -q
$ git commit -m "Add a third feature" --allow-empty -q
```
``` (stderr)
$ git push rad HEAD:refs/for/refs/heads/accepted
✓ Patch [..] opened
To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new reference] HEAD -> refs/for/refs/heads/accepted
```
This will create a third feature branch and verify that pushing to `refs/for/refs/heads/accepted` successfully opens a patch.
```
$ rad patch show c846bb5 -v
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Title Initialize accepted branch │
│ Patch c846bb5f8298802e8de589174549d78c1d9aa00f │
│ Author alice (you) │
│ Head 99c1d5a04c17ba854f1f4d59985c980c0faf16e7 │
│ Base f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
│ Branches feature/3 │
│ Commits ahead 2, behind 0 │
│ Status open │
│ │
│ Add new feature │
│ │
│ │
│ Add another feature │
│ │
│ │
│ Add a third feature │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 99c1d5a Add a third feature │
│ 7e6814e Add another feature │
│ 46fd342 Add new feature │
│ f9a3b89 Initialize accepted branch │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision c846bb5f8298802e8de589174549d78c1d9aa00f with range f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354..99c1d5a04c17ba854f1f4d59985c980c0faf16e7 by alice (you) now │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

View File

@ -431,3 +431,8 @@ fn rad_patch_merge_on_first_push() {
.run()
.unwrap();
}
#[test]
fn rad_patch_magic_push() {
Environment::alice(["rad-init", "rad-patch-magic-push"]);
}

View File

@ -31,6 +31,8 @@ use crate::service::GitService;
use crate::service::NodeSession;
use crate::{Options, Verbosity, hint, warn};
const PATCHES_FOR_PREFIX: &str = "refs/for/";
#[derive(Debug, Error)]
pub(super) enum Error {
/// Public key doesn't match the remote namespace we're pushing to.
@ -114,6 +116,9 @@ pub(super) enum Error {
UnknownObjectType { oid: git::Oid },
#[error(transparent)]
FindObjects(#[from] git::canonical::error::FindObjectsError),
/// Conflicting merge targets.
#[error("conflicting merge targets: push option '{0}' and magic ref '{1}' specified")]
ConflictingTargets(cob::patch::TargetBranch, cob::patch::TargetBranch),
/// Default branch error.
#[error(transparent)]
DefaultBranch(#[from] radicle::identity::doc::DefaultBranchError),
@ -212,7 +217,9 @@ impl Command {
}
enum PushAction {
OpenPatch,
OpenPatch {
target: Option<cob::patch::TargetBranch>,
},
UpdatePatch {
dst: git::fmt::Qualified<'static>,
patch: patch::PatchId,
@ -225,7 +232,17 @@ enum PushAction {
impl PushAction {
fn new(dst: &git::fmt::RefString) -> Result<Self, error::PushAction> {
if dst == &*rad::PATCHES_REFNAME {
Ok(Self::OpenPatch)
Ok(Self::OpenPatch { target: None })
} else if let Some(stripped) = dst.as_str().strip_prefix(PATCHES_FOR_PREFIX) {
let target = cob::patch::TargetBranch::try_from(stripped).map_err(|_| {
error::PushAction::InvalidRef {
refname: dst.clone(),
}
})?;
Ok(Self::OpenPatch {
target: Some(target),
})
} else {
let dst = git::fmt::Qualified::from_refstr(dst)
.ok_or_else(|| error::PushAction::InvalidRef {
@ -326,17 +343,24 @@ pub(super) fn run(
let action = PushAction::new(dst)?;
match action {
PushAction::OpenPatch => patch_open(
src,
&remote,
&nid,
&working,
stored,
patches,
profile,
opts.clone(),
git,
),
PushAction::OpenPatch { target } => {
let mut push_opts = opts.clone();
if let Some(magic_target) = target {
if let cob::patch::MergeTarget::Branch(opt_target) = &opts.target
&& magic_target != *opt_target
{
return Err(Error::ConflictingTargets(
opt_target.clone(),
magic_target,
));
}
push_opts.target = cob::patch::MergeTarget::Branch(magic_target);
}
patch_open(
src, &remote, &nid, &working, stored, patches, profile, push_opts, git,
)
}
PushAction::UpdatePatch { dst, patch } => patch_update(
src,
&dst,