rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodb438d5e5cb8e13817aa6db3b6e2e1b2915f71c43
{
"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:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Updated",
"patch": {
"id": "63b4e1d9046a2af75401f74234d7fc18b447109d",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "Canonical Symbolic References",
"state": {
"status": "open",
"conflicts": []
},
"before": "c06b00e330d82c8b8221cc8f8776c883208d159f",
"after": "b438d5e5cb8e13817aa6db3b6e2e1b2915f71c43",
"commits": [
"b438d5e5cb8e13817aa6db3b6e2e1b2915f71c43",
"17e7ce6c9087ab410d6e4d03280c38429dbc811e",
"f6ac64903568397434b96a9dda16e43ab3440b41",
"cfa89ac3ea81e23e4cc3a726da86acadc4444a9e",
"c11aa45a37f8ce5eac0269202b7355203270aab8",
"111cdd5d5b9997670099450d2f1049917165a050",
"e45357ab9f850c532df208d27ac4d58db5d15c86",
"609d0b303e1c4ca3cf03fb18442a9cf93f137a66"
],
"target": "c06b00e330d82c8b8221cc8f8776c883208d159f",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "63b4e1d9046a2af75401f74234d7fc18b447109d",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Extend the payload `xyz.radicle.crefs` to additionally support creating symbolic references via the member \"symbolic\".",
"base": "86472fdccbf95d08d0184776ee1ca75d01caf2c8",
"oid": "0d8ccb51101df44aa643c14e421f0a8f5a1aca53",
"timestamp": 1758036727
},
{
"id": "a8f4547661b3e0c0ae87a7b2bf10bac784529d42",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "First working revision.",
"base": "86472fdccbf95d08d0184776ee1ca75d01caf2c8",
"oid": "4910c5f03beefe20d7a1dea9e0c986de867987b5",
"timestamp": 1758063904
},
{
"id": "4728a5335d735afa5c6a0427bd88a35f93c7b8fa",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "REVIEW\n\nSuper nice set of changes, and seems to fit so nicely into the canonical\nreference system!\n\nAppreciate the type safety too :)\n\nLeft some bits of feedback, but all-in-all I'm very happy with these changes.",
"base": "86472fdccbf95d08d0184776ee1ca75d01caf2c8",
"oid": "cc99c8cc509107757916307fdc5f29210ef70114",
"timestamp": 1758111930
},
{
"id": "61f41e3e3bf3469acd72b9b3f237bb29356f328f",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Work in Fintan's review.",
"base": "86472fdccbf95d08d0184776ee1ca75d01caf2c8",
"oid": "72584651f1818dfc2e9759d4f4afa60ad0ada357",
"timestamp": 1758117424
},
{
"id": "4f8b72024cc6e070f6e8324292b8205536f6f924",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Rebase",
"base": "7b00bf2e3ac5e83eab262182bf51a5ede656145c",
"oid": "3324ebbb01151e6f0db951155d342a1782c27f95",
"timestamp": 1758120326
},
{
"id": "e4e142e1b405905ca7219e1d2534619408104095",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Handle interactions between rules and symrefs, and cyclic symrefs.",
"base": "e70850cb36c5f08d2d8714017d5404dde34deefa",
"oid": "902d99f2131f8420f2a8a9c70f645f69e13cfbf1",
"timestamp": 1758459092
},
{
"id": "6a1155bceb7d3ca55b39311848111f8fdc1f1552",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "REVIEW\n\nSee commits for review suggestions.\n\nLooking really good and clean still!",
"base": "e70850cb36c5f08d2d8714017d5404dde34deefa",
"oid": "ce4864fc46c4463b6aeabb79b0712bcaf9987bdf",
"timestamp": 1758559598
},
{
"id": "52de3142d005a0d934cf6d84f3dab33f2b94dcc0",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Squash in Fintan's review, rename tests as mentioned on Zulip, improve docs on `Doc::canonical_refs`.",
"base": "e70850cb36c5f08d2d8714017d5404dde34deefa",
"oid": "0891ad955e8a68b689b56a1b5e1a9e1077147586",
"timestamp": 1758614851
},
{
"id": "d76a072ff1259a4a1e584e93d5e4eb47a1ff3d27",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Rebase",
"base": "ed8b086045ee5d7bd1327f579de7861a1cf49e3b",
"oid": "0b0f73d2b39088bbfeb30c496e870b27c915a81b",
"timestamp": 1758725112
},
{
"id": "17797c5586de8002dbd28d8c647ebc9268430ee0",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Rebase",
"base": "4787b53b1e85d8052744fc77e4160e4d90e46d0f",
"oid": "d02c4c127a1ff0a8a2d56c8dcad3d815b6cd9427",
"timestamp": 1759255430
},
{
"id": "9956ae65550991f8b8bed09349588e828e8099c1",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Rebase",
"base": "ee49e28766ce7c703b95e22d177cce046072f03d",
"oid": "79f3230dd0d29fbf79d931a9adea19855d5f7448",
"timestamp": 1759333073
},
{
"id": "785d6bf241783c872f468b25c030686ecc9f812b",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Push broken rebase. Will fix in future revision.",
"base": "c06b00e330d82c8b8221cc8f8776c883208d159f",
"oid": "b438d5e5cb8e13817aa6db3b6e2e1b2915f71c43",
"timestamp": 1771121329
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "09fedefa-c37e-4c65-b6b1-184a319d5617"
},
"info_url": "https://cci.rad.levitte.org//09fedefa-c37e-4c65-b6b1-184a319d5617.html"
}
Started at: 2026-02-15 03:25:57.688705+01:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ 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/09fedefa-c37e-4c65-b6b1-184a319d5617/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 137 issues · 32 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 63b4e1d9046a2af75401f74234d7fc18b447109d
✓ Switched to branch patch/63b4e1d at revision 785d6bf
✓ Branch patch/63b4e1d setup to track rad/patches/63b4e1d9046a2af75401f74234d7fc18b447109d
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout b438d5e5cb8e13817aa6db3b6e2e1b2915f71c43
HEAD is now at b438d5e5 radicle/crefs: Support Symbolic References
Exit code: 0
$ rad patch show 63b4e1d9046a2af75401f74234d7fc18b447109d -p
╭──────────────────────────────────────────────────────────────────────────────────╮
│ Title Canonical Symbolic References │
│ Patch 63b4e1d9046a2af75401f74234d7fc18b447109d │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head b438d5e5cb8e13817aa6db3b6e2e1b2915f71c43 │
│ Base c06b00e330d82c8b8221cc8f8776c883208d159f │
│ Branches patch/63b4e1d │
│ Commits ahead 8, behind 0 │
│ Status open │
│ │
│ Extend the payload `xyz.radicle.crefs` to additionally support creating symbolic │
│ references via the member "symbolic". │
├──────────────────────────────────────────────────────────────────────────────────┤
│ b438d5e radicle/crefs: Support Symbolic References │
│ 17e7ce6 radicle/crefs: Use `GetPayload` to load │
│ f6ac649 radicle: Introduce `trait GetPayload` │
│ cfa89ac radicle/crefs: Refactor `GetCanonicalRefs` │
│ c11aa45 radicle/crefs/protect: Module for protected refs │
│ 111cdd5 radicle/crefs/rules: Exact patterns for branches │
│ e45357a radicle/storage: Split `WriteRepository::set_head` │
│ 609d0b3 radicle: More convenience methods to get default branch │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 63b4e1d @ 0d8ccb5 by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision a8f4547 @ 4910c5f by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision 4728a53 @ cc99c8c by fintohaps z6Mkire…SQZ3voM 4 months ago │
│ ↑ Revision 61f41e3 @ 7258465 by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision 4f8b720 @ 3324ebb by fintohaps z6Mkire…SQZ3voM 4 months ago │
│ ↑ Revision e4e142e @ 902d99f by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision 6a1155b @ ce4864f by fintohaps z6Mkire…SQZ3voM 4 months ago │
│ ↑ Revision 52de314 @ 0891ad9 by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision d76a072 @ 0b0f73d by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision 17797c5 @ d02c4c1 by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision 9956ae6 @ 79f3230 by lorenz z6MkkPv…WX5sTEz 4 months ago │
│ ↑ Revision 785d6bf @ b438d5e by lorenz z6MkkPv…WX5sTEz 17 minutes ago │
╰──────────────────────────────────────────────────────────────────────────────────╯
commit b438d5e5cb8e13817aa6db3b6e2e1b2915f71c43
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Tue Sep 16 22:02:09 2025 +0200
radicle/crefs: Support Symbolic References
Canonical references can not be used to model symbolic references.
Relax this restriction by adding another member to the payload, named
"symbolic". Its key-value/name-target pairs then translate directly to
canonical symbolic references.
Re-use `Unprotected` just like rules do.
Care is taken to not allow circular references, and that there always
is a rule that could generate the target of a (chain of) symbolic
reference. This does not guarantee that no symref can ever dangle, but
at least it can be prevented that a symref always dangles.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b813a7bbc..a0ac1d33a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -172,6 +172,8 @@ With the introduction of `clap`, this helped with the introduction of a command
environment variable `RAD_PASSPHRASE` (lower priority than the
credential). The identifier of the credential is
"xyz.radicle.node.passphrase".
+- Symbolic references can now be handled by canonical references by coding them
+ in the payload `xyz.radicle.crefs` under the key `symbolic`.
## Fixed Bugs
diff --git a/crates/radicle-node/src/worker/fetch.rs b/crates/radicle-node/src/worker/fetch.rs
index b1d24a7fa..c2018965d 100644
--- a/crates/radicle-node/src/worker/fetch.rs
+++ b/crates/radicle-node/src/worker/fetch.rs
@@ -13,8 +13,7 @@ use radicle::prelude::RepoId;
use radicle::storage::git::Repository;
use radicle::storage::refs::RefsAt;
use radicle::storage::{
- ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, RepositoryError,
- WriteRepository as _,
+ ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, WriteRepository as _,
};
use radicle::{cob, git, node, Storage};
use radicle_fetch::git::refs::Applied;
@@ -341,8 +340,21 @@ fn set_canonical_refs(
repo: &Repository,
applied: &Applied,
) -> Result<Option<UpdatedCanonicalRefs>, error::Canonical> {
+ const LOG_MESSAGE: &str = "set-canonical from fetch (radicle)";
+
let identity = repo.identity()?;
- let rules = identity.doc().canonical_refs()?.rules().clone();
+ let crefs = identity.doc().canonical_refs()?;
+
+ for (name, target) in crefs.symbolic().iter() {
+ if let Err(e) = repo.set_symbolic_ref(name, target, LOG_MESSAGE) {
+ log::warn!(
+ target: "worker",
+ "Failed to set canonical symbolic reference {name}->{target}: {e}"
+ );
+ }
+ }
+
+ let rules = crefs.rules().clone();
let mut updated_refs = UpdatedCanonicalRefs::default();
let refnames = applied
@@ -384,12 +396,10 @@ fn set_canonical_refs(
refname, object, ..
}) => {
let oid = object.id();
- if let Err(e) = repo.backend.reference(
- refname.clone().as_str(),
- oid.into(),
- true,
- "set-canonical-reference from fetch (radicle)",
- ) {
+ if let Err(e) =
+ repo.backend
+ .reference(refname.clone().as_str(), oid.into(), true, LOG_MESSAGE)
+ {
log::warn!(
target: "worker",
"Failed to set canonical reference {refname}->{oid}: {e}"
diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs
index 6600f9ee5..a63253587 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -251,6 +251,8 @@ pub fn run(
opts: Options,
expected_refs: &[String],
) -> Result<(), Error> {
+ const LOG_MESSAGE: &str = "set-canonical from push (radicle)";
+
// Don't allow push if either of these conditions is true:
//
// 1. Our key is not in ssh-agent, which means we won't be able to sign the refs.
@@ -283,9 +285,6 @@ pub fn run(
}
}
let delegates = stored.delegates()?;
- let identity = stored.identity()?;
- let project = identity.project()?;
- let canonical_ref = git::refs::branch(project.default_branch());
let mut set_canonical_refs: Vec<(git::fmt::Qualified, git::canonical::Object)> =
Vec::with_capacity(specs.len());
@@ -399,6 +398,8 @@ pub fn run(
if !ok.is_empty() {
let _ = stored.sign_refs(&signer)?;
+ stored.set_canonical_symbolic_refs(LOG_MESSAGE)?;
+
for (refname, object) in &set_canonical_refs {
let oid = object.id();
let kind = object.object_type();
@@ -411,20 +412,11 @@ pub fn run(
)
};
- // N.b. special case for handling the canonical ref, since it
- // creates a symlink to HEAD
- if *refname == canonical_ref {
- stored.set_head_to_default_branch()?;
- }
-
match stored.backend.refname_to_id(refname.as_str()) {
Ok(new) if oid != new => {
- stored.backend.reference(
- refname.as_str(),
- oid.into(),
- true,
- "set-canonical-reference from git-push (radicle)",
- )?;
+ stored
+ .backend
+ .reference(refname.as_str(), oid.into(), true, LOG_MESSAGE)?;
print_update();
}
Err(e) if e.code() == git::raw::ErrorCode::NotFound => {
diff --git a/crates/radicle/src/git/canonical.rs b/crates/radicle/src/git/canonical.rs
index 9ab72d046..d5851df38 100644
--- a/crates/radicle/src/git/canonical.rs
+++ b/crates/radicle/src/git/canonical.rs
@@ -12,6 +12,7 @@ mod voting;
pub mod effects;
pub mod protect;
pub mod rules;
+pub mod symbolic;
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
diff --git a/crates/radicle/src/git/canonical/protect.rs b/crates/radicle/src/git/canonical/protect.rs
index eebe7aaca..962cb9559 100644
--- a/crates/radicle/src/git/canonical/protect.rs
+++ b/crates/radicle/src/git/canonical/protect.rs
@@ -42,6 +42,13 @@ impl<T: RefLike> Unprotected<T> {
pub fn into_inner(self) -> T {
self.0
}
+
+ /// Allows creation without any checking. Callers must ensure that
+ /// `reflike` is indeed unprotected!
+ #[inline]
+ const fn new_unchecked(reflike: T) -> Self {
+ Self(reflike)
+ }
}
impl<T: RefLike> AsRef<T> for Unprotected<T> {
@@ -68,7 +75,10 @@ impl<T: RefLike + std::fmt::Display> std::fmt::Display for Unprotected<T> {
/// For types that are commonly used in conjunction with [`Unprotected`]
/// have some `impl`s and companion infallible injections.
mod impls {
- use crate::git::fmt::{refspec::QualifiedPattern, RefString};
+ use crate::git::{
+ fmt::{refname, refspec::QualifiedPattern, Qualified, RefStr, RefString},
+ refs::branch,
+ };
use super::*;
@@ -76,6 +86,33 @@ mod impls {
/// means to be [`RefLike`].
impl RefLike for RefString {}
+ impl Unprotected<RefString> {
+ /// The reference name `HEAD`.
+ // We would like to have a `pub const HEAD`, but
+ // [`crate::git::RefStr::from_str`] is private.
+ #[inline]
+ pub fn head() -> Self {
+ // Calling [`Unprotected::new_unchecked`] here is legal,
+ // because we know statically that `HEAD` is not protected.
+ Unprotected::new_unchecked(refname!("HEAD"))
+ }
+ }
+
+ /// [`Qualified`] is a restriction on [`RefString`].
+ impl RefLike for Qualified<'_> {}
+
+ impl Unprotected<Qualified<'_>> {
+ /// Construct a qualified reference name for given branch, i.e.,
+ /// return `/refs/heads/<name>`
+ pub fn branch(name: &RefStr) -> Self {
+ Self::new(branch(name)).expect("branches are never protected")
+ }
+
+ pub fn to_ref_string(&self) -> Unprotected<RefString> {
+ Unprotected::new_unchecked(self.0.to_ref_string())
+ }
+ }
+
/// A [`QualifiedPattern`] is [`RefLike`] in the sense that it matches a
/// (possibly infinite) set of [`crate::git::Qualified`].
impl RefLike for QualifiedPattern<'_> {}
diff --git a/crates/radicle/src/git/canonical/rules.rs b/crates/radicle/src/git/canonical/rules.rs
index 3b1bfbf87..40c935398 100644
--- a/crates/radicle/src/git/canonical/rules.rs
+++ b/crates/radicle/src/git/canonical/rules.rs
@@ -389,6 +389,22 @@ impl From<Did> for Allowed {
}
}
+impl std::fmt::Display for Allowed {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Allowed::Delegates => f.write_str("\"allowed\""),
+ Allowed::Set(dids) => {
+ let dids = dids
+ .iter()
+ .map(|did| did.to_string())
+ .collect::<Vec<_>>()
+ .join("\", \"");
+ f.write_fmt(format_args!("[\"{dids}\"]"))
+ }
+ }
+ }
+}
+
/// A marker `enum` that is used in a [`ValidRule`].
///
/// It ensures that a rule that has been deserialized, resolving the `delegates`
diff --git a/crates/radicle/src/git/canonical/symbolic.rs b/crates/radicle/src/git/canonical/symbolic.rs
new file mode 100644
index 000000000..18c6058e7
--- /dev/null
+++ b/crates/radicle/src/git/canonical/symbolic.rs
@@ -0,0 +1,324 @@
+//! Symbolic references, which link neither to nor from protected references.
+//! The prototypical example is `HEAD → refs/heads/main`.
+
+use std::collections::BTreeMap;
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use crate::git::fmt::RefString;
+
+use super::protect::{self, Unprotected};
+
+use reachability::reachable;
+
+pub type RawName = RefString;
+
+/// Names of symbolic references are unprotected references.
+/// Requiring [`Unprotected`] makes symbolic references that link *from*
+/// protected references unrepresentable.
+pub(super) type Name = Unprotected<RefString>;
+
+impl std::cmp::Ord for Name {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.as_ref().cmp(other.as_ref())
+ }
+}
+
+impl std::cmp::PartialOrd for Name {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+pub type RawTarget = RefString;
+
+/// Targets for symbolic references are unprotected references.
+/// Requiring [`Unprotected`] makes symbolic references that link *to*
+/// protected references unrepresentable.
+pub(super) type Target = Unprotected<RefString>;
+
+/// Maintains a cycle-free set of symbolic references.
+/// Note that dangling references are not detected.
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(try_from = "BTreeMap<Name, Target>")]
+pub struct SymbolicRefs(BTreeMap<Name, Target>);
+
+// Read-only access.
+impl SymbolicRefs {
+ /// Returns an iterator over all contained symbolic references, as pairs of
+ /// their name [`RawName`] and [`RawTarget`].
+ pub fn iter(&self) -> impl Iterator<Item = (&RawName, &RawTarget)> {
+ self.0
+ .iter()
+ .map(|(name, target)| (name.as_ref(), target.as_ref()))
+ }
+
+ /// Returns an iterator over all contained symbolic references, as pairs of
+ /// their name [`RawName`] and resolved [`RawTarget`].
+ pub fn iter_resolved(&self) -> impl Iterator<Item = (&RawName, &RawTarget)> {
+ self.iter_resolved_unprotected()
+ .map(|(name, target)| (name.as_ref(), target.as_ref()))
+ }
+
+ pub(super) fn iter_resolved_unprotected(&self) -> impl Iterator<Item = (&Name, &Target)> {
+ self.0
+ .keys()
+ .filter_map(|name| self.resolve_unprotected(name).map(|target| (name, target)))
+ }
+
+ fn resolve_unprotected<'a, 'b>(&'a self, mut name: &'b Name) -> Option<&'a Target>
+ where
+ 'a: 'b,
+ {
+ while let Some(target) = self.0.get(name) {
+ match self.0.get(target) {
+ Some(next) => {
+ name = next;
+ }
+ None => return Some(target),
+ }
+ }
+
+ None
+ }
+
+ /// Returns `true` if the set of symbolic references is empty.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+// Utilities for handling of `HEAD`.
+impl SymbolicRefs {
+ /// Construct [`SymbolicRefs`] for the single symbolic reference `HEAD`
+ /// targeting `/refs/heads/<branch_name>`.
+ // This exists to encapsulate [`Unprotected`].
+ pub fn head(branch_name: &RefString) -> Self {
+ let mut result = Self::default();
+ result
+ .try_insert_unprotected(
+ Unprotected::head().to_owned(),
+ Unprotected::branch(branch_name).to_ref_string(),
+ )
+ .expect("not creating cycle");
+ result
+ }
+
+ /// Convenience method to get the target of the `HEAD` reference.
+ /// See also [`SymbolicRefs::head`].
+ pub fn resolve_head(&self) -> Option<&RawTarget> {
+ self.resolve_unprotected(&Unprotected::head())
+ .map(|target| target.as_ref())
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum InsertionError {
+ #[error("inserting symbolic reference '{name} → {target}' would create a cycle")]
+ Cyclic { name: RawName, target: RawTarget },
+
+ #[error(transparent)]
+ Protected(#[from] protect::Error),
+}
+
+// Mutability.
+impl SymbolicRefs {
+ /// Insert a symbolic reference.
+ /// Even though this method will never return [`InsertionError::Protected`]
+ /// we opt to return that (slightly more general) error, as it allows
+ /// construction of [`InsertionError::Cyclic`] by consuming `name` and
+ /// `target`, avoiding an early copy in [`Self::try_insert`].
+ pub(super) fn try_insert_unprotected(
+ &mut self,
+ name: Name,
+ target: Target,
+ ) -> Result<(), InsertionError> {
+ if reachable(&self.0, &target, &name) {
+ return Err(InsertionError::Cyclic {
+ name: name.into_inner(),
+ target: target.into_inner(),
+ });
+ }
+
+ self.0.insert(name, target);
+ Ok(())
+ }
+
+ /// Try to insert a symbolic reference.
+ /// Errors if `name` or `target` is protected (see [`protect`]) or would
+ /// cause infinite recursion (e.g. `A → B` and `B → A`).
+ pub fn try_insert(&mut self, name: RawName, target: RawTarget) -> Result<(), InsertionError> {
+ self.try_insert_unprotected(Unprotected::new(name)?, Unprotected::new(target)?)
+ }
+
+ /// Consume `other` by iteratively inserting into self.
+ pub fn combine(&mut self, other: SymbolicRefs) -> Result<(), InsertionError> {
+ for (name, target) in other.0 {
+ self.try_insert_unprotected(name, target)?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Debug, Error)]
+#[error("symbolic reference '{name}' is cyclic")]
+pub struct Cyclic {
+ name: RawName,
+}
+
+impl TryFrom<BTreeMap<Name, Target>> for SymbolicRefs {
+ type Error = Cyclic;
+
+ fn try_from(map: BTreeMap<Name, Target>) -> Result<Self, Self::Error> {
+ for (name, target) in map.iter() {
+ if reachable(&map, target, name) {
+ return Err(Cyclic {
+ name: name.to_owned().into_inner(),
+ });
+ }
+ }
+
+ Ok(Self(map))
+ }
+}
+
+mod reachability {
+ pub(super) trait Get<'a, 'b, K, V> {
+ fn get(&'a self, key: &'b K) -> Option<&'a V>;
+ }
+
+ impl<'a, 'b, K: Ord, V> Get<'a, 'b, K, V> for std::collections::BTreeMap<K, V> {
+ fn get(&'a self, key: &'b K) -> Option<&'a V> {
+ std::collections::BTreeMap::get(self, key)
+ }
+ }
+
+ impl<'a, 'b, K: Eq + std::hash::Hash, V> Get<'a, 'b, K, V> for std::collections::HashMap<K, V> {
+ fn get(&'a self, key: &'b K) -> Option<&'a V> {
+ std::collections::HashMap::get(self, key)
+ }
+ }
+
+ impl<'a, 'b, K: Eq + std::hash::Hash, V> Get<'a, 'b, K, V> for indexmap::IndexMap<K, V> {
+ fn get(&'a self, key: &'b K) -> Option<&'a V> {
+ indexmap::IndexMap::get(self, key)
+ }
+ }
+
+ /// A reachability check linking
+ /// from `K` to `V` using [`Get`], and
+ /// from `V` to `K` using [`Into`].
+ /// Note that the bound is trivial if `K = V`.
+ ///
+ /// This can be used to check whether inserting `key → val`
+ /// would create a cycle.
+ ///
+ /// # Returns
+ ///
+ /// Whether `key == val` (under [`Into::into`]) or
+ /// `key` is reachable from `val` (under [`Into::into`] and [`Get::get`]).
+ pub(super) fn reachable<'a, 'b, K: PartialEq, V: 'a>(
+ map: &'a impl Get<'a, 'b, K, V>,
+ val: &'b V,
+ key: &'b K,
+ ) -> bool
+ where
+ 'a: 'b,
+ &'b V: Into<&'b K>,
+ {
+ // Self-Reference
+ let src = val.into();
+ if *src == *key {
+ return true;
+ }
+
+ // Chase
+ let mut src = src;
+ while let Some(tmp) = map.get(src).map(|value| value.into()) {
+ if *tmp == *key {
+ return true;
+ }
+ src = tmp;
+ }
+
+ false
+ }
+}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod test {
+ use crate::assert_matches;
+ use crate::git::fmt::refname;
+
+ use super::*;
+
+ #[test]
+ fn infinite_single() {
+ let mut symbolic = SymbolicRefs::default();
+
+ assert_matches!(
+ symbolic.try_insert(refname!("a"), refname!("a")),
+ Err(InsertionError::Cyclic { .. })
+ );
+
+ assert!(symbolic.is_empty());
+ }
+
+ #[test]
+ fn infinite_multi() {
+ let mut symbolic = SymbolicRefs::default();
+
+ assert_matches!(symbolic.try_insert(refname!("a"), refname!("b")), Ok(()));
+
+ assert_matches!(symbolic.try_insert(refname!("b"), refname!("c")), Ok(()));
+
+ assert_matches!(
+ symbolic.try_insert(refname!("c"), refname!("a")),
+ Err(InsertionError::Cyclic { .. })
+ );
+ }
+
+ #[test]
+ fn deserialize_valid() {
+ assert_matches!(
+ serde_json::from_value::<SymbolicRefs>(serde_json::json!({
+ "refs/heads/a": "refs/heads/b",
+ })),
+ Ok(_)
+ );
+ }
+
+ #[test]
+ fn deserialize_infinite() {
+ assert_matches!(
+ serde_json::from_value::<SymbolicRefs>(serde_json::json!({
+ "refs/heads/a": "refs/heads/a",
+ })),
+ Err(_)
+ );
+
+ assert_matches!(
+ serde_json::from_value::<SymbolicRefs>(serde_json::json!({
+ "refs/heads/a": "refs/heads/b",
+ "refs/heads/b": "refs/heads/c",
+ "refs/heads/c": "refs/heads/a",
+ })),
+ Err(_)
+ );
+ }
+
+ /// Motivates why we cannot simply delegate to [`BTreeMap::extend`]
+ /// for combining [`SymbolicRefs`].
+ #[test]
+ fn infinite_extend() {
+ let mut a = SymbolicRefs::default();
+ assert_matches!(a.try_insert(refname!("a"), refname!("b")), Ok(()));
+
+ let mut b = SymbolicRefs::default();
+ assert_matches!(b.try_insert(refname!("b"), refname!("a")), Ok(()));
+
+ assert_matches!(a.combine(b), Err(InsertionError::Cyclic { .. }));
+ }
+}
diff --git a/crates/radicle/src/identity/crefs.rs b/crates/radicle/src/identity/crefs.rs
index 5da76c931..4d27832d8 100644
--- a/crates/radicle/src/identity/crefs.rs
+++ b/crates/radicle/src/identity/crefs.rs
@@ -1,9 +1,29 @@
use serde::{Deserialize, Serialize};
+use thiserror::Error;
-use crate::git::canonical::rules::{RawRules, Rules, ValidationError};
+use crate::git::canonical::rules::{self, RawRules, Rules};
+use crate::git::canonical::symbolic::{self, SymbolicRefs};
+use crate::git::fmt::Qualified;
use super::doc::{Delegates, Payload};
+#[derive(Debug, Error)]
+pub enum ValidationError {
+ #[error(transparent)]
+ Rules(#[from] rules::ValidationError),
+
+ #[error("the target of the symbolic reference '{name} → {target}' is not matched by any rule")]
+ Dangling {
+ name: symbolic::RawName,
+ target: symbolic::RawTarget,
+ },
+ #[error("the symbolic reference name '{name}' is also matched by rule(s) with pattern(s) {patterns:?}")]
+ Clash {
+ patterns: Vec<String>,
+ name: Qualified<'static>,
+ },
+}
+
/// Configuration for canonical references and their rules.
///
/// `RawCanonicalRefs` are verified into [`CanonicalRefs`].
@@ -11,12 +31,15 @@ use super::doc::{Delegates, Payload};
#[serde(rename_all = "camelCase")]
pub struct RawCanonicalRefs {
rules: RawRules,
+
+ #[serde(default)] // Default to empty for backwards compatibility.
+ symbolic: SymbolicRefs,
}
impl RawCanonicalRefs {
/// Construct a new [`RawCanonicalRefs`] from a set of [`RawRules`].
- pub fn new(rules: RawRules) -> Self {
- Self { rules }
+ pub fn new(rules: RawRules, symbolic: SymbolicRefs) -> Self {
+ Self { rules, symbolic }
}
/// Return the [`RawRules`].
@@ -24,6 +47,21 @@ impl RawCanonicalRefs {
&self.rules
}
+ /// Return the [`RawRules`] for mutation.
+ pub fn raw_rules_mut(&mut self) -> &mut RawRules {
+ &mut self.rules
+ }
+
+ /// Return the [`SymbolicRefs`].
+ pub fn symbolic(&self) -> &SymbolicRefs {
+ &self.symbolic
+ }
+
+ /// Return the [`SymbolicRefs`] for mutation.
+ pub fn symbolic_mut(&mut self) -> &mut SymbolicRefs {
+ &mut self.symbolic
+ }
+
/// Validate the [`RawCanonicalRefs`] into a set of [`CanonicalRefs`].
pub fn try_into_canonical_refs<R>(
self,
@@ -33,7 +71,7 @@ impl RawCanonicalRefs {
R: Fn() -> Delegates,
{
let rules = Rules::from_raw(self.rules, resolve)?;
- Ok(CanonicalRefs::new(rules))
+ CanonicalRefs::new(rules, self.symbolic)
}
}
@@ -41,22 +79,65 @@ impl RawCanonicalRefs {
///
/// [`CanonicalRefs`] can be converted into a [`Payload`] using its [`From`]
/// implementation.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CanonicalRefs {
rules: Rules,
+
+ #[serde(default, skip_serializing_if = "SymbolicRefs::is_empty")]
+ symbolic: SymbolicRefs,
}
impl CanonicalRefs {
- /// Construct a new [`CanonicalRefs`] from a set of [`Rules`].
- pub fn new(rules: Rules) -> Self {
- CanonicalRefs { rules }
+ /// Construct a new [`CanonicalRefs`] from a set of [`Rules`] and
+ /// [`SymbolicRefs`], validating that these may be evaluated to a well
+ /// formed set of references when interpreted together.
+ pub fn new(rules: Rules, symbolic: SymbolicRefs) -> Result<Self, ValidationError> {
+ for (name, target) in symbolic.iter_resolved() {
+ if Qualified::from_refstr(target)
+ .and_then(|qualified| rules.matches(&qualified).next())
+ .is_none()
+ {
+ return Err(ValidationError::Dangling {
+ name: name.to_owned(),
+ target: target.to_owned(),
+ });
+ }
+
+ let Some(name) = Qualified::from_refstr(name) else {
+ continue;
+ };
+
+ let patterns = rules
+ .matches(&name)
+ .map(|(pattern, _)| pattern.to_string())
+ .collect::<Vec<_>>();
+ if !patterns.is_empty() {
+ return Err(ValidationError::Clash {
+ patterns,
+ name: name.to_owned(),
+ });
+ }
+ }
+
+ Ok(CanonicalRefs { rules, symbolic })
}
/// Return the [`Rules`].
pub fn rules(&self) -> &Rules {
&self.rules
}
+
+ /// Return the [`SymbolicRefs`].
+ pub fn symbolic(&self) -> &SymbolicRefs {
+ &self.symbolic
+ }
+}
+
+impl Extend<(rules::RawPattern, rules::RawRule)> for RawCanonicalRefs {
+ fn extend<T: IntoIterator<Item = (rules::RawPattern, rules::RawRule)>>(&mut self, iter: T) {
+ self.rules.extend(iter)
+ }
}
#[derive(Debug, thiserror::Error)]
@@ -74,3 +155,120 @@ impl TryFrom<CanonicalRefs> for Payload {
Ok(Self::from(value))
}
}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod tests {
+ use serde_json::json;
+
+ use crate::assert_matches;
+
+ use super::{ValidationError::*, *};
+
+ fn from(value: serde_json::Value) -> Result<CanonicalRefs, super::ValidationError> {
+ let delegates: Delegates = crate::test::arbitrary::gen::<crate::prelude::Did>(1).into();
+ serde_json::from_value::<RawCanonicalRefs>(value)
+ .unwrap()
+ .try_into_canonical_refs(&mut || delegates.clone())
+ }
+
+ /// Backwards compatibility to before addition of symbolic references.
+ #[test]
+ fn omit_symbolic() {
+ assert_matches!(
+ from(json!({
+ "rules": {},
+ })),
+ Ok(_)
+ );
+ }
+
+ #[test]
+ fn invalid_dangling() {
+ assert_matches!(
+ from(json!({
+ "symbolic": {
+ "HEAD": "refs/heads/master"
+ },
+ "rules": {},
+ })),
+ Err(Dangling { .. })
+ );
+ }
+
+ #[test]
+ fn invalid_clash() {
+ assert_matches!(
+ from(json!({
+ "symbolic": {
+ "refs/heads/foo": "refs/heads/bar",
+ },
+ "rules": {
+ "refs/heads/foo": {
+ "allow": "delegates",
+ "threshold": 1,
+ },
+ "refs/heads/bar": {
+ "allow": "delegates",
+ "threshold": 1,
+ },
+ },
+ })),
+ Err(Clash { .. })
+ );
+ }
+
+ #[test]
+ fn invalid_clash_asterisk_name() {
+ assert_matches!(
+ from(json!({
+ "symbolic": {
+ "refs/heads/foo": "refs/heads/bar",
+ },
+ "rules": {
+ "refs/heads/*": {
+ "allow": "delegates",
+ "threshold": 1,
+ },
+ },
+ })),
+ Err(Clash { .. })
+ );
+ }
+
+ #[test]
+ fn valid_asterisk_target() {
+ assert_matches!(
+ from(json!({
+ "symbolic": {
+ "HEAD": "refs/heads/master",
+ },
+ "rules": {
+ "refs/heads/*": {
+ "allow": "delegates",
+ "threshold": 1,
+ },
+ },
+ })),
+ Ok(_)
+ );
+ }
+
+ #[test]
+ fn valid() {
+ assert_matches!(
+ from(json!({
+ "symbolic": {
+ "refs/heads/foo": "refs/heads/ruled/bar",
+ },
+ "rules": {
+ "refs/heads/ruled/*": {
+ "allow": "delegates",
+ "threshold": 1,
+ },
+ },
+ })),
+ Ok(_)
+ );
+ }
+}
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index f8a883805..ed5aedd66 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -20,7 +20,10 @@ use crate::crypto;
use crate::crypto::Signature;
use crate::git;
use crate::git::canonical::rules;
+use crate::git::canonical::symbolic;
+use crate::git::fmt::Qualified;
use crate::git::raw::ErrorExt as _;
+use crate::identity::crefs;
use crate::identity::{project::Project, Did};
use crate::node::device::Device;
use crate::storage;
@@ -75,7 +78,7 @@ impl DocError {
}
#[derive(Debug, Error)]
-pub enum DefaultBranchRuleError {
+pub enum DefaultBranchError {
#[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
Payload(#[from] PayloadError),
}
@@ -756,41 +759,113 @@ impl Doc {
Ok(proj)
}
- /// Gets the qualified reference name of the default branch,
- /// according to the project payload in this document.
- pub fn default_branch(&self) -> Result<git::fmt::Qualified, PayloadError> {
- Ok(git::refs::branch(self.project()?.default_branch()))
- }
-
- pub fn default_branch_rule(&self) -> Result<rules::Rules, DefaultBranchRuleError> {
- let pattern = git::fmt::refspec::QualifiedPattern::from(git::refs::branch(
- self.project()?.default_branch(),
- ));
- let rule = rules::Rule::new(
- rules::ResolvedDelegates::Delegates(self.delegates.clone()),
- self.threshold,
- );
- Ok(rules::Rules::from_raw(
- rules::RawRules::from_iter([(pattern, rule.into())]),
- &mut || self.delegates.clone(),
- )
- .expect("default rules are valid"))
- }
-
/// Construct the canonical references for this document.
- /// The implementation of [`crefs::RawCanonicalRefs`] is used to
- /// obtain the payload identified by [`PayloadId::canonical_refs`], if it
- /// exists.
- /// The resulting [`CanonicalRefs`] are constructed by extension with
- /// [`Self::default_branch_rule`].
+ ///
+ /// Starts by obtaining the payload identified by
+ /// [`PayloadId::canonical_refs`].
+ ///
+ /// If the payload exists, and it contains both a symbolic reference with
+ /// the name `HEAD` and a rule matching the corresponding target branch,
+ /// then this rule is verified to be backwards compatible, i.e. that the
+ /// value for `allowed` is [`rules::Allowed::Delegates`] and the threshold
+ /// matches [`Self::threshold`].
+ ///
+ /// If the payload is missing, canonical references are synthesized from
+ /// the payload identified by [`PayloadId::project`]:
+ /// - A rule exactly matching [`Project::default_branch`]
+ /// that is compatible with self.
+ /// - A symbolic reference with name `HEAD`
+ /// (see [`symbolic::SymbolicRefs::head`]) that targets the same branch.
+ ///
+ /// The resulting [`CanonicalRefs`] must pass validation, and there are
+ /// cases where the payload is valid as such, but invalid in combination
+ /// with the synthesized rule and symbolic reference. For example, if
+ /// there is a symbolic reference already, with the name of the default
+ /// branch, this will clash with the synthesized rule.
pub fn canonical_refs(&self) -> Result<CanonicalRefs, CanonicalRefsError> {
let raw_crefs = self.raw_canonical_refs()?.unwrap_or_default();
- let mut raw_rules = raw_crefs.raw_rules().clone();
- raw_rules.extend(rules::RawRules::from(self.default_branch_rule()?));
+ let resolve = &mut || self.delegates.clone();
+
+ // If there is a symbolic reference with name `HEAD` in the crefs
+ // payload, we do not need to access the project payload to obtain
+ // the name of the default branch from there.
+ // However, we must still ensure that the crefs payload, in particular
+ // the rule matching the target brach of the symbolic reference with
+ // name `HEAD`, is backwards compatible with the rest of the identity
+ // document.
+ // These conditions may only be relaxed by introducing a new version of
+ // the identity document.
+ if let Some(default_branch) = raw_crefs.symbolic().resolve_head() {
+ if let Some(default_branch) = Qualified::from_refstr(default_branch) {
+ if let Some((pattern, rule)) = raw_crefs.raw_rules().matches(&default_branch).next()
+ {
+ if *rule.allowed() != rules::Allowed::Delegates {
+ return Err(CanonicalRefsError::DefaultBranchRuleError(
+ DefaultBranchRuleError::Allowed {
+ pattern: pattern.to_string(),
+ actual: rule.allowed().to_string(),
+ },
+ ));
+ }
+ if *rule.threshold() != self.threshold() {
+ return Err(CanonicalRefsError::DefaultBranchRuleError(
+ DefaultBranchRuleError::Threshold {
+ pattern: pattern.to_string(),
+ actual: *rule.threshold(),
+ expected: self.threshold(),
+ },
+ ));
+ }
+ } else {
+ // There is a symbolic reference for `HEAD`, but not matching
+ // canonical reference rule. `HEAD` is dangling!
+ // `raw_crefs` is malformed and will not pass validation below.
+ }
+ }
+ return Ok(raw_crefs.try_into_canonical_refs(resolve)?);
+ }
+
+ // Since there is no symbolic reference with name `HEAD`, we fall back
+ // to the project payload for obtaining the default branch.
+ let project = self.project().map_err(CanonicalRefsError::DefaultBranch)?;
+
+ // Only now, once we know that we will be synthesizing, and have a
+ // project to do so, make `raw_crefs` mutable.
+ let mut raw_crefs = raw_crefs;
+
+ let default_branch = project.default_branch_ref();
+
+ if raw_crefs
+ .raw_rules()
+ .matches(default_branch.as_ref())
+ .next()
+ .is_none()
+ {
+ let rule = rules::Rule::new(rules::Allowed::Delegates, self.threshold());
+
+ raw_crefs.raw_rules_mut().insert(
+ git::fmt::refspec::QualifiedPattern::from(default_branch.to_owned()),
+ rule,
+ );
+ }
+
+ raw_crefs
+ .symbolic_mut()
+ .combine(symbolic::SymbolicRefs::head(project.default_branch()))?;
- let raw_crefs = RawCanonicalRefs::new(raw_rules);
- Ok(raw_crefs.try_into_canonical_refs(&mut || self.delegates.clone())?)
+ Ok(raw_crefs.try_into_canonical_refs(resolve)?)
+ }
+
+ /// Gets the qualified reference name of the default branch,
+ /// according to the cref or project payload in this document.
+ pub fn default_branch(&self) -> Result<git::fmt::Qualified<'_>, CanonicalRefsError> {
+ self.canonical_refs()?
+ .symbolic()
+ .resolve_head()
+ .cloned()
+ .and_then(Qualified::from_refstr)
+ .ok_or(CanonicalRefsError::MissingHead)
}
/// Return the associated [`Visibility`] of this document.
@@ -950,20 +1025,42 @@ impl Doc {
}
}
+#[derive(Debug, Error)]
+pub enum RawCanonicalRefsError {
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
+}
+
#[derive(Debug, Error)]
pub enum CanonicalRefsError {
#[error(transparent)]
Raw(#[from] RawCanonicalRefsError),
#[error(transparent)]
- CanonicalRefs(#[from] rules::ValidationError),
+ CanonicalRefs(#[from] crefs::ValidationError),
+ #[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
+ DefaultBranch(PayloadError),
+
+ #[error("no symbolic reference with name `HEAD` is defined")]
+ MissingHead,
+
#[error(transparent)]
- DefaultBranch(#[from] DefaultBranchRuleError),
+ DefaultBranchRuleError(#[from] DefaultBranchRuleError),
+
+ #[error("synthesizing canonical references from project payload failed: {0}")]
+ Synthesis(#[from] symbolic::InsertionError),
}
#[derive(Debug, Error)]
-pub enum RawCanonicalRefsError {
- #[error(transparent)]
- Json(#[from] serde_json::Error),
+pub enum DefaultBranchRuleError {
+ #[error("rule for pattern '{pattern}' which matches the target of symbolic reference 'HEAD' (possibly synthesized from payload 'xyz.radicle.project') must use 'allow' value of \"delegates\" but uses {actual}")]
+ Allowed { pattern: String, actual: String },
+
+ #[error("rule for pattern '{pattern}' which matches the target of symbolic reference 'HEAD' (possibly synthesized from payload 'xyz.radicle.project') must use a threshold of {expected} as required by the identity document but uses {actual}")]
+ Threshold {
+ pattern: String,
+ actual: usize,
+ expected: usize,
+ },
}
pub trait GetRawCanonicalRefs: GetPayload {
@@ -1230,4 +1327,32 @@ mod test {
serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] })
);
}
+
+ #[test]
+ fn test_default_branch_without_project() {
+ let value = serde_json::json!(
+ {
+ "payload": {
+ "xyz.radicle.crefs": {
+ "symbolic": {
+ "HEAD": "refs/heads/main",
+ },
+ "rules": {
+ "refs/heads/main": {
+ "allow": "delegates",
+ "threshold": 1,
+ }
+ }
+ }
+ },
+ "delegates": [
+ "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
+ ],
+ "threshold": 1
+ }
+ );
+
+ let doc = serde_json::from_value::<Doc>(value).unwrap();
+ assert_eq!(doc.default_branch().unwrap().as_str(), "refs/heads/main");
+ }
}
diff --git a/crates/radicle/src/identity/doc/update.rs b/crates/radicle/src/identity/doc/update.rs
index 06e0240ec..412b365e2 100644
--- a/crates/radicle/src/identity/doc/update.rs
+++ b/crates/radicle/src/identity/doc/update.rs
@@ -211,9 +211,13 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
})
}
};
- // Ensure that if we have canonical reference rules and a project, that no
- // rule exists for the default branch. This rule must be synthesized when
- // constructing the canonical reference rules.
+
+ // If we have both payloads `xyz.radicle.{project,crefs}` ensure that,
+ // in the `crefs` payload there is no …
+ // 1. … rule that matches the default branch from the `project` payload.
+ // (This rule must be synthesized!)
+ // 2. … symbolic reference with the name `HEAD`.
+ // (This reference must be synthesized!)
use super::GetRawCanonicalRefs as _;
match raw
.raw_canonical_refs()
@@ -228,11 +232,19 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
.map(|(pattern, _)| pattern.to_string())
.collect::<Vec<_>>();
if !matches.is_empty() {
- return Err(error::DocVerification::DisallowDefault { matches, default });
+ return Err(error::DocVerification::DisallowDefaultBranchRule { matches, default });
+ }
+
+ if let Some(symbolic) = crefs.symbolic().resolve_head() {
+ return Err(error::DocVerification::DisallowDefaultBranchSymbolic {
+ symbolic: symbolic.to_owned(),
+ default,
+ });
}
}
_ => { /* we validate below */ }
}
+
// Verify that the canonical references payload is valid
if let Err(e) = proposal.canonical_refs() {
return Err(error::DocVerification::PayloadError {
@@ -336,7 +348,7 @@ mod test {
assert!(
matches!(
super::verify(raw),
- Err(error::DocVerification::DisallowDefault { .. })
+ Err(error::DocVerification::DisallowDefaultBranchRule { .. })
),
"Verification should be rejected for including default branch rule"
)
diff --git a/crates/radicle/src/identity/doc/update/error.rs b/crates/radicle/src/identity/doc/update/error.rs
index 05dd37036..d175572b8 100644
--- a/crates/radicle/src/identity/doc/update/error.rs
+++ b/crates/radicle/src/identity/doc/update/error.rs
@@ -29,10 +29,15 @@ pub enum DocVerification {
#[error(transparent)]
Doc(#[from] DocError),
#[error("incompatible payloads: The rule(s) xyz.radicle.crefs.rules.{matches:?} matches the value of xyz.radicle.project.defaultBranch ('{default}'). Possible resolutions: Change the name of the default branch or remove the rule(s).")]
- DisallowDefault {
+ DisallowDefaultBranchRule {
matches: Vec<String>,
default: git::fmt::Qualified<'static>,
},
+ #[error("incompatible payloads: The symbolic reference xyz.radicle.crefs.symbolic.HEAD → '{symbolic}' conflicts with xyz.radicle.project.defaultBranch ('{default}'). Possible resolutions: Remove either of the two.")]
+ DisallowDefaultBranchSymbolic {
+ symbolic: RefString,
+ default: git::fmt::Qualified<'static>,
+ },
}
#[derive(Clone, Debug)]
diff --git a/crates/radicle/src/identity/project.rs b/crates/radicle/src/identity/project.rs
index 908d8b902..da9a9f318 100644
--- a/crates/radicle/src/identity/project.rs
+++ b/crates/radicle/src/identity/project.rs
@@ -8,6 +8,7 @@ use thiserror::Error;
use crate::crypto;
use crate::git::BranchName;
+use crate::git::{fmt::Qualified, refs::branch};
use crate::identity::doc;
use crate::identity::doc::Payload;
@@ -254,6 +255,11 @@ impl Project {
pub fn default_branch(&self) -> &BranchName {
&self.default_branch
}
+
+ #[inline]
+ pub fn default_branch_ref(&self) -> Qualified<'_> {
+ branch(&self.default_branch)
+ }
}
impl From<Project> for Payload {
diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs
index b8a5dd9c5..b17872b84 100644
--- a/crates/radicle/src/rad.rs
+++ b/crates/radicle/src/rad.rs
@@ -143,7 +143,7 @@ where
)?;
stored.set_remote_identity_root_to(pk, identity)?;
stored.set_identity_head_to(identity)?;
- stored.set_head_to_default_branch()?;
+ stored.set_canonical_symbolic_refs("set-canonical from init (radicle)")?;
stored.set_default_branch_to_canonical_head()?;
let signed = stored.sign_refs(signer)?;
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index cba8b3380..9f98b851a 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -122,8 +122,10 @@ pub enum RepositoryError {
Refs(#[from] refs::Error),
#[error("missing canonical reference rule for default branch")]
MissingBranchRule,
+ #[error("missing canonical symbolic reference for default branch (`HEAD`)")]
+ MissingBranchSymbolic,
#[error("could not get the default branch rule: {0}")]
- DefaultBranchRule(#[from] doc::DefaultBranchRuleError),
+ DefaultBranchRule(#[from] doc::DefaultBranchError),
#[error("failed to get canonical reference rules: {0}")]
CanonicalRefs(#[from] doc::CanonicalRefsError),
#[error(transparent)]
@@ -666,11 +668,26 @@ where
/// Allows read-write access to a repository.
pub trait WriteRepository: ReadRepository + SignRepository {
- /// Sets the symbolic reference `HEAD` to target the default branch.
- /// This only depends on the value for the default branch in the identity
- /// document, and does not require the canonical reference behind the
- /// default branch to be computed, or even exist.
- fn set_head_to_default_branch(&self) -> Result<(), RepositoryError>;
+ /// Sets the canonical symbolic references.
+ ///
+ /// This only depends on the cref payload in the identity document, and does
+ /// not require the targeted canonical references to be computed, or even
+ /// exist.
+ fn set_canonical_symbolic_refs(&self, message: &str) -> Result<(), RepositoryError> {
+ for (name, target) in self.identity_doc()?.canonical_refs()?.symbolic().iter() {
+ self.set_symbolic_ref(name, target, message)?;
+ }
+ Ok(())
+ }
+
+ /// Sets a symbolic reference, if it does not exist or its target is different
+ /// from the given one.
+ fn set_symbolic_ref(
+ &self,
+ name: &RefStr,
+ target: &RefStr,
+ message: &str,
+ ) -> Result<(), RepositoryError>;
/// Computes the head of the default branch based on the delegate set,
/// and sets it.
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 5996a2b68..51c80642a 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -29,7 +29,7 @@ use crate::storage::{
use crate::{git, git::Oid, node};
use crate::git::fmt::{
- refname, refspec, refspec::PatternStr, refspec::PatternString, Qualified, RefString,
+ refspec, refspec::PatternStr, refspec::PatternString, Qualified, RefStr, RefString,
};
use crate::git::RefError;
use crate::git::UserInfo;
@@ -812,13 +812,20 @@ impl ReadRepository for Repository {
fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
let doc = self.identity_doc()?;
- let refname = doc.default_branch()?.to_owned();
let crefs = doc.canonical_refs()?;
+ let Some(refname) = crefs
+ .symbolic()
+ .resolve_head()
+ .and_then(Qualified::from_refstr)
+ else {
+ return Err(RepositoryError::MissingBranchSymbolic);
+ };
+
Ok(crefs
.rules()
- .canonical(refname, self)
+ .canonical(refname.to_owned(), self)
.ok_or(RepositoryError::MissingBranchRule)?
.find_objects()?
.quorum()?)
@@ -899,31 +906,28 @@ impl ReadRepository for Repository {
}
impl WriteRepository for Repository {
- fn set_head_to_default_branch(&self) -> Result<(), RepositoryError> {
- let head_ref = refname!("HEAD");
- let branch_ref = self.default_branch()?;
-
- match self.raw().find_reference(head_ref.as_str()) {
+ fn set_symbolic_ref(
+ &self,
+ name: &RefStr,
+ target: &RefStr,
+ message: &str,
+ ) -> Result<(), RepositoryError> {
+ match self.raw().find_reference(name.as_str()) {
Ok(mut head_ref) => {
if head_ref
.symbolic_target()
- .is_some_and(|t| t != branch_ref.as_str())
+ .is_some_and(|t| t != target.as_str())
{
- head_ref.symbolic_set_target(branch_ref.as_str(), "set-head (radicle)")?;
+ head_ref.symbolic_set_target(target.as_str(), message)?;
}
- Ok(())
}
Err(err) if err.is_not_found() => {
- self.raw().reference_symbolic(
- head_ref.as_str(),
- branch_ref.as_str(),
- true,
- "set-head (radicle)",
- )?;
- Ok(())
+ self.raw()
+ .reference_symbolic(name.as_str(), target.as_str(), true, message)?;
}
- Err(err) => Err(err.into()),
+ Err(err) => return Err(err.into()),
}
+ Ok(())
}
fn set_default_branch_to_canonical_head(&self) -> Result<SetHead, RepositoryError> {
diff --git a/crates/radicle/src/test.rs b/crates/radicle/src/test.rs
index 62d31b502..8081f6c92 100644
--- a/crates/radicle/src/test.rs
+++ b/crates/radicle/src/test.rs
@@ -59,7 +59,7 @@ pub fn fetch<W: WriteRepository>(
drop(opts);
repo.set_identity_head()?;
- repo.set_head_to_default_branch()?;
+ repo.set_canonical_symbolic_refs("set-canonical test (radicle)")?;
repo.set_default_branch_to_canonical_head()?;
let validations = repo.validate()?;
diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs
index b6b10c214..fbb160631 100644
--- a/crates/radicle/src/test/storage.rs
+++ b/crates/radicle/src/test/storage.rs
@@ -329,7 +329,12 @@ impl WriteRepository for MockRepository {
todo!()
}
- fn set_head_to_default_branch(&self) -> Result<(), RepositoryError> {
+ fn set_symbolic_ref(
+ &self,
+ _name: &fmt::RefStr,
+ _target: &fmt::RefStr,
+ _message: &str,
+ ) -> Result<(), RepositoryError> {
todo!()
}
commit 17e7ce6c9087ab410d6e4d03280c38429dbc811e
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Sep 18 22:06:53 2025 +0200
radicle/crefs: Use `GetPayload` to load
Using `GetPayload`, `GetRawCanonicalRefs` can be implemented
generically.
diff --git a/crates/radicle/src/identity/crefs.rs b/crates/radicle/src/identity/crefs.rs
index 88951301f..5da76c931 100644
--- a/crates/radicle/src/identity/crefs.rs
+++ b/crates/radicle/src/identity/crefs.rs
@@ -4,19 +4,6 @@ use crate::git::canonical::rules::{RawRules, Rules, ValidationError};
use super::doc::{Delegates, Payload};
-/// Implemented by any data type or store that can return [`CanonicalRefs`] and
-/// [`RawCanonicalRefs`].
-pub trait GetRawCanonicalRefs {
- type Error: std::error::Error + Send + Sync + 'static;
-
- /// Retrieve the [`RawCanonicalRefs`], returning `Some` if they are
- /// present, and `None` if they are absent.
- ///
- /// [`Self::Error`] is used to return any domain-specific error by the
- /// implementing type.
- fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error>;
-}
-
/// Configuration for canonical references and their rules.
///
/// `RawCanonicalRefs` are verified into [`CanonicalRefs`].
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index f9ae5db56..f8a883805 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -29,7 +29,7 @@ use crate::storage::{ReadRepository, RepositoryError};
pub use crypto::PublicKey;
pub use radicle_core::repo::*;
-use super::crefs::{self, RawCanonicalRefs};
+use super::crefs::RawCanonicalRefs;
use super::CanonicalRefs;
/// Path to the identity document in the identity branch.
@@ -256,6 +256,10 @@ impl Payload {
) -> Option<&mut serde_json::value::Map<String, serde_json::Value>> {
self.value.as_object_mut()
}
+
+ pub fn into_inner(self) -> serde_json::Value {
+ self.value
+ }
}
impl From<serde_json::Value> for Payload {
@@ -780,7 +784,6 @@ impl Doc {
/// The resulting [`CanonicalRefs`] are constructed by extension with
/// [`Self::default_branch_rule`].
pub fn canonical_refs(&self) -> Result<CanonicalRefs, CanonicalRefsError> {
- use crefs::GetRawCanonicalRefs;
let raw_crefs = self.raw_canonical_refs()?.unwrap_or_default();
let mut raw_rules = raw_crefs.raw_rules().clone();
@@ -950,41 +953,35 @@ impl Doc {
#[derive(Debug, Error)]
pub enum CanonicalRefsError {
#[error(transparent)]
- Json(#[from] serde_json::Error),
+ Raw(#[from] RawCanonicalRefsError),
#[error(transparent)]
CanonicalRefs(#[from] rules::ValidationError),
#[error(transparent)]
DefaultBranch(#[from] DefaultBranchRuleError),
}
-impl crefs::GetRawCanonicalRefs for Doc {
- type Error = CanonicalRefsError;
-
- fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
- let value = self.payload.get(&PayloadId::canonical_refs());
- let crefs = value
- .map(|value| {
- serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
- })
- .transpose()?;
- Ok(crefs)
- }
+#[derive(Debug, Error)]
+pub enum RawCanonicalRefsError {
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
}
-impl crefs::GetRawCanonicalRefs for RawDoc {
- type Error = CanonicalRefsError;
+pub trait GetRawCanonicalRefs: GetPayload {
+ /// Retrieve the [`RawCanonicalRefs`] by deserializing from the payload
+ /// (if present).
+ fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, RawCanonicalRefsError> {
+ let Some(value) = self.get_payload(&PayloadId::canonical_refs()) else {
+ return Ok(None);
+ };
- fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
- let value = self.payload.get(&PayloadId::canonical_refs());
- let crefs = value
- .map(|value| {
- serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
- })
- .transpose()?;
- Ok(crefs)
+ Ok(Some(serde_json::from_value(value.to_owned().into_inner())?))
}
}
+impl GetRawCanonicalRefs for Doc {}
+
+impl GetRawCanonicalRefs for RawDoc {}
+
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
diff --git a/crates/radicle/src/identity/doc/update.rs b/crates/radicle/src/identity/doc/update.rs
index d1dbf6cbf..06e0240ec 100644
--- a/crates/radicle/src/identity/doc/update.rs
+++ b/crates/radicle/src/identity/doc/update.rs
@@ -214,7 +214,7 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
// Ensure that if we have canonical reference rules and a project, that no
// rule exists for the default branch. This rule must be synthesized when
// constructing the canonical reference rules.
- use super::crefs::GetRawCanonicalRefs as _;
+ use super::GetRawCanonicalRefs as _;
match raw
.raw_canonical_refs()
.map(|rcrefs| rcrefs.and_then(|c| project.map(|p| (c, p))))
commit f6ac64903568397434b96a9dda16e43ab3440b41
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Sep 18 15:38:33 2025 +0200
radicle: Introduce `trait GetPayload`
This new trait can be implemented by any struct that allows loading of
payloads. Currently, this only applies to the identity document.
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 1b5ac4ad5..f9ae5db56 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -272,6 +272,23 @@ impl Deref for Payload {
}
}
+/// Trait for all types that may carry payloads.
+pub trait GetPayload {
+ fn get_payload(&self, id: &PayloadId) -> Option<&Payload>;
+}
+
+impl GetPayload for Doc {
+ fn get_payload(&self, id: &PayloadId) -> Option<&Payload> {
+ self.payload.get(id)
+ }
+}
+
+impl GetPayload for RawDoc {
+ fn get_payload(&self, id: &PayloadId) -> Option<&Payload> {
+ self.payload.get(id)
+ }
+}
+
/// A verified identity document at a specific commit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocAt {
commit cfa89ac3ea81e23e4cc3a726da86acadc4444a9e
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Thu Sep 18 15:38:33 2025 +0200
radicle/crefs: Refactor `GetCanonicalRefs`
There is only one type that can really construct canonical references,
which is the the identity document. Remove the corresponding method
from the trait and rename it to `GetRawCanonicalRefs` accordingly.
diff --git a/crates/radicle-node/src/worker/fetch.rs b/crates/radicle-node/src/worker/fetch.rs
index 95567d08a..b1d24a7fa 100644
--- a/crates/radicle-node/src/worker/fetch.rs
+++ b/crates/radicle-node/src/worker/fetch.rs
@@ -1,5 +1,3 @@
-use radicle::identity::doc::CanonicalRefsError;
-use radicle::identity::CanonicalRefs;
use radicle::storage::git::TempRepository;
pub(crate) use radicle_protocol::worker::fetch::error;
@@ -10,7 +8,6 @@ use localtime::LocalTime;
use radicle::cob::TypedId;
use radicle::crypto::PublicKey;
-use radicle::identity::crefs::GetCanonicalRefs as _;
use radicle::prelude::NodeId;
use radicle::prelude::RepoId;
use radicle::storage::git::Repository;
@@ -345,16 +342,7 @@ fn set_canonical_refs(
applied: &Applied,
) -> Result<Option<UpdatedCanonicalRefs>, error::Canonical> {
let identity = repo.identity()?;
- let rules = identity
- .canonical_refs_or_default(|| {
- identity
- .doc()
- .default_branch_rule()
- .map(CanonicalRefs::new)
- .map_err(CanonicalRefsError::from)
- })?
- .rules()
- .clone();
+ let rules = identity.doc().canonical_refs()?.rules().clone();
let mut updated_refs = UpdatedCanonicalRefs::default();
let refnames = applied
diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs
index 1343773ee..6600f9ee5 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -9,8 +9,6 @@ use std::process::ExitStatus;
use std::str::FromStr;
use std::{assert_eq, io};
-use radicle::identity::crefs::GetCanonicalRefs as _;
-use radicle::identity::doc::CanonicalRefsError;
use radicle::node::device::Device;
use thiserror::Error;
@@ -20,7 +18,7 @@ use radicle::cob::patch;
use radicle::cob::patch::cache::Patches as _;
use radicle::crypto;
use radicle::explorer::ExplorerResource;
-use radicle::identity::{CanonicalRefs, Did};
+use radicle::identity::Did;
use radicle::node;
use radicle::node::{Handle, NodeId};
use radicle::storage;
@@ -346,11 +344,7 @@ pub fn run(
),
PushAction::PushRef { dst } => {
let identity = stored.identity()?;
- let crefs = identity.canonical_refs_or_default(|| {
- Ok::<_, CanonicalRefsError>(CanonicalRefs::new(
- identity.doc().default_branch_rule()?,
- ))
- })?;
+ let crefs = identity.doc().canonical_refs()?;
let rules = crefs.rules();
let me = Did::from(nid);
diff --git a/crates/radicle/src/identity/crefs.rs b/crates/radicle/src/identity/crefs.rs
index e3a39c2c7..88951301f 100644
--- a/crates/radicle/src/identity/crefs.rs
+++ b/crates/radicle/src/identity/crefs.rs
@@ -6,35 +6,15 @@ use super::doc::{Delegates, Payload};
/// Implemented by any data type or store that can return [`CanonicalRefs`] and
/// [`RawCanonicalRefs`].
-pub trait GetCanonicalRefs {
+pub trait GetRawCanonicalRefs {
type Error: std::error::Error + Send + Sync + 'static;
- /// Retrieve the [`CanonicalRefs`], returning `Some` if they are not
- /// present, and `None` if they are missing.
- ///
- /// [`Self::Error`] is used to return any domain-specific error by the
- /// implementing type.
- fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error>;
-
- /// Retrieve the [`RawCanonicalRefs`], returning `Some` if they are not
- /// present, and `None` if they are missing.
+ /// Retrieve the [`RawCanonicalRefs`], returning `Some` if they are
+ /// present, and `None` if they are absent.
///
/// [`Self::Error`] is used to return any domain-specific error by the
/// implementing type.
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error>;
-
- /// Retrieve the [`CanonicalRefs`], and in the case of `None`, then use the
- /// `default` function to return a default set of [`CanonicalRefs`].
- fn canonical_refs_or_default<D, E>(&self, default: D) -> Result<CanonicalRefs, E>
- where
- D: Fn() -> Result<CanonicalRefs, E>,
- E: From<Self::Error>,
- {
- match self.canonical_refs()? {
- Some(crefs) => Ok(crefs),
- None => Ok(default()?),
- }
- }
}
/// Configuration for canonical references and their rules.
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 2d534f03b..1b5ac4ad5 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -756,6 +756,23 @@ impl Doc {
.expect("default rules are valid"))
}
+ /// Construct the canonical references for this document.
+ /// The implementation of [`crefs::RawCanonicalRefs`] is used to
+ /// obtain the payload identified by [`PayloadId::canonical_refs`], if it
+ /// exists.
+ /// The resulting [`CanonicalRefs`] are constructed by extension with
+ /// [`Self::default_branch_rule`].
+ pub fn canonical_refs(&self) -> Result<CanonicalRefs, CanonicalRefsError> {
+ use crefs::GetRawCanonicalRefs;
+ let raw_crefs = self.raw_canonical_refs()?.unwrap_or_default();
+
+ let mut raw_rules = raw_crefs.raw_rules().clone();
+ raw_rules.extend(rules::RawRules::from(self.default_branch_rule()?));
+
+ let raw_crefs = RawCanonicalRefs::new(raw_rules);
+ Ok(raw_crefs.try_into_canonical_refs(&mut || self.delegates.clone())?)
+ }
+
/// Return the associated [`Visibility`] of this document.
pub fn visibility(&self) -> &Visibility {
&self.visibility
@@ -923,26 +940,9 @@ pub enum CanonicalRefsError {
DefaultBranch(#[from] DefaultBranchRuleError),
}
-impl crefs::GetCanonicalRefs for Doc {
+impl crefs::GetRawCanonicalRefs for Doc {
type Error = CanonicalRefsError;
- fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
- let Some(raw_crefs) = self.raw_canonical_refs()? else {
- return Ok(None);
- };
-
- let mut raw_rules = raw_crefs.raw_rules().clone();
-
- let default_branch_rule = self.default_branch_rule()?;
- raw_rules.extend(rules::RawRules::from(default_branch_rule));
-
- let raw_crefs = RawCanonicalRefs::new(raw_rules);
-
- Ok(Some(
- raw_crefs.try_into_canonical_refs(&mut || self.delegates.clone())?,
- ))
- }
-
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
let value = self.payload.get(&PayloadId::canonical_refs());
let crefs = value
@@ -954,13 +954,9 @@ impl crefs::GetCanonicalRefs for Doc {
}
}
-impl crefs::GetCanonicalRefs for RawDoc {
+impl crefs::GetRawCanonicalRefs for RawDoc {
type Error = CanonicalRefsError;
- fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
- Ok(None)
- }
-
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
let value = self.payload.get(&PayloadId::canonical_refs());
let crefs = value
diff --git a/crates/radicle/src/identity/doc/update.rs b/crates/radicle/src/identity/doc/update.rs
index 0c4b4865a..d1dbf6cbf 100644
--- a/crates/radicle/src/identity/doc/update.rs
+++ b/crates/radicle/src/identity/doc/update.rs
@@ -6,7 +6,6 @@ use serde_json as json;
use crate::{
git,
- identity::crefs::GetCanonicalRefs as _,
prelude::Did,
storage::{refs, ReadRepository, RepositoryError},
};
@@ -215,6 +214,7 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
// Ensure that if we have canonical reference rules and a project, that no
// rule exists for the default branch. This rule must be synthesized when
// constructing the canonical reference rules.
+ use super::crefs::GetRawCanonicalRefs as _;
match raw
.raw_canonical_refs()
.map(|rcrefs| rcrefs.and_then(|c| project.map(|p| (c, p))))
@@ -281,10 +281,7 @@ mod test {
use crate::{
git,
- identity::{
- crefs::GetCanonicalRefs,
- doc::{update::error, PayloadId},
- },
+ identity::doc::{update::error, PayloadId},
prelude::RawDoc,
test::arbitrary,
};
@@ -366,7 +363,7 @@ mod test {
)
.unwrap();
let verified = super::verify(raw).unwrap();
- let crefs = verified.canonical_refs().unwrap().unwrap();
+ let crefs = verified.canonical_refs().unwrap();
assert!(
crefs.rules().matches(&branch).next().is_some(),
"Default branch rule is missing!"
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 8a5e4331a..5996a2b68 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -15,9 +15,8 @@ use crypto::Verified;
use crate::git::canonical::Quorum;
use crate::git::raw::ErrorExt as _;
-use crate::identity::crefs::GetCanonicalRefs as _;
use crate::identity::doc::DocError;
-use crate::identity::{CanonicalRefs, Doc, DocAt, RepoId};
+use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
use crate::node::device::Device;
use crate::node::SyncedAt;
@@ -815,11 +814,7 @@ impl ReadRepository for Repository {
let doc = self.identity_doc()?;
let refname = doc.default_branch()?.to_owned();
- let crefs = doc.canonical_refs_or_default(|| {
- doc.default_branch_rule()
- .map_err(RepositoryError::from)
- .map(CanonicalRefs::new)
- })?;
+ let crefs = doc.canonical_refs()?;
Ok(crefs
.rules()
commit c11aa45a37f8ce5eac0269202b7355203270aab8
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Tue Sep 16 22:02:09 2025 +0200
radicle/crefs/protect: Module for protected refs
Separate module for the concern of protecting `refs/rad`.
Note that this also fixes a bug, as previously all refs
*starting with* `refs/rad`, such as `refs/radieschen` were protected.
diff --git a/crates/radicle-node/src/worker/fetch.rs b/crates/radicle-node/src/worker/fetch.rs
index 180f6fd5b..95567d08a 100644
--- a/crates/radicle-node/src/worker/fetch.rs
+++ b/crates/radicle-node/src/worker/fetch.rs
@@ -347,8 +347,11 @@ fn set_canonical_refs(
let identity = repo.identity()?;
let rules = identity
.canonical_refs_or_default(|| {
- let rule = identity.doc().default_branch_rule()?;
- Ok::<_, CanonicalRefsError>(CanonicalRefs::from_iter([rule]))
+ identity
+ .doc()
+ .default_branch_rule()
+ .map(CanonicalRefs::new)
+ .map_err(CanonicalRefsError::from)
})?
.rules()
.clone();
diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs
index 42efdb5db..1343773ee 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -347,8 +347,9 @@ pub fn run(
PushAction::PushRef { dst } => {
let identity = stored.identity()?;
let crefs = identity.canonical_refs_or_default(|| {
- let rule = identity.doc().default_branch_rule()?;
- Ok::<_, CanonicalRefsError>(CanonicalRefs::from_iter([rule]))
+ Ok::<_, CanonicalRefsError>(CanonicalRefs::new(
+ identity.doc().default_branch_rule()?,
+ ))
})?;
let rules = crefs.rules();
let me = Did::from(nid);
diff --git a/crates/radicle/src/git/canonical.rs b/crates/radicle/src/git/canonical.rs
index 56702310b..9ab72d046 100644
--- a/crates/radicle/src/git/canonical.rs
+++ b/crates/radicle/src/git/canonical.rs
@@ -10,6 +10,7 @@ use quorum::{CommitQuorum, CommitQuorumFailure, TagQuorum, TagQuorumFailure};
mod voting;
pub mod effects;
+pub mod protect;
pub mod rules;
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
diff --git a/crates/radicle/src/git/canonical/protect.rs b/crates/radicle/src/git/canonical/protect.rs
new file mode 100644
index 000000000..eebe7aaca
--- /dev/null
+++ b/crates/radicle/src/git/canonical/protect.rs
@@ -0,0 +1,109 @@
+//! Some reference names are protected and cannot be used with canonical
+//! references. This module contains checks for these cases.
+//!
+//! Protected references are:
+//! 1. `refs/rad`
+//! 2. Any reference matching `refs/rad/*`, e.g. `refs/rad/id`, `refs/rad/foo/bar`.
+
+const REFS_RAD: &str = "refs/rad";
+
+/// Reference-like types, which we encounter when working with canonical references.
+pub(crate) trait RefLike: AsRef<str> + Ord + std::fmt::Display + serde::Serialize {}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("reference-like string '{REFS_RAD}' is protected")]
+ RefsRad,
+ #[error("reference-like string '{reflike}' is protected because it starts with '{REFS_RAD}/'")]
+ RefsRadChild { reflike: String },
+}
+
+/// A witnesses that the inner reference-like value is not protected.
+#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+#[repr(transparent)]
+#[serde(transparent)]
+pub(super) struct Unprotected<T: RefLike>(T);
+
+impl<T: RefLike> Unprotected<T> {
+ pub fn new(reflike: T) -> Result<Self, Error> {
+ match reflike
+ .as_ref()
+ .strip_prefix(REFS_RAD)
+ .map(|rest| rest.get(..1))
+ {
+ Some(None) => Err(Error::RefsRad),
+ Some(Some("/")) => Err(Error::RefsRadChild {
+ reflike: reflike.to_string(),
+ }),
+ Some(_) | None => Ok(Self(reflike)),
+ }
+ }
+
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+}
+
+impl<T: RefLike> AsRef<T> for Unprotected<T> {
+ fn as_ref(&self) -> &T {
+ &self.0
+ }
+}
+
+impl<'de, T: RefLike + serde::Deserialize<'de>> serde::Deserialize<'de> for Unprotected<T> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Self::new(T::deserialize(deserializer)?).map_err(serde::de::Error::custom)
+ }
+}
+
+impl<T: RefLike + std::fmt::Display> std::fmt::Display for Unprotected<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// For types that are commonly used in conjunction with [`Unprotected`]
+/// have some `impl`s and companion infallible injections.
+mod impls {
+ use crate::git::fmt::{refspec::QualifiedPattern, RefString};
+
+ use super::*;
+
+ /// [`RefString`] models reference names, thus the prototype of what it
+ /// means to be [`RefLike`].
+ impl RefLike for RefString {}
+
+ /// A [`QualifiedPattern`] is [`RefLike`] in the sense that it matches a
+ /// (possibly infinite) set of [`crate::git::Qualified`].
+ impl RefLike for QualifiedPattern<'_> {}
+}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used)]
+mod tests {
+ use crate::assert_matches;
+ use crate::git::fmt::refname;
+
+ use super::{Error::*, Unprotected};
+
+ #[test]
+ fn refs_rad() {
+ assert_matches!(Unprotected::new(refname!("refs/rad")), Err(RefsRad))
+ }
+
+ #[test]
+ fn refs_rad_id() {
+ assert_matches!(
+ Unprotected::new(refname!("refs/rad/id")),
+ Err(RefsRadChild { .. })
+ )
+ }
+
+ #[test]
+ fn refs_radieschen() {
+ assert_matches!(Unprotected::new(refname!("refs/radieschen")), Ok(_))
+ }
+}
diff --git a/crates/radicle/src/git/canonical/rules.rs b/crates/radicle/src/git/canonical/rules.rs
index 7b1220777..3b1bfbf87 100644
--- a/crates/radicle/src/git/canonical/rules.rs
+++ b/crates/radicle/src/git/canonical/rules.rs
@@ -9,10 +9,8 @@
//! the first matched rule, and this can be used to calculate the
//! [`Canonical::quorum`].
-use core::fmt;
use std::cmp::Ordering;
use std::collections::BTreeMap;
-use std::sync::LazyLock;
use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
@@ -24,12 +22,11 @@ use crate::git::canonical;
use crate::git::canonical::Canonical;
use crate::git::fmt::refspec::QualifiedPattern;
use crate::git::fmt::Qualified;
-use crate::git::fmt::{refname, RefString};
use crate::identity::{doc, Did};
-const ASTERISK: char = '*';
+use super::protect::{self, Unprotected};
-static REFS_RAD: LazyLock<RefString> = LazyLock::new(|| refname!("refs/rad"));
+const ASTERISK: char = '*';
/// Private trait to ensure that not any `Rule` can be deserialized.
/// Implementations are provided for `Allowed` and `usize` so that `RawRule`s
@@ -39,94 +36,39 @@ trait Sealed {}
impl Sealed for Allowed {}
impl Sealed for usize {}
-/// A `Pattern` is a `QualifiedPattern` reference, however, it disallows any
-/// references under the `refs/rad` hierarchy.
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(into = "QualifiedPattern", try_from = "QualifiedPattern")]
-pub struct Pattern(QualifiedPattern<'static>);
-
-impl fmt::Display for Pattern {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(self.0.as_str())
- }
-}
-
-impl From<Pattern> for QualifiedPattern<'static> {
- fn from(Pattern(pattern): Pattern) -> Self {
- pattern
- }
-}
-
-impl<'a> TryFrom<QualifiedPattern<'a>> for Pattern {
- type Error = PatternError;
-
- fn try_from(pattern: QualifiedPattern<'a>) -> Result<Self, Self::Error> {
- if pattern.starts_with(REFS_RAD.as_str()) {
- Err(PatternError::ProtectedRef {
- prefix: (*REFS_RAD).clone(),
- pattern: pattern.to_owned(),
- })
- } else {
- Ok(Self(pattern.to_owned()))
+/// A pattern for a rule is an unprotected qualified reference pattern.
+/// Requiring [`Unprotected`] makes rules that would create protected references
+/// unrepresentable.
+pub(super) type Pattern = Unprotected<QualifiedPattern<'static>>;
+
+pub type RawPattern = QualifiedPattern<'static>;
+
+/// Check if the `pattern` matches the `refname`.
+fn matches(pattern: &RawPattern, refname: &Qualified) -> bool {
+ // N.b. Git's refspecs do not quite match with glob-star semantics. A
+ // single `*` in a refspec is expected to match all references under
+ // that namespace, even if they are further down the hierarchy.
+ // Thus, the following rules are applied:
+ //
+ // - a trailing `*` changes to `**/*`
+ // - a `*` in between path components changes to `**`
+ let spec = match pattern.as_str().split_once(ASTERISK) {
+ None => pattern.to_string(),
+ // Expand `refs/tags/*` to `refs/tags/**/*`
+ Some((prefix, "")) => {
+ let mut spec = prefix.to_string();
+ spec.push_str("**/*");
+ spec
}
- }
-}
-
-impl<'a> TryFrom<Qualified<'a>> for Pattern {
- type Error = PatternError;
-
- fn try_from(name: Qualified<'a>) -> Result<Self, Self::Error> {
- Self::try_from(QualifiedPattern::from(name))
- }
-}
-
-impl Pattern {
- /// Construct a [`Pattern`] that matches a branch exactly.
- ///
- /// The resulting [`Pattern`] will match `refs/heads/<name>`.
- pub fn refs_heads_exact(name: &git::fmt::RefStr) -> Self {
- Self(QualifiedPattern::from(git::refs::branch(name)))
- }
-
- /// Check if the `refname` matches the rule's `refspec`.
- pub fn matches(&self, refname: &Qualified) -> bool {
- // N.b. Git's refspecs do not quite match with glob-star semantics. A
- // single `*` in a refspec is expected to match all references under
- // that namespace, even if they are further down the hierarchy.
- // Thus, the following rules are applied:
- //
- // - a trailing `*` changes to `**/*`
- // - a `*` in between path components changes to `**`
- let spec = match self.0.as_str().split_once(ASTERISK) {
- None => self.0.to_string(),
- // Expand `refs/tags/*` to `refs/tags/**/*`
- Some((prefix, "")) => {
- let mut spec = prefix.to_string();
- spec.push_str("**/*");
- spec
- }
- // Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
- Some((prefix, suffix)) => {
- let mut spec = prefix.to_string();
- spec.push_str("**");
- spec.push_str(suffix);
- spec
- }
- };
- fast_glob::glob_match(&spec, refname.as_str())
- }
-}
-
-impl AsRef<QualifiedPattern<'static>> for Pattern {
- fn as_ref(&self) -> &QualifiedPattern<'static> {
- &self.0
- }
-}
-
-impl PartialOrd for Pattern {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
+ // Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
+ Some((prefix, suffix)) => {
+ let mut spec = prefix.to_string();
+ spec.push_str("**");
+ spec.push_str(suffix);
+ spec
+ }
+ };
+ fast_glob::glob_match(&spec, refname.as_str())
}
/// Patterns are ordered by their specificity.
@@ -248,8 +190,8 @@ impl Ord for Pattern {
}
let mut result = ComponentOrdering::default();
- let mut lhs = self.0.components();
- let mut rhs = other.0.components();
+ let mut lhs = self.as_ref().components();
+ let mut rhs = other.as_ref().components();
loop {
match (lhs.next(), rhs.next()) {
(None, Some(_)) => return Ordering::Greater, // (1.)
@@ -263,6 +205,12 @@ impl Ord for Pattern {
}
}
+impl PartialOrd for Pattern {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
/// A [`Rule`] that can be serialized and deserialized safely.
///
/// Should be converted to a [`ValidRule`] via [`Rule::validate`].
@@ -309,27 +257,27 @@ pub struct RawRules {
/// Note that this can be a fully-qualified pattern, e.g. `refs/heads/qa`,
/// as well as a wild-card pattern, e.g. `refs/tags/*`.
#[serde(flatten)]
- pub rules: BTreeMap<Pattern, RawRule>,
+ pub rules: BTreeMap<RawPattern, RawRule>,
}
impl RawRules {
- /// Returns an iterator over the [`Pattern`] and [`RawRule`] in the set of
- /// rules.
- pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &RawRule)> {
+ /// Returns an iterator over the [`RawPattern`] and [`RawRule`]
+ /// in the set of rules.
+ pub fn iter(&self) -> impl Iterator<Item = (&RawPattern, &RawRule)> {
self.rules.iter()
}
/// Add a new [`RawRule`] to the set of rules.
///
/// Returns the replaced rule, if it existed.
- pub fn insert(&mut self, pattern: Pattern, rule: RawRule) -> Option<RawRule> {
+ pub fn insert(&mut self, pattern: RawPattern, rule: RawRule) -> Option<RawRule> {
self.rules.insert(pattern, rule)
}
/// Remove the rule that matches the `pattern` parameter.
///
/// Returns the rule if it existed.
- pub fn remove(&mut self, pattern: &Pattern) -> Option<RawRule> {
+ pub fn remove(&mut self, pattern: &RawPattern) -> Option<RawRule> {
self.rules.remove(pattern)
}
@@ -338,7 +286,7 @@ impl RawRules {
let refname = refname.as_str();
self.rules
.iter()
- .any(|(pattern, _)| pattern.0.as_str() == refname)
+ .any(|(pattern, _)| pattern.as_str() == refname)
}
/// Check if the `refname` matches any existing rules, including glob
@@ -346,35 +294,35 @@ impl RawRules {
pub fn matches<'a, 'b>(
&self,
refname: &Qualified<'b>,
- ) -> impl Iterator<Item = (&Pattern, &RawRule)> + use<'a, '_, 'b> {
+ ) -> impl Iterator<Item = (&RawPattern, &RawRule)> + use<'a, '_, 'b> {
let refname = refname.clone();
self.rules
.iter()
- .filter(move |(pattern, _)| pattern.matches(&refname))
+ .filter(move |(pattern, _)| matches(pattern, &refname))
}
}
-impl Extend<(Pattern, RawRule)> for RawRules {
- fn extend<T: IntoIterator<Item = (Pattern, RawRule)>>(&mut self, iter: T) {
+impl Extend<(RawPattern, RawRule)> for RawRules {
+ fn extend<T: IntoIterator<Item = (RawPattern, RawRule)>>(&mut self, iter: T) {
self.rules.extend(iter)
}
}
-impl From<BTreeMap<Pattern, RawRule>> for RawRules {
- fn from(rules: BTreeMap<Pattern, RawRule>) -> Self {
+impl From<BTreeMap<RawPattern, RawRule>> for RawRules {
+ fn from(rules: BTreeMap<RawPattern, RawRule>) -> Self {
RawRules { rules }
}
}
-impl FromIterator<(Pattern, RawRule)> for RawRules {
- fn from_iter<T: IntoIterator<Item = (Pattern, RawRule)>>(iter: T) -> Self {
+impl FromIterator<(RawPattern, RawRule)> for RawRules {
+ fn from_iter<T: IntoIterator<Item = (RawPattern, RawRule)>>(iter: T) -> Self {
iter.into_iter().collect::<BTreeMap<_, _>>().into()
}
}
impl IntoIterator for RawRules {
- type Item = (Pattern, RawRule);
- type IntoIter = std::collections::btree_map::IntoIter<Pattern, RawRule>;
+ type Item = (RawPattern, RawRule);
+ type IntoIter = std::collections::btree_map::IntoIter<RawPattern, RawRule>;
fn into_iter(self) -> Self::IntoIter {
self.rules.into_iter()
@@ -390,40 +338,6 @@ impl IntoIterator for RawRules {
/// document and validated.
pub type ValidRule = Rule<ResolvedDelegates, doc::Threshold>;
-impl ValidRule {
- /// Initialize a `ValidRule` for the default branch, given by `name`. The
- /// rule will contain the single `did` as the allowed DID, and use a
- /// threshold of `1`.
- ///
- /// Note that the serialization of the rule will use the `delegates` token
- /// for the rule. E.g.
- /// ```json
- /// {
- /// "pattern": "refs/heads/main",
- /// "allow": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"],
- /// "threshold": 1
- /// }
- /// ```
- ///
- /// # Errors
- ///
- /// If the `name` reference begins with `refs/rad`.
- pub fn default_branch(
- did: Did,
- name: &git::fmt::RefStr,
- ) -> Result<(Pattern, Self), PatternError> {
- let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
- let rule = Self {
- allow: ResolvedDelegates::Delegates(doc::Delegates::from(did)),
- // N.B. this needs to be the minimum since we only have one
- // delegate.
- threshold: doc::Threshold::MIN,
- extensions: json::Map::new(),
- };
- Ok((pattern, rule))
- }
-}
-
impl From<ValidRule> for RawRule {
fn from(rule: ValidRule) -> Self {
let Rule {
@@ -546,7 +460,7 @@ impl MatchedRule<'_> {
/// cannot be duplicated.
///
/// To construct the set of rules, use [`Rules::from_raw`], which validates a
-/// set of [`RawRule`]s, and their [`Pattern`] references, into a set of
+/// set of [`RawRule`]s, and their [`RawPattern`] references, into a set of
/// [`ValidRule`]s.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Rules {
@@ -562,48 +476,18 @@ impl FromIterator<(Pattern, ValidRule)> for Rules {
}
}
-impl<'a> IntoIterator for &'a Rules {
- type Item = (&'a Pattern, &'a ValidRule);
- type IntoIter = std::collections::btree_map::Iter<'a, Pattern, ValidRule>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.rules.iter()
- }
-}
-
-impl IntoIterator for Rules {
- type Item = (Pattern, ValidRule);
- type IntoIter = std::collections::btree_map::IntoIter<Pattern, ValidRule>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.rules.into_iter()
- }
-}
-
-impl Extend<(Pattern, ValidRule)> for Rules {
- fn extend<T: IntoIterator<Item = (Pattern, ValidRule)>>(&mut self, iter: T) {
- self.rules.extend(iter)
- }
-}
-
impl From<Rules> for RawRules {
fn from(Rules { rules }: Rules) -> Self {
Self {
rules: rules
.into_iter()
- .map(|(pattern, rule)| (pattern, rule.into()))
+ .map(|(pattern, rule)| (pattern.into_inner(), rule.into()))
.collect(),
}
}
}
impl Rules {
- /// Returns an iterator over the [`Pattern`] and [`ValidRule`] in the set of
- /// rules.
- pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &ValidRule)> {
- self.rules.iter()
- }
-
/// Returns `true` is the set of rules is empty.
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
@@ -611,7 +495,7 @@ impl Rules {
/// Construct a set of `Rules` given a set of `RawRule`s.
pub fn from_raw<R>(
- rules: impl IntoIterator<Item = (Pattern, RawRule)>,
+ rules: impl IntoIterator<Item = (RawPattern, RawRule)>,
resolve: &mut R,
) -> Result<Self, ValidationError>
where
@@ -619,7 +503,10 @@ impl Rules {
{
let valid = rules
.into_iter()
- .map(|(pattern, rule)| rule.validate(resolve).map(|rule| (pattern, rule)))
+ .map(|(pattern, rule)| {
+ let pattern = Unprotected::new(pattern)?;
+ rule.validate(resolve).map(|rule| (pattern, rule))
+ })
.collect::<Result<_, _>>()?;
Ok(Self { rules: valid })
}
@@ -628,11 +515,12 @@ impl Rules {
pub fn matches<'a>(
&self,
refname: &Qualified<'a>,
- ) -> impl Iterator<Item = (&Pattern, &ValidRule)> + use<'a, '_> {
+ ) -> impl Iterator<Item = (&RawPattern, &ValidRule)> + use<'a, '_> {
let refname_cloned = refname.clone();
self.rules
.iter()
- .filter(move |(pattern, _)| pattern.matches(&refname_cloned))
+ .filter(move |(pattern, _)| matches(pattern.as_ref(), &refname_cloned))
+ .map(|(pattern, rule)| (pattern.as_ref(), rule))
}
/// Match given refname, take the most specific rule, and prepare evaluation
@@ -657,7 +545,7 @@ impl Rules {
}
/// A `Rule` defines how a reference or set of references can be made canonical,
-/// i.e. have a top-level `refs/*` entry – see [`Pattern`].
+/// i.e. have a top-level `refs/*` entry – see [`RawPattern`].
///
/// The [`Rule::allowed`] type is generic to allow for [`Allowed`] to be used
/// for serialization and deserialization, however, the use of
@@ -720,23 +608,14 @@ impl<D, T> Rule<D, T> {
}
}
-#[derive(Debug, Error)]
-pub enum PatternError {
- #[error("cannot create rule for '{pattern}' since references under '{prefix}' are protected")]
- ProtectedRef {
- prefix: RefString,
- pattern: QualifiedPattern<'static>,
- },
-}
-
#[derive(Debug, Error)]
pub enum ValidationError {
#[error(transparent)]
Threshold(#[from] doc::ThresholdError),
#[error(transparent)]
Delegates(#[from] doc::DelegatesError),
- #[error("cannot create rule for reserved `rad` references '{pattern}'")]
- RadRef { pattern: QualifiedPattern<'static> },
+ #[error(transparent)]
+ Protected(#[from] protect::Error),
}
#[derive(Debug, Error)]
@@ -780,8 +659,12 @@ mod tests {
s.parse().unwrap()
}
+ fn raw_pattern(qp: QualifiedPattern<'static>) -> RawPattern {
+ qp
+ }
+
fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
- Pattern::try_from(qp).unwrap()
+ Pattern::new(qp).unwrap()
}
fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
@@ -857,7 +740,7 @@ mod tests {
"#;
let expected = [
(
- pattern(qualified_pattern!("refs/heads/main")),
+ raw_pattern(qualified_pattern!("refs/heads/main")),
Rule::new(
Allowed::Set(nonempty![
did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
@@ -867,7 +750,7 @@ mod tests {
),
),
(
- pattern(qualified_pattern!("refs/tags/releases/*")),
+ raw_pattern(qualified_pattern!("refs/tags/releases/*")),
Rule::new(
Allowed::Set(nonempty![
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
@@ -878,7 +761,7 @@ mod tests {
),
),
(
- pattern(qualified_pattern!("refs/heads/development")),
+ raw_pattern(qualified_pattern!("refs/heads/development")),
Rule::new(
Allowed::Set(nonempty![did(
"did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
@@ -887,13 +770,13 @@ mod tests {
),
),
(
- pattern(qualified_pattern!("refs/heads/release/*")),
+ raw_pattern(qualified_pattern!("refs/heads/release/*")),
Rule::new(Allowed::Delegates, 1),
),
]
.into_iter()
.collect::<RawRules>();
- let rules = serde_json::from_str::<BTreeMap<Pattern, RawRule>>(examples)
+ let rules = serde_json::from_str::<BTreeMap<RawPattern, RawRule>>(examples)
.unwrap()
.into();
assert_eq!(expected, rules)
@@ -1091,9 +974,12 @@ mod tests {
// Duplicate rules are overwritten
let rules = vec![
- (pattern.clone(), Rule::new(Allowed::Delegates, 1)),
(
- pattern.clone(),
+ pattern.clone().into_inner(),
+ Rule::new(Allowed::Delegates, 1),
+ ),
+ (
+ pattern.clone().into_inner(),
Rule::new(doc.delegates().as_ref().clone().into(), 1),
),
];
@@ -1186,18 +1072,18 @@ mod tests {
let rules = Rules::from_raw(
[
(
- pattern(qualified_pattern!("refs/tags/*")),
+ raw_pattern(qualified_pattern!("refs/tags/*")),
Rule::new(Allowed::Delegates, 1),
),
(
- pattern(qualified_pattern!("refs/tags/release/*")),
+ raw_pattern(qualified_pattern!("refs/tags/release/*")),
Rule::new(Allowed::Delegates, 1),
),
// Ensure that none of the other rules apply by ensuring we need
// both delegates to get the quorum of the
// `refs/tags/release/candidates/v1.0` reference
(
- pattern(qualified_pattern!("refs/tags/release/candidates/*")),
+ raw_pattern(qualified_pattern!("refs/tags/release/candidates/*")),
Rule::new(Allowed::Delegates, 2),
),
],
@@ -1235,8 +1121,8 @@ mod tests {
#[test]
fn test_special_branches() {
- assert!(Pattern::try_from((*IDENTITY_BRANCH).clone()).is_err());
- assert!(Pattern::try_from((*SIGREFS_BRANCH).clone()).is_err());
- assert!(Pattern::try_from((*IDENTITY_ROOT).clone()).is_err());
+ assert!(Pattern::new((*IDENTITY_BRANCH).clone().into()).is_err());
+ assert!(Pattern::new((*SIGREFS_BRANCH).clone().into()).is_err());
+ assert!(Pattern::new((*IDENTITY_ROOT).clone().into()).is_err());
}
}
diff --git a/crates/radicle/src/identity/crefs.rs b/crates/radicle/src/identity/crefs.rs
index 939d5b16c..e3a39c2c7 100644
--- a/crates/radicle/src/identity/crefs.rs
+++ b/crates/radicle/src/identity/crefs.rs
@@ -1,9 +1,6 @@
use serde::{Deserialize, Serialize};
-use crate::git::canonical::{
- rules::{self, RawRules, Rules, ValidationError},
- ValidRule,
-};
+use crate::git::canonical::rules::{RawRules, Rules, ValidationError};
use super::doc::{Delegates, Payload};
@@ -95,18 +92,6 @@ impl CanonicalRefs {
}
}
-impl FromIterator<(rules::Pattern, ValidRule)> for CanonicalRefs {
- fn from_iter<T: IntoIterator<Item = (rules::Pattern, ValidRule)>>(iter: T) -> Self {
- Self::new(Rules::from_iter(iter))
- }
-}
-
-impl Extend<(rules::Pattern, ValidRule)> for CanonicalRefs {
- fn extend<T: IntoIterator<Item = (rules::Pattern, ValidRule)>>(&mut self, iter: T) {
- self.rules.extend(iter)
- }
-}
-
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CanonicalRefsPayloadError {
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 84f354d29..2d534f03b 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -741,15 +741,19 @@ impl Doc {
Ok(git::refs::branch(self.project()?.default_branch()))
}
- pub fn default_branch_rule(
- &self,
- ) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
- let pattern = rules::Pattern::refs_heads_exact(self.project()?.default_branch());
+ pub fn default_branch_rule(&self) -> Result<rules::Rules, DefaultBranchRuleError> {
+ let pattern = git::fmt::refspec::QualifiedPattern::from(git::refs::branch(
+ self.project()?.default_branch(),
+ ));
let rule = rules::Rule::new(
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
self.threshold,
);
- Ok((pattern, rule))
+ Ok(rules::Rules::from_raw(
+ rules::RawRules::from_iter([(pattern, rule.into())]),
+ &mut || self.delegates.clone(),
+ )
+ .expect("default rules are valid"))
}
/// Return the associated [`Visibility`] of this document.
@@ -923,21 +927,20 @@ impl crefs::GetCanonicalRefs for Doc {
type Error = CanonicalRefsError;
fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
- self.raw_canonical_refs().and_then(|raw| {
- raw.map(|raw| {
- raw.try_into_canonical_refs(&mut || self.delegates.clone())
- .map_err(CanonicalRefsError::from)
- .and_then(|mut crefs| {
- self.default_branch_rule()
- .map_err(CanonicalRefsError::from)
- .map(|rule| {
- crefs.extend([rule]);
- crefs
- })
- })
- })
- .transpose()
- })
+ let Some(raw_crefs) = self.raw_canonical_refs()? else {
+ return Ok(None);
+ };
+
+ let mut raw_rules = raw_crefs.raw_rules().clone();
+
+ let default_branch_rule = self.default_branch_rule()?;
+ raw_rules.extend(rules::RawRules::from(default_branch_rule));
+
+ let raw_crefs = RawCanonicalRefs::new(raw_rules);
+
+ Ok(Some(
+ raw_crefs.try_into_canonical_refs(&mut || self.delegates.clone())?,
+ ))
}
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 9d678200e..8a5e4331a 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -814,12 +814,13 @@ impl ReadRepository for Repository {
fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
let doc = self.identity_doc()?;
let refname = doc.default_branch()?.to_owned();
- let crefs = match doc.canonical_refs()? {
- Some(crefs) => crefs,
- // Fallback to constructing the default branch via the project
- // payload
- None => CanonicalRefs::from_iter([doc.default_branch_rule()?]),
- };
+
+ let crefs = doc.canonical_refs_or_default(|| {
+ doc.default_branch_rule()
+ .map_err(RepositoryError::from)
+ .map(CanonicalRefs::new)
+ })?;
+
Ok(crefs
.rules()
.canonical(refname, self)
commit 111cdd5d5b9997670099450d2f1049917165a050
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Tue Sep 16 14:41:27 2025 +0200
radicle/crefs/rules: Exact patterns for branches
Allows avoiding `try_from`, thus also get rid of one error variant.
diff --git a/crates/radicle/src/git/canonical/rules.rs b/crates/radicle/src/git/canonical/rules.rs
index ee676b450..7b1220777 100644
--- a/crates/radicle/src/git/canonical/rules.rs
+++ b/crates/radicle/src/git/canonical/rules.rs
@@ -81,6 +81,13 @@ impl<'a> TryFrom<Qualified<'a>> for Pattern {
}
impl Pattern {
+ /// Construct a [`Pattern`] that matches a branch exactly.
+ ///
+ /// The resulting [`Pattern`] will match `refs/heads/<name>`.
+ pub fn refs_heads_exact(name: &git::fmt::RefStr) -> Self {
+ Self(QualifiedPattern::from(git::refs::branch(name)))
+ }
+
/// Check if the `refname` matches the rule's `refspec`.
pub fn matches(&self, refname: &Qualified) -> bool {
// N.b. Git's refspecs do not quite match with glob-star semantics. A
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 292fded06..84f354d29 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -76,8 +76,6 @@ impl DocError {
#[derive(Debug, Error)]
pub enum DefaultBranchRuleError {
- #[error("could not create rule due to the reference name being invalid: {0}")]
- Pattern(#[from] rules::PatternError),
#[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
Payload(#[from] PayloadError),
}
@@ -746,7 +744,7 @@ impl Doc {
pub fn default_branch_rule(
&self,
) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
- let pattern = rules::Pattern::try_from(self.default_branch()?.to_owned())?;
+ let pattern = rules::Pattern::refs_heads_exact(self.project()?.default_branch());
let rule = rules::Rule::new(
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
self.threshold,
commit e45357ab9f850c532df208d27ac4d58db5d15c86
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sun Sep 7 14:01:27 2025 +0200
radicle/storage: Split `WriteRepository::set_head`
The method `set_head` does two things:
1. Compute the canonical head and set the default branch to target.
2. Set the symbolic reference `HEAD` to target the default branch.
Split these two concerns into:
1. `set_default_branch_to_canonical_head`
2. `set_head_to_default_branch`
diff --git a/crates/radicle-node/src/worker/fetch.rs b/crates/radicle-node/src/worker/fetch.rs
index f3fb13ed3..180f6fd5b 100644
--- a/crates/radicle-node/src/worker/fetch.rs
+++ b/crates/radicle-node/src/worker/fetch.rs
@@ -126,11 +126,9 @@ impl Handle {
// points to a repository that is temporary and gets moved by [`mv`].
let repo = storage.repository(rid)?;
repo.set_identity_head()?;
- match repo.set_head() {
- Ok(head) => {
- if head.is_updated() {
- log::trace!(target: "worker", "Set HEAD to {}", head.new);
- }
+ match repo.set_head_to_default_branch() {
+ Ok(()) => {
+ log::trace!(target: "worker", "Set HEAD successfully");
}
Err(RepositoryError::Quorum(e)) => {
log::warn!(target: "worker", "Fetch could not set HEAD for {rid}: {e}")
@@ -347,9 +345,6 @@ fn set_canonical_refs(
applied: &Applied,
) -> Result<Option<UpdatedCanonicalRefs>, error::Canonical> {
let identity = repo.identity()?;
- // TODO(finto): it's unfortunate that we may end up computing the default
- // branch again after `set_head` is called after the fetch. This is due to
- // the storage capabilities being leaked to this part of the code base.
let rules = identity
.canonical_refs_or_default(|| {
let rule = identity.doc().default_branch_rule()?;
diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs
index 7de690f20..42efdb5db 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -418,14 +418,8 @@ pub fn run(
// N.b. special case for handling the canonical ref, since it
// creates a symlink to HEAD
- if *refname == canonical_ref
- && stored
- .set_head()
- .map(|head| head.is_updated())
- .unwrap_or(false)
- {
- print_update();
- continue;
+ if *refname == canonical_ref {
+ stored.set_head_to_default_branch()?;
}
match stored.backend.refname_to_id(refname.as_str()) {
diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs
index fdcd15b9c..b8a5dd9c5 100644
--- a/crates/radicle/src/rad.rs
+++ b/crates/radicle/src/rad.rs
@@ -143,7 +143,8 @@ where
)?;
stored.set_remote_identity_root_to(pk, identity)?;
stored.set_identity_head_to(identity)?;
- stored.set_head()?;
+ stored.set_head_to_default_branch()?;
+ stored.set_default_branch_to_canonical_head()?;
let signed = stored.sign_refs(signer)?;
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index 4f604c52c..cba8b3380 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -88,7 +88,7 @@ impl FromIterator<PublicKey> for Namespaces {
}
}
-/// Output of [`WriteRepository::set_head`].
+/// Output of [`WriteRepository::set_default_branch_to_canonical_head`].
pub struct SetHead {
/// Old branch head.
pub old: Option<Oid>,
@@ -666,9 +666,16 @@ where
/// Allows read-write access to a repository.
pub trait WriteRepository: ReadRepository + SignRepository {
- /// Set the repository head to the canonical branch.
- /// This computes the head based on the delegate set.
- fn set_head(&self) -> Result<SetHead, RepositoryError>;
+ /// Sets the symbolic reference `HEAD` to target the default branch.
+ /// This only depends on the value for the default branch in the identity
+ /// document, and does not require the canonical reference behind the
+ /// default branch to be computed, or even exist.
+ fn set_head_to_default_branch(&self) -> Result<(), RepositoryError>;
+
+ /// Computes the head of the default branch based on the delegate set,
+ /// and sets it.
+ fn set_default_branch_to_canonical_head(&self) -> Result<SetHead, RepositoryError>;
+
/// Set the repository 'rad/id' to the canonical commit, agreed by quorum.
fn set_identity_head(&self) -> Result<Oid, RepositoryError> {
let head = self.canonical_identity_head()?;
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index b5c2fa2de..9d678200e 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -903,16 +903,42 @@ impl ReadRepository for Repository {
}
impl WriteRepository for Repository {
- fn set_head(&self) -> Result<SetHead, RepositoryError> {
+ fn set_head_to_default_branch(&self) -> Result<(), RepositoryError> {
let head_ref = refname!("HEAD");
+ let branch_ref = self.default_branch()?;
+
+ match self.raw().find_reference(head_ref.as_str()) {
+ Ok(mut head_ref) => {
+ if head_ref
+ .symbolic_target()
+ .is_some_and(|t| t != branch_ref.as_str())
+ {
+ head_ref.symbolic_set_target(branch_ref.as_str(), "set-head (radicle)")?;
+ }
+ Ok(())
+ }
+ Err(err) if err.is_not_found() => {
+ self.raw().reference_symbolic(
+ head_ref.as_str(),
+ branch_ref.as_str(),
+ true,
+ "set-head (radicle)",
+ )?;
+ Ok(())
+ }
+ Err(err) => Err(err.into()),
+ }
+ }
+
+ fn set_default_branch_to_canonical_head(&self) -> Result<SetHead, RepositoryError> {
+ let (branch_ref, new) = self.canonical_head()?;
+
let old = self
.raw()
- .refname_to_id(&head_ref)
+ .refname_to_id(&branch_ref)
.ok()
.map(|oid| oid.into());
- let (branch_ref, new) = self.canonical_head()?;
-
if old == Some(new) {
return Ok(SetHead { old, new });
}
@@ -920,10 +946,6 @@ impl WriteRepository for Repository {
self.raw()
.reference(&branch_ref, new.into(), true, "set-local-branch (radicle)")?;
- log::debug!(target: "storage", "Setting ref: {head_ref} -> {branch_ref}");
- self.raw()
- .reference_symbolic(&head_ref, &branch_ref, true, "set-head (radicle)")?;
-
Ok(SetHead { old, new })
}
diff --git a/crates/radicle/src/test.rs b/crates/radicle/src/test.rs
index e6b40ed2a..62d31b502 100644
--- a/crates/radicle/src/test.rs
+++ b/crates/radicle/src/test.rs
@@ -59,7 +59,8 @@ pub fn fetch<W: WriteRepository>(
drop(opts);
repo.set_identity_head()?;
- repo.set_head()?;
+ repo.set_head_to_default_branch()?;
+ repo.set_default_branch_to_canonical_head()?;
let validations = repo.validate()?;
if !validations.is_empty() {
diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs
index 36454800a..b6b10c214 100644
--- a/crates/radicle/src/test/storage.rs
+++ b/crates/radicle/src/test/storage.rs
@@ -329,7 +329,11 @@ impl WriteRepository for MockRepository {
todo!()
}
- fn set_head(&self) -> Result<SetHead, RepositoryError> {
+ fn set_head_to_default_branch(&self) -> Result<(), RepositoryError> {
+ todo!()
+ }
+
+ fn set_default_branch_to_canonical_head(&self) -> Result<SetHead, RepositoryError> {
todo!()
}
commit 609d0b303e1c4ca3cf03fb18442a9cf93f137a66
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Sun Sep 7 14:01:27 2025 +0200
radicle: More convenience methods to get default branch
diff --git a/crates/radicle/src/identity/doc.rs b/crates/radicle/src/identity/doc.rs
index 02e38bc02..292fded06 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -737,12 +737,16 @@ impl Doc {
Ok(proj)
}
+ /// Gets the qualified reference name of the default branch,
+ /// according to the project payload in this document.
+ pub fn default_branch(&self) -> Result<git::fmt::Qualified, PayloadError> {
+ Ok(git::refs::branch(self.project()?.default_branch()))
+ }
+
pub fn default_branch_rule(
&self,
) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
- let proj = self.project()?;
- let refname = proj.default_branch();
- let pattern = rules::Pattern::try_from(git::refs::branch(refname).to_owned())?;
+ let pattern = rules::Pattern::try_from(self.default_branch()?.to_owned())?;
let rule = rules::Rule::new(
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
self.threshold,
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index d16406824..4f604c52c 100644
--- a/crates/radicle/src/storage.rs
+++ b/crates/radicle/src/storage.rs
@@ -518,6 +518,12 @@ pub trait ReadRepository: Sized + ValidateRepository {
/// Returns the [`Oid`] as well as the qualified reference name.
fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError>;
+ /// Gets the qualified reference name of the default branch of self,
+ /// according to the project payload in the identity document.
+ fn default_branch(&self) -> Result<Qualified<'_>, RepositoryError> {
+ Ok(self.identity_doc()?.default_branch()?.to_owned())
+ }
+
/// Compute the canonical head of this repository.
///
/// Ignores any existing `HEAD` reference.
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index 887481c4a..b5c2fa2de 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -813,7 +813,7 @@ impl ReadRepository for Repository {
fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
let doc = self.identity_doc()?;
- let refname = git::refs::branch(doc.project()?.default_branch());
+ let refname = doc.default_branch()?.to_owned();
let crefs = match doc.canonical_refs()? {
Some(crefs) => crefs,
// Fallback to constructing the default branch via the project
Exit code: 0
shell: 'export RUSTDOCFLAGS=''-D warnings'' cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny warnings cargo build --all-targets --workspace cargo doc --workspace --no-deps --all-features cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 09fedefa-c37e-4c65-b6b1-184a319d5617 -v /opt/radcis/ci.rad.levitte.org/cci/state/09fedefa-c37e-4c65-b6b1-184a319d5617/s:/09fedefa-c37e-4c65-b6b1-184a319d5617/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/09fedefa-c37e-4c65-b6b1-184a319d5617/w:/09fedefa-c37e-4c65-b6b1-184a319d5617/w -w /09fedefa-c37e-4c65-b6b1-184a319d5617/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /09fedefa-c37e-4c65-b6b1-184a319d5617/s/script.sh
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.90-x86_64-unknown-linux-gnu'
info: latest update on 2025-09-18, rust version 1.90.0 (1159e78c4 2025-09-14)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.90.0 (840b83a10 2025-07-30)
+ rustc --version
rustc 1.90.0 (1159e78c4 2025-09-14)
+ cargo fmt --check
+ cargo clippy --all-targets --workspace -- --deny warnings
Updating crates.io index
Downloading crates ...
Downloaded addr2line v0.25.1
Downloaded filetime v0.2.23
Downloaded icu_locid v1.5.0
Downloaded base64ct v1.6.0
Downloaded fraction v0.15.3
Downloaded human-panic v2.0.6
Downloaded base64 v0.22.1
Downloaded itoa v1.0.11
Downloaded gix-config-value v0.15.1
Downloaded keccak v0.1.5
Downloaded gix-object v0.50.2
Downloaded itertools v0.14.0
Downloaded nonempty v0.9.0
Downloaded newline-converter v0.3.0
Downloaded generic-array v0.14.7
Downloaded inout v0.1.3
Downloaded inquire v0.7.5
Downloaded noise-framework v0.4.0
Downloaded normalize-line-endings v0.3.0
Downloaded maybe-async v0.2.10
Downloaded outref v0.5.2
Downloaded percent-encoding v2.3.1
Downloaded pbkdf2 v0.12.2
Downloaded radicle-git-ext v0.11.0
Downloaded ref-cast v1.0.24
Downloaded ref-cast-impl v1.0.24
Downloaded ppv-lite86 v0.2.17
Downloaded once_cell v1.21.3
Downloaded same-file v1.0.6
Downloaded rand_core v0.9.3
Downloaded shell-words v1.1.0
Downloaded signature v2.2.0
Downloaded siphasher v1.0.1
Downloaded signal-hook-mio v0.2.4
Downloaded gix-tempfile v18.0.0
Downloaded signature v1.6.4
Downloaded ssh-cipher v0.2.0
Downloaded sha2 v0.10.8
Downloaded strsim v0.11.1
Downloaded shlex v1.3.0
Downloaded sqlite v0.32.0
Downloaded sval_ref v2.14.1
Downloaded test-log v0.2.19
Downloaded serde_derive_internals v0.29.1
Downloaded sval_dynamic v2.14.1
Downloaded sval_fmt v2.14.1
Downloaded log v0.4.27
Downloaded tinyvec_macros v0.1.1
Downloaded serde_core v1.0.228
Downloaded tree-sitter-language v0.1.2
Downloaded test-log-macros v0.2.19
Downloaded thiserror-impl v2.0.17
Downloaded tree-sitter-highlight v0.24.4
Downloaded unarray v0.1.4
Downloaded toml_datetime v0.7.5+spec-1.1.0
Downloaded unicode-display-width v0.3.0
Downloaded universal-hash v0.5.1
Downloaded typeid v1.0.3
Downloaded pkcs1 v0.7.5
Downloaded uuid-simd v0.8.0
Downloaded gix-protocol v0.51.0
Downloaded value-bag-serde1 v1.11.1
Downloaded tree-sitter-css v0.23.1
Downloaded zerofrom v0.1.6
Downloaded write16 v1.0.0
Downloaded zerofrom-derive v0.1.6
Downloaded thread_local v1.1.9
Downloaded xattr v1.3.1
Downloaded unicode-ident v1.0.12
Downloaded zerovec-derive v0.10.3
Downloaded toml v0.9.12+spec-1.1.0
Downloaded value-bag v1.11.1
Downloaded zeroize v1.7.0
Downloaded yansi v0.5.1
Downloaded writeable v0.5.5
Downloaded uuid v1.20.0
Downloaded url v2.5.4
Downloaded clap_builder v4.5.44
Downloaded unicode-segmentation v1.11.0
Downloaded gix-pack v0.60.0
Downloaded unicode-normalization v0.1.23
Downloaded tracing-core v0.1.36
Downloaded p384 v0.13.0
Downloaded zerovec v0.10.4
Downloaded zerocopy v0.7.35
Downloaded winnow v0.7.13
Downloaded zlib-rs v0.5.2
Downloaded syn v1.0.109
Downloaded tree-sitter-c v0.23.2
Downloaded vcpkg v0.2.15
Downloaded syn v2.0.106
Downloaded tree-sitter-python v0.23.4
Downloaded tree-sitter-md v0.3.2
Downloaded tree-sitter-rust v0.23.2
Downloaded rustix v1.0.7
Downloaded regex v1.11.1
Downloaded unicode-width v0.2.1
Downloaded tracing-subscriber v0.3.22
Downloaded tree-sitter-bash v0.23.3
Downloaded regex-syntax v0.8.5
Downloaded sysinfo v0.37.2
Downloaded icu_properties_data v1.5.1
Downloaded git2 v0.19.0
Downloaded rustix v0.38.34
Downloaded proptest v1.9.0
Downloaded gimli v0.32.3
Downloaded tree-sitter v0.24.4
Downloaded tracing v0.1.44
Downloaded tree-sitter-ruby v0.23.1
Downloaded portable-atomic v1.11.0
Downloaded object v0.37.3
Downloaded serde_json v1.0.140
Downloaded regex-automata v0.4.9
Downloaded radicle-surf v0.26.0
Downloaded sha1-checked v0.10.0
Downloaded bloomy v1.2.0
Downloaded ssh-key v0.6.6
Downloaded idna v1.0.3
Downloaded tokio v1.47.1
Downloaded jiff v0.2.15
Downloaded libc v0.2.174
Downloaded libm v0.2.8
Downloaded bstr v1.12.0
Downloaded hashbrown v0.15.5
Downloaded hashbrown v0.14.3
Downloaded tree-sitter-typescript v0.23.2
Downloaded sha3 v0.10.8
Downloaded tree-sitter-go v0.23.4
Downloaded prodash v30.0.1
Downloaded mio v0.8.11
Downloaded thiserror v2.0.17
Downloaded icu_collections v1.5.0
Downloaded rsa v0.9.6
Downloaded p521 v0.13.3
Downloaded gix-ref v0.53.1
Downloaded flate2 v1.1.1
Downloaded unicode-width v0.1.11
Downloaded proc-macro2 v1.0.101
Downloaded chrono v0.4.38
Downloaded qcheck v1.0.0
Downloaded yoke-derive v0.7.5
Downloaded yoke v0.7.5
Downloaded vsimd v0.8.0
Downloaded typenum v1.17.0
Downloaded tree-sitter-html v0.23.2
Downloaded num-bigint-dig v0.8.4
Downloaded tracing-log v0.2.0
Downloaded snapbox v0.4.17
Downloaded signals_receipts v0.2.0
Downloaded memchr v2.7.2
Downloaded indicatif v0.18.0
Downloaded thiserror v1.0.69
Downloaded sem_safe v0.2.0
Downloaded walkdir v2.5.0
Downloaded value-bag-sval2 v1.11.1
Downloaded version_check v0.9.4
Downloaded utf8parse v0.2.2
Downloaded unit-prefix v0.5.1
Downloaded wait-timeout v0.2.1
Downloaded linux-raw-sys v0.4.13
Downloaded utf8_iter v1.0.4
Downloaded utf16_iter v1.0.5
Downloaded systemd-journal-logger v2.2.2
Downloaded tempfile v3.23.0
Downloaded num-bigint v0.4.6
Downloaded gix-hash v0.19.0
Downloaded tar v0.4.40
Downloaded synstructure v0.13.1
Downloaded serde v1.0.228
Downloaded schemars v1.0.4
Downloaded gix-odb v0.70.0
Downloaded tree-sitter-toml-ng v0.6.0
Downloaded tinyvec v1.6.0
Downloaded sval_buffer v2.14.1
Downloaded sval v2.14.1
Downloaded rand v0.9.2
Downloaded phf v0.11.3
Downloaded sharded-slab v0.1.7
Downloaded indexmap v2.2.6
Downloaded heapless v0.8.0
Downloaded tree-sitter-json v0.24.8
Downloaded libgit2-sys v0.17.0+1.8.1
Downloaded toml_writer v1.0.6+spec-1.1.0
Downloaded socket2 v0.5.7
Downloaded smallvec v1.15.1
Downloaded tinystr v0.7.6
Downloaded timeago v0.4.2
Downloaded rand v0.8.5
Downloaded spin v0.9.8
Downloaded signal-hook-registry v1.4.5
Downloaded jsonschema v0.30.0
Downloaded gix-diff v0.53.0
Downloaded thiserror-impl v1.0.69
Downloaded similar v2.5.0
Downloaded signal-hook v0.3.18
Downloaded serde_derive v1.0.228
Downloaded linux-raw-sys v0.9.4
Downloaded mio v1.0.4
Downloaded icu_normalizer_data v1.5.1
Downloaded getrandom v0.3.3
Downloaded sval_nested v2.14.1
Downloaded structured-logger v1.0.4
Downloaded sec1 v0.7.3
Downloaded crossbeam-channel v0.5.15
Downloaded streaming-iterator v0.1.9
Downloaded sqlite3-sys v0.15.2
Downloaded spki v0.7.3
Downloaded icu_provider v1.5.0
Downloaded crossterm v0.29.0
Downloaded litrs v0.4.1
Downloaded crossterm v0.25.0
Downloaded subtle v2.5.0
Downloaded hmac v0.12.1
Downloaded stable_deref_trait v1.2.0
Downloaded ssh-encoding v0.2.0
Downloaded quote v1.0.41
Downloaded ryu v1.0.17
Downloaded pretty_assertions v1.4.0
Downloaded pkg-config v0.3.30
Downloaded sval_serde v2.14.1
Downloaded sval_json v2.14.1
Downloaded poly1305 v0.8.0
Downloaded sqlite3-src v0.5.1
Downloaded pin-project-lite v0.2.16
Downloaded num-rational v0.4.2
Downloaded rustc-demangle v0.1.26
Downloaded pkcs8 v0.10.2
Downloaded derive_more v2.0.1
Downloaded primeorder v0.13.6
Downloaded polyval v0.6.2
Downloaded crypto-bigint v0.5.5
Downloaded lock_api v0.4.14
Downloaded icu_properties v1.5.1
Downloaded socks5-client v0.4.1
Downloaded jobserver v0.1.31
Downloaded serde_fmt v1.0.3
Downloaded serde-untagged v0.1.7
Downloaded snapbox-macros v0.3.8
Downloaded scopeguard v1.2.0
Downloaded schemars_derive v1.0.4
Downloaded rusty-fork v0.3.1
Downloaded referencing v0.30.0
Downloaded scrypt v0.11.0
Downloaded gix-fs v0.16.1
Downloaded p256 v0.13.2
Downloaded num-traits v0.2.19
Downloaded libz-rs-sys v0.5.2
Downloaded gix-traverse v0.47.0
Downloaded der v0.7.9
Downloaded siphasher v0.3.11
Downloaded gix-features v0.43.1
Downloaded salsa20 v0.10.2
Downloaded pem-rfc7468 v0.7.0
Downloaded parking_lot v0.12.5
Downloaded sha1 v0.10.6
Downloaded serde_spanned v1.0.4
Downloaded rfc6979 v0.4.0
Downloaded quick-error v1.2.3
Downloaded rand_core v0.6.4
Downloaded qcheck-macros v1.0.0
Downloaded proc-macro-error-attr2 v2.0.0
Downloaded phf_shared v0.11.3
Downloaded parking_lot_core v0.9.12
Downloaded rand_chacha v0.9.0
Downloaded rand_chacha v0.3.1
Downloaded num v0.4.3
Downloaded miniz_oxide v0.8.8
Downloaded chacha20poly1305 v0.10.1
Downloaded icu_locid_transform_data v1.5.1
Downloaded fxhash v0.2.1
Downloaded elliptic-curve v0.13.8
Downloaded rand_xorshift v0.4.0
Downloaded radicle-std-ext v0.2.0
Downloaded proc-macro-error2 v2.0.1
Downloaded lexopt v0.3.0
Downloaded gix-packetline v0.19.1
Downloaded emojis v0.6.4
Downloaded num-iter v0.1.45
Downloaded lazy_static v1.5.0
Downloaded idna_adapter v1.2.0
Downloaded gix-revwalk v0.21.0
Downloaded gix-date v0.10.5
Downloaded cc v1.2.2
Downloaded paste v1.0.15
Downloaded num-integer v0.1.46
Downloaded nu-ansi-term v0.50.3
Downloaded memmap2 v0.9.8
Downloaded const-oid v0.9.6
Downloaded gix-sec v0.12.0
Downloaded derive_more-impl v2.0.1
Downloaded num-cmp v0.1.0
Downloaded gix-shallow v0.5.0
Downloaded gix-quote v0.6.0
Downloaded gix-lock v18.0.0
Downloaded email_address v0.2.9
Downloaded opaque-debug v0.3.1
Downloaded num-complex v0.4.6
Downloaded heck v0.5.0
Downloaded gix-commitgraph v0.29.0
Downloaded fancy-regex v0.14.0
Downloaded bitflags v1.3.2
Downloaded data-encoding v2.5.0
Downloaded jiff-static v0.2.15
Downloaded iana-time-zone v0.1.60
Downloaded gix-transport v0.48.0
Downloaded libz-sys v1.1.16
Downloaded ghash v0.5.1
Downloaded fnv v1.0.7
Downloaded crc32fast v1.5.0
Downloaded block-buffer v0.10.4
Downloaded multibase v0.9.1
Downloaded matchers v0.2.0
Downloaded group v0.13.0
Downloaded gix-utils v0.3.0
Downloaded faster-hex v0.10.0
Downloaded errno v0.3.13
Downloaded erased-serde v0.4.6
Downloaded dyn-clone v1.0.17
Downloaded diff v0.1.13
Downloaded cyphernet v0.5.2
Downloaded clap_complete v4.5.60
Downloaded env_filter v0.1.3
Downloaded cyphergraphy v0.3.0
Downloaded console v0.16.0
Downloaded clap_derive v4.5.41
Downloaded cfg-if v1.0.0
Downloaded amplify_derive v4.0.0
Downloaded gix-command v0.6.2
Downloaded ec25519 v0.1.0
Downloaded dunce v1.0.5
Downloaded crossbeam-utils v0.8.19
Downloaded convert_case v0.7.1
Downloaded chacha20 v0.9.1
Downloaded env_logger v0.11.8
Downloaded cpufeatures v0.2.12
Downloaded bytes v1.11.1
Downloaded byteorder v1.5.0
Downloaded gix-trace v0.1.13
Downloaded form_urlencoded v1.2.1
Downloaded ed25519 v1.5.3
Downloaded colored v2.1.0
Downloaded cbc v0.1.2
Downloaded bit-vec v0.8.0
Downloaded litemap v0.7.5
Downloaded icu_provider_macros v1.5.0
Downloaded gix-path v0.10.20
Downloaded fast-glob v0.3.3
Downloaded ecdsa v0.16.9
Downloaded digest v0.10.7
Downloaded data-encoding-macro v0.1.14
Downloaded cypheraddr v0.4.0
Downloaded ctr v0.9.2
Downloaded clap v4.5.44
Downloaded borrow-or-share v0.2.2
Downloaded amplify v4.6.0
Downloaded home v0.5.9
Downloaded gix-refspec v0.31.0
Downloaded git-ref-format-macro v0.6.0
Downloaded fluent-uri v0.3.2
Downloaded fastrand v2.3.0
Downloaded escargot v0.5.10
Downloaded equivalent v1.0.1
Downloaded either v1.11.0
Downloaded document-features v0.2.11
Downloaded data-encoding-macro-internal v0.1.12
Downloaded ct-codecs v1.1.1
Downloaded cipher v0.4.4
Downloaded bit-set v0.8.0
Downloaded base32 v0.4.0
Downloaded crypto-common v0.1.6
Downloaded anyhow v1.0.82
Downloaded icu_locid_transform v1.5.0
Downloaded hash32 v0.3.1
Downloaded gix-validate v0.10.0
Downloaded gix-negotiate v0.21.0
Downloaded gix-credentials v0.30.0
Downloaded gix-chunk v0.4.11
Downloaded bytecount v0.6.8
Downloaded getrandom v0.2.15
Downloaded bitflags v2.9.1
Downloaded bcrypt-pbkdf v0.10.0
Downloaded is_terminal_polyfill v1.70.2
Downloaded icu_normalizer v1.5.0
Downloaded gix-url v0.32.0
Downloaded gix-revision v0.35.0
Downloaded gix-prompt v0.11.1
Downloaded gix-hashtable v0.9.0
Downloaded git-ref-format-core v0.6.0
Downloaded git-ref-format v0.6.0
Downloaded displaydoc v0.2.5
Downloaded clap_lex v0.7.5
Downloaded bytesize v2.0.1
Downloaded blowfish v0.9.1
Downloaded block-padding v0.3.3
Downloaded backtrace v0.3.76
Downloaded aho-corasick v1.1.3
Downloaded autocfg v1.2.0
Downloaded anstyle-parse v0.2.3
Downloaded amplify_num v0.5.2
Downloaded ahash v0.8.11
Downloaded aes-gcm v0.10.3
Downloaded aes v0.8.4
Downloaded gix-actor v0.35.4
Downloaded colorchoice v1.0.0
Downloaded base-x v0.2.11
Downloaded ascii v1.1.0
Downloaded arc-swap v1.7.1
Downloaded aead v0.5.2
Downloaded base64 v0.21.7
Downloaded anstyle-query v1.0.2
Downloaded anstream v0.6.21
Downloaded amplify_syn v2.0.1
Downloaded base16ct v0.2.0
Downloaded anstyle v1.0.13
Downloaded ff v0.13.0
Downloaded adler2 v2.0.0
Compiling libc v0.2.174
Compiling proc-macro2 v1.0.101
Compiling unicode-ident v1.0.12
Compiling quote v1.0.41
Checking cfg-if v1.0.0
Compiling shlex v1.3.0
Compiling version_check v0.9.4
Checking memchr v2.7.2
Compiling typenum v1.17.0
Checking getrandom v0.2.15
Compiling generic-array v0.14.7
Compiling syn v2.0.106
Compiling jobserver v0.1.31
Checking rand_core v0.6.4
Compiling cc v1.2.2
Compiling serde_core v1.0.228
Checking crypto-common v0.1.6
Checking regex-syntax v0.8.5
Checking aho-corasick v1.1.3
Compiling serde v1.0.228
Checking smallvec v1.15.1
Compiling thiserror v2.0.17
Checking subtle v2.5.0
Checking regex-automata v0.4.9
Checking once_cell v1.21.3
Checking stable_deref_trait v1.2.0
Checking cpufeatures v0.2.12
Checking fastrand v2.3.0
Compiling parking_lot_core v0.9.12
Checking scopeguard v1.2.0
Checking lock_api v0.4.14
Checking block-buffer v0.10.4
Checking parking_lot v0.12.5
Checking digest v0.10.7
Compiling crc32fast v1.5.0
Checking byteorder v1.5.0
Checking tinyvec_macros v0.1.1
Checking bitflags v2.9.1
Checking tinyvec v1.6.0
Checking gix-trace v0.1.13
Checking home v0.5.9
Compiling typeid v1.0.3
Compiling synstructure v0.13.1
Checking unicode-normalization v0.1.23
Checking gix-utils v0.3.0
Checking zlib-rs v0.5.2
Checking itoa v1.0.11
Checking bstr v1.12.0
Compiling serde_derive v1.0.228
Compiling thiserror-impl v2.0.17
Compiling zerofrom-derive v0.1.6
Checking libz-rs-sys v0.5.2
Checking same-file v1.0.6
Checking walkdir v2.5.0
Checking flate2 v1.1.1
Compiling yoke-derive v0.7.5
Checking gix-validate v0.10.0
Checking zerofrom v0.1.6
Checking gix-path v0.10.20
Checking prodash v30.0.1
Compiling zerovec-derive v0.10.3
Checking gix-features v0.43.1
Compiling displaydoc v0.2.5
Checking yoke v0.7.5
Compiling getrandom v0.3.3
Checking zerovec v0.10.4
Compiling heapless v0.8.0
Checking hash32 v0.3.1
Checking zeroize v1.7.0
Compiling icu_locid_transform_data v1.5.1
Checking litemap v0.7.5
Compiling rustix v1.0.7
Checking tinystr v0.7.6
Checking writeable v0.5.5
Compiling pkg-config v0.3.30
Checking icu_locid v1.5.0
Checking erased-serde v0.4.6
Checking serde_fmt v1.0.3
Checking faster-hex v0.10.0
Compiling icu_provider_macros v1.5.0
Checking value-bag-serde1 v1.11.1
Checking value-bag v1.11.1
Checking linux-raw-sys v0.9.4
Compiling icu_properties_data v1.5.1
Checking sha1 v0.10.6
Checking log v0.4.27
Checking icu_provider v1.5.0
Checking block-padding v0.3.3
Compiling icu_normalizer_data v1.5.1
Checking inout v0.1.3
Checking icu_locid_transform v1.5.0
Checking sha1-checked v0.10.0
Checking icu_collections v1.5.0
Compiling syn v1.0.109
Checking gix-hash v0.19.0
Checking icu_properties v1.5.1
Checking cipher v0.4.4
Checking utf8_iter v1.0.4
Checking utf16_iter v1.0.5
Checking write16 v1.0.0
Checking percent-encoding v2.3.1
Compiling thiserror v1.0.69
Compiling thiserror-impl v1.0.69
Checking icu_normalizer v1.5.0
Compiling serde_json v1.0.140
Checking idna_adapter v1.2.0
Checking equivalent v1.0.1
Checking idna v1.0.3
Checking hashbrown v0.14.3
Checking form_urlencoded v1.2.1
Checking sha2 v0.10.8
Compiling vcpkg v0.2.15
Compiling ref-cast v1.0.24
Checking indexmap v2.2.6
Checking ryu v1.0.17
Checking url v2.5.4
Compiling libz-sys v1.1.16
Checking tempfile v3.23.0
Compiling ref-cast-impl v1.0.24
Checking universal-hash v0.5.1
Checking opaque-debug v0.3.1
Checking dyn-clone v1.0.17
Compiling autocfg v1.2.0
Compiling amplify_syn v2.0.1
Compiling libgit2-sys v0.17.0+1.8.1
Checking signature v1.6.4
Checking ed25519 v1.5.3
Compiling num-traits v0.2.19
Compiling serde_derive_internals v0.29.1
Checking aead v0.5.2
Checking ct-codecs v1.1.1
Compiling amplify_derive v4.0.0
Checking ascii v1.1.0
Checking amplify_num v0.5.2
Compiling schemars_derive v1.0.4
Checking ec25519 v0.1.0
Checking amplify v4.6.0
Checking git-ref-format-core v0.6.0
Checking poly1305 v0.8.0
Checking chacha20 v0.9.1
Checking schemars v1.0.4
Checking cyphergraphy v0.3.0
Checking polyval v0.6.2
Compiling sqlite3-src v0.5.1
Checking hmac v0.12.1
Checking base64ct v1.6.0
Checking keccak v0.1.5
Checking sha3 v0.10.8
Checking pem-rfc7468 v0.7.0
Checking pbkdf2 v0.12.2
Checking ghash v0.5.1
Checking aes v0.8.4
Checking ctr v0.9.2
Checking rand v0.8.5
Compiling data-encoding v2.5.0
Checking base32 v0.4.0
Checking cypheraddr v0.4.0
Checking qcheck v1.0.0
Compiling data-encoding-macro-internal v0.1.12
Checking aes-gcm v0.10.3
Checking ssh-encoding v0.2.0
Checking chacha20poly1305 v0.10.1
Checking blowfish v0.9.1
Checking cbc v0.1.2
Checking bcrypt-pbkdf v0.10.0
Checking ssh-cipher v0.2.0
Checking noise-framework v0.4.0
Checking data-encoding-macro v0.1.14
Checking socks5-client v0.4.1
Compiling crossbeam-utils v0.8.19
Checking signature v2.2.0
Checking base-x v0.2.11
Checking multibase v0.9.1
Checking ssh-key v0.6.6
Checking cyphernet v0.5.2
Checking radicle-ssh v0.10.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-ssh)
Checking jiff v0.2.15
Checking crossbeam-channel v0.5.15
Checking lazy_static v1.5.0
Checking nonempty v0.9.0
Checking siphasher v1.0.1
Checking anstyle-query v1.0.2
Checking radicle-git-metadata v0.1.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-git-metadata)
Checking radicle-dag v0.10.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-dag)
Checking winnow v0.7.13
Checking hashbrown v0.15.5
Checking gix-date v0.10.5
Checking utf8parse v0.2.2
Checking gix-hashtable v0.9.0
Checking anstyle-parse v0.2.3
Checking gix-actor v0.35.4
Checking radicle-git-ref-format v0.1.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-git-ref-format)
Checking base64 v0.21.7
Checking anstyle v1.0.13
Checking iana-time-zone v0.1.60
Checking is_terminal_polyfill v1.70.2
Checking colorchoice v1.0.0
Checking chrono v0.4.38
Checking anstream v0.6.21
Checking gix-object v0.50.2
Checking colored v2.1.0
Checking radicle-localtime v0.1.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-localtime)
Checking serde-untagged v0.1.7
Checking bytesize v2.0.1
Checking memmap2 v0.9.8
Checking dunce v1.0.5
Checking tree-sitter-language v0.1.2
Checking fast-glob v0.3.3
Checking gix-chunk v0.4.11
Checking gix-fs v0.16.1
Checking gix-commitgraph v0.29.0
Checking gix-tempfile v18.0.0
Checking gix-revwalk v0.21.0
Checking mio v1.0.4
Checking gix-quote v0.6.0
Checking errno v0.3.13
Checking sem_safe v0.2.0
Checking shell-words v1.1.0
Checking either v1.11.0
Checking gix-command v0.6.2
Checking signals_receipts v0.2.0
Compiling signal-hook v0.3.18
Compiling object v0.37.3
Checking gix-lock v18.0.0
Checking gix-url v0.32.0
Checking gix-config-value v0.15.1
Checking gix-sec v0.12.0
Checking signal-hook-registry v1.4.5
Checking gimli v0.32.3
Compiling rustix v0.38.34
Checking adler2 v2.0.0
Checking miniz_oxide v0.8.8
Checking gix-prompt v0.11.1
Checking addr2line v0.25.1
Checking radicle-signals v0.11.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-signals)
Checking gix-traverse v0.47.0
Checking gix-revision v0.35.0
Checking gix-diff v0.53.0
Checking mio v0.8.11
Checking gix-packetline v0.19.1
Compiling tree-sitter v0.24.4
Compiling anyhow v1.0.82
Compiling linux-raw-sys v0.4.13
Compiling unicode-segmentation v1.11.0
Checking rustc-demangle v0.1.26
Compiling convert_case v0.7.1
Checking backtrace v0.3.76
Checking gix-transport v0.48.0
Checking signal-hook-mio v0.2.4
Checking gix-pack v0.60.0
Checking gix-refspec v0.31.0
Checking gix-credentials v0.30.0
Checking gix-shallow v0.5.0
Checking gix-ref v0.53.1
Checking gix-negotiate v0.21.0
Compiling maybe-async v0.2.10
Checking regex v1.11.1
Compiling proc-macro-error-attr2 v2.0.0
Checking arc-swap v1.7.1
Compiling portable-atomic v1.11.0
Checking gix-odb v0.70.0
Compiling proc-macro-error2 v2.0.1
Checking gix-protocol v0.51.0
Compiling xattr v1.3.1
Compiling derive_more-impl v2.0.1
Compiling filetime v0.2.23
Checking uuid v1.20.0
Checking unicode-width v0.2.1
Checking bitflags v1.3.2
Compiling litrs v0.4.1
Checking bytes v1.11.1
Compiling document-features v0.2.11
Checking console v0.16.0
Checking crossterm v0.25.0
Checking derive_more v2.0.1
Compiling tar v0.4.40
Compiling git-ref-format-macro v0.6.0
Checking newline-converter v0.3.0
Checking snapbox-macros v0.3.8
Checking salsa20 v0.10.2
Checking fxhash v0.2.1
Checking normalize-line-endings v0.3.0
Checking similar v2.5.0
Checking strsim v0.11.1
Checking streaming-iterator v0.1.9
Checking siphasher v0.3.11
Checking unit-prefix v0.5.1
Checking unicode-width v0.1.11
Compiling heck v0.5.0
Checking clap_lex v0.7.5
Checking clap_builder v4.5.44
Compiling clap_derive v4.5.41
Checking sqlite3-sys v0.15.2
Checking sqlite v0.32.0
Checking radicle-crypto v0.14.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-crypto)
Checking inquire v0.7.5
Checking bloomy v1.2.0
Checking indicatif v0.18.0
Checking snapbox v0.4.17
Checking scrypt v0.11.0
Compiling radicle-surf v0.26.0
Checking git-ref-format v0.6.0
Checking crossterm v0.29.0
Checking unicode-display-width v0.3.0
Checking systemd-journal-logger v2.2.2
Checking serde_spanned v1.0.4
Checking toml_datetime v0.7.5+spec-1.1.0
Compiling tree-sitter-toml-ng v0.6.0
Compiling tree-sitter-c v0.23.2
Compiling tree-sitter-bash v0.23.3
Compiling tree-sitter-html v0.23.2
Compiling tree-sitter-css v0.23.1
Compiling tree-sitter-json v0.24.8
Compiling tree-sitter-typescript v0.23.2
Compiling tree-sitter-md v0.3.2
Compiling tree-sitter-ruby v0.23.1
Compiling tree-sitter-python v0.23.4
Compiling tree-sitter-go v0.23.4
Compiling tree-sitter-rust v0.23.2
Checking pin-project-lite v0.2.16
Checking toml_writer v1.0.6+spec-1.1.0
Checking radicle-std-ext v0.2.0
Checking toml v0.9.12+spec-1.1.0
Checking tokio v1.47.1
Checking clap v4.5.44
Checking sysinfo v0.37.2
Checking yansi v0.5.1
Compiling radicle-cli v0.17.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-cli)
Checking diff v0.1.13
Compiling radicle-node v0.16.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-node)
Checking pretty_assertions v1.4.0
Checking human-panic v2.0.6
Checking clap_complete v4.5.60
Checking structured-logger v1.0.4
Checking radicle-systemd v0.11.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-systemd)
Checking tree-sitter-highlight v0.24.4
Checking itertools v0.14.0
Checking num-integer v0.1.46
Compiling qcheck-macros v1.0.0
Checking socket2 v0.5.7
Checking lexopt v0.3.0
Checking timeago v0.4.2
Compiling escargot v0.5.10
Checking bit-vec v0.8.0
Checking bit-set v0.8.0
Checking num-bigint v0.4.6
Checking rand_core v0.9.3
Compiling ahash v0.8.11
Checking num-iter v0.1.45
Checking num-complex v0.4.6
Checking env_filter v0.1.3
Checking zerocopy v0.7.35
Checking borrow-or-share v0.2.2
Checking fluent-uri v0.3.2
Checking num-rational v0.4.2
Checking env_logger v0.11.8
Checking num v0.4.3
Checking phf_shared v0.11.3
Compiling test-log-macros v0.2.19
Checking wait-timeout v0.2.1
Checking quick-error v1.2.3
Compiling radicle-remote-helper v0.14.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-remote-helper)
Checking vsimd v0.8.0
Checking fnv v1.0.7
Checking outref v0.5.2
Checking ppv-lite86 v0.2.17
Compiling paste v1.0.15
Checking uuid-simd v0.8.0
Checking rand_chacha v0.9.0
Checking rusty-fork v0.3.1
Checking test-log v0.2.19
Checking phf v0.11.3
Checking referencing v0.30.0
Checking fraction v0.15.3
Checking rand v0.9.2
Checking rand_xorshift v0.4.0
Checking fancy-regex v0.14.0
Checking email_address v0.2.9
Checking bytecount v0.6.8
Checking base64 v0.22.1
Checking unarray v0.1.4
Checking num-cmp v0.1.0
Checking proptest v1.9.0
Checking emojis v0.6.4
Checking jsonschema v0.30.0
Checking radicle-windows v0.1.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-windows)
Checking git2 v0.19.0
Checking radicle-oid v0.1.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-oid)
Checking radicle-git-ext v0.11.0
Checking radicle-term v0.16.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-term)
Checking radicle-cob v0.17.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-cob)
Checking radicle-core v0.1.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-core)
Checking radicle v0.20.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle)
Checking radicle-fetch v0.16.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-fetch)
Checking radicle-cli-test v0.13.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-cli-test)
Checking radicle-schemars v0.6.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-schemars)
Checking radicle-protocol v0.4.0 (/09fedefa-c37e-4c65-b6b1-184a319d5617/w/crates/radicle-protocol)
error[E0433]: failed to resolve: use of undeclared type `RepositoryError`
--> crates/radicle-node/src/worker/fetch.rs:129:25
|
129 | Err(RepositoryError::Quorum(e)) => {
| ^^^^^^^^^^^^^^^ use of undeclared type `RepositoryError`
|
help: a struct with a similar name exists
|
129 - Err(RepositoryError::Quorum(e)) => {
129 + Err(Repository::Quorum(e)) => {
|
help: consider importing one of these enums
|
1 + use crate::test::storage::RepositoryError;
|
1 + use radicle::test::storage::RepositoryError;
|
error[E0599]: no method named `set_head_to_default_branch` found for struct `radicle::storage::git::Repository` in the current scope
--> crates/radicle-node/src/worker/fetch.rs:125:28
|
125 | match repo.set_head_to_default_branch() {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: there is a method `default_branch` with a similar name
|
125 - match repo.set_head_to_default_branch() {
125 + match repo.default_branch() {
|
Some errors have detailed explanations: E0433, E0599.
For more information about an error, try `rustc --explain E0433`.
error: could not compile `radicle-node` (lib) due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `radicle-node` (lib test) due to 2 previous errors
Exit code: 101
{
"response": "finished",
"result": "failure"
}