rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood0d8ccb51101df44aa643c14e421f0a8f5a1aca53
{
"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": "Created",
"patch": {
"id": "63b4e1d9046a2af75401f74234d7fc18b447109d",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"title": "Canonical Symbolic References",
"state": {
"status": "draft",
"conflicts": []
},
"before": "86472fdccbf95d08d0184776ee1ca75d01caf2c8",
"after": "0d8ccb51101df44aa643c14e421f0a8f5a1aca53",
"commits": [
"0d8ccb51101df44aa643c14e421f0a8f5a1aca53",
"dcb39974eb51db8214787d94e3db87ba64cd9466",
"b7425da06cc20af24219c2b2e2fe9f1af1c3577b",
"8df72286807a06acfb985c1dec5704ec0303dd0e",
"8f5d1d62fd4457db6b5e720163c66b91af4df333",
"14c7559de499278793eeb7331f3e1b891989e0fc"
],
"target": "19210faab807e9163feea8c4ca2d3fe99e00a011",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "63b4e1d9046a2af75401f74234d7fc18b447109d",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": "lorenz"
},
"description": "Work in progress.",
"base": "86472fdccbf95d08d0184776ee1ca75d01caf2c8",
"oid": "0d8ccb51101df44aa643c14e421f0a8f5a1aca53",
"timestamp": 1758036727
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "bffb0275-43a9-497f-a492-499754f47148"
},
"info_url": "https://cci.rad.levitte.org//bffb0275-43a9-497f-a492-499754f47148.html"
}
Started at: 2025-09-16 17:34:13.647106+02: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/bffb0275-43a9-497f-a492-499754f47148/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 118 issues · 17 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 63b4e1d9046a2af75401f74234d7fc18b447109d
✓ Switched to branch patch/63b4e1d at revision 63b4e1d
✓ Branch patch/63b4e1d setup to track rad/patches/63b4e1d9046a2af75401f74234d7fc18b447109d
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 0d8ccb51101df44aa643c14e421f0a8f5a1aca53
HEAD is now at 0d8ccb51 radicle/crefs: Support symrefs
Exit code: 0
$ git show 0d8ccb51101df44aa643c14e421f0a8f5a1aca53
commit 0d8ccb51101df44aa643c14e421f0a8f5a1aca53
Author: Lorenz Leutgeb <lorenz.leutgeb@radicle.xyz>
Date: Tue Sep 16 14:41:27 2025 +0200
radicle/crefs: Support symrefs
diff --git a/crates/radicle-node/src/worker/fetch.rs b/crates/radicle-node/src/worker/fetch.rs
index 9202cb4b..5056bbd4 100644
--- a/crates/radicle-node/src/worker/fetch.rs
+++ b/crates/radicle-node/src/worker/fetch.rs
@@ -15,8 +15,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;
@@ -119,15 +118,6 @@ 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_to_default_branch() {
- Ok(()) => {
- log::trace!(target: "worker", "Set HEAD successfully");
- }
- Err(RepositoryError::Quorum(e)) => {
- log::warn!(target: "worker", "Fetch could not set HEAD: {e}")
- }
- Err(e) => return Err(e.into()),
- }
let canonical = match set_canonical_refs(&repo, &applied) {
Ok(updates) => updates.unwrap_or_default(),
@@ -366,14 +356,45 @@ fn set_canonical_refs(
repo: &Repository,
applied: &Applied,
) -> Result<Option<UpdatedCanonicalRefs>, error::Canonical> {
+ const LOG_MESSAGE: &str = "set-canonical-reference from fetch (radicle)";
+
let identity = repo.identity()?;
- let rules = identity
- .canonical_refs_or_default(|| {
- let rule = identity.doc().default_branch_rule()?;
- Ok::<_, CanonicalRefsError>(CanonicalRefs::from_iter([rule]))
- })?
- .rules()
- .clone();
+
+ let crefs = identity.canonical_refs_or_default(|| {
+ let doc = identity.doc();
+ let mut crefs = CanonicalRefs::default();
+ crefs.extend([doc.default_branch_rule()?]);
+ crefs.extend([doc.default_branch_symbolic()?]);
+
+ Ok::<_, CanonicalRefsError>(crefs)
+ })?;
+
+ log::info!(
+ target: "worker",
+ "Attempting to set canonical symbolic references!"
+ );
+ for (name, target) in crefs.symbolic().into_iter() {
+ log::info!(
+ target: "worker",
+ "Attempting to set canonical symbolic reference {name}->{target}"
+ );
+ if let Err(e) =
+ repo.backend
+ .reference_symbolic(name.as_str(), target.as_str(), true, LOG_MESSAGE)
+ {
+ log::warn!(
+ target: "worker",
+ "Failed to set canonical symbolic reference {name}->{target}: {e}"
+ );
+ } else {
+ log::info!(
+ target: "worker",
+ "Set canonical symbolic reference {name}->{target}"
+ );
+ }
+ }
+
+ let rules = crefs.rules().clone();
let mut updated_refs = UpdatedCanonicalRefs::default();
let refnames = applied
@@ -415,12 +436,10 @@ fn set_canonical_refs(
refname, object, ..
}) => {
let oid = object.id();
- if let Err(e) = repo.backend.reference(
- refname.clone().as_str(),
- *oid,
- true,
- "set-canonical-reference from fetch (radicle)",
- ) {
+ if let Err(e) =
+ repo.backend
+ .reference(refname.clone().as_str(), *oid, 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 f42edb9b..cfa4416a 100644
--- a/crates/radicle-remote-helper/src/push.rs
+++ b/crates/radicle-remote-helper/src/push.rs
@@ -285,7 +285,9 @@ pub fn run(
}
let delegates = stored.delegates()?;
let identity = stored.identity()?;
- let canonical_ref = identity.default_branch()?;
+ let canonical_ref = identity
+ .default_branch()
+ .map_err(CanonicalRefsError::from)?;
let mut set_canonical_refs: Vec<(git::Qualified, git::canonical::Object)> =
Vec::with_capacity(specs.len());
@@ -344,8 +346,10 @@ 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]))
+ let mut crefs = CanonicalRefs::default();
+ crefs.extend([identity.doc().default_branch_rule()?]);
+ crefs.extend([identity.doc().default_branch_symbolic()?]);
+ Ok::<_, CanonicalRefsError>(crefs)
})?;
let rules = crefs.rules();
let me = Did::from(nid);
@@ -400,6 +404,8 @@ pub fn run(
if !ok.is_empty() {
let _ = stored.sign_refs(&signer)?;
+ // TODO: Set all canonical symbolic refs.
+
for (refname, object) in &set_canonical_refs {
let oid = object.id();
let kind = object.object_type();
diff --git a/crates/radicle/src/git/canonical.rs b/crates/radicle/src/git/canonical.rs
index aab1d20e..d8e73667 100644
--- a/crates/radicle/src/git/canonical.rs
+++ b/crates/radicle/src/git/canonical.rs
@@ -11,6 +11,7 @@ mod voting;
pub mod effects;
pub mod rules;
+pub mod symbolic;
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
diff --git a/crates/radicle/src/git/canonical/symbolic.rs b/crates/radicle/src/git/canonical/symbolic.rs
new file mode 100644
index 00000000..e89e7832
--- /dev/null
+++ b/crates/radicle/src/git/canonical/symbolic.rs
@@ -0,0 +1,46 @@
+// TODO(lorenz): Forbid linking to/from `refs/rad`.
+
+use std::collections::BTreeMap;
+
+use serde::{Deserialize, Serialize};
+
+use crate::git::fmt::{Qualified, RefStr, RefString};
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RawSymbolicRefs {
+ #[serde(flatten)]
+ pub symbolic_refs: BTreeMap<RefString, Qualified<'static>>,
+}
+
+impl<'a> RawSymbolicRefs {
+ pub fn get(&self, key: &RefStr) -> Option<&Qualified<'a>> {
+ self.symbolic_refs.get(key)
+ }
+}
+
+pub type SymbolicRefs = RawSymbolicRefs;
+
+impl SymbolicRefs {
+ pub fn from_raw(raw: RawSymbolicRefs) -> Self {
+ raw
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.symbolic_refs.is_empty()
+ }
+}
+
+impl Extend<(RefString, Qualified<'static>)> for SymbolicRefs {
+ fn extend<T: IntoIterator<Item = (RefString, Qualified<'static>)>>(&mut self, iter: T) {
+ self.symbolic_refs.extend(iter)
+ }
+}
+
+impl<'a> IntoIterator for &'a SymbolicRefs {
+ type Item = (&'a RefString, &'a Qualified<'static>);
+ type IntoIter = std::collections::btree_map::Iter<'a, RefString, Qualified<'static>>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.symbolic_refs.iter()
+ }
+}
diff --git a/crates/radicle/src/identity/crefs.rs b/crates/radicle/src/identity/crefs.rs
index 939d5b16..b561fbb1 100644
--- a/crates/radicle/src/identity/crefs.rs
+++ b/crates/radicle/src/identity/crefs.rs
@@ -1,10 +1,14 @@
use serde::{Deserialize, Serialize};
+use crate::git::{Qualified, RefString};
+
use crate::git::canonical::{
rules::{self, RawRules, Rules, ValidationError},
ValidRule,
};
+use crate::git::canonical::symbolic::{RawSymbolicRefs, SymbolicRefs};
+
use super::doc::{Delegates, Payload};
/// Implemented by any data type or store that can return [`CanonicalRefs`] and
@@ -47,12 +51,18 @@ pub trait GetCanonicalRefs {
#[serde(rename_all = "camelCase")]
pub struct RawCanonicalRefs {
rules: RawRules,
+
+ #[serde(default)]
+ symbolic: RawSymbolicRefs,
}
impl RawCanonicalRefs {
/// Construct a new [`RawCanonicalRefs`] from a set of [`RawRules`].
pub fn new(rules: RawRules) -> Self {
- Self { rules }
+ Self {
+ rules,
+ symbolic: RawSymbolicRefs::default(),
+ }
}
/// Return the [`RawRules`].
@@ -60,6 +70,11 @@ impl RawCanonicalRefs {
&self.rules
}
+ /// Return the [`RawSymbolicRefs`].
+ pub fn raw_symbolic(&self) -> &RawSymbolicRefs {
+ &self.symbolic
+ }
+
/// Validate the [`RawCanonicalRefs`] into a set of [`CanonicalRefs`].
pub fn try_into_canonical_refs<R>(
self,
@@ -69,7 +84,8 @@ impl RawCanonicalRefs {
R: Fn() -> Delegates,
{
let rules = Rules::from_raw(self.rules, resolve)?;
- Ok(CanonicalRefs::new(rules))
+ let symbolic = SymbolicRefs::from_raw(self.symbolic);
+ Ok(CanonicalRefs::new(rules, symbolic))
}
}
@@ -77,27 +93,28 @@ 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(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 }
+ pub fn new(rules: Rules, symbolic: SymbolicRefs) -> Self {
+ CanonicalRefs { rules, symbolic }
}
/// Return the [`Rules`].
pub fn rules(&self) -> &Rules {
&self.rules
}
-}
-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))
+ /// Return the [`SymbolicRefs`].
+ pub fn symbolic(&self) -> &SymbolicRefs {
+ &self.symbolic
}
}
@@ -107,6 +124,12 @@ impl Extend<(rules::Pattern, ValidRule)> for CanonicalRefs {
}
}
+impl Extend<(RefString, Qualified<'static>)> for CanonicalRefs {
+ fn extend<T: IntoIterator<Item = (RefString, Qualified<'static>)>>(&mut self, iter: T) {
+ self.symbolic.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 22096f31..a5cf0143 100644
--- a/crates/radicle/src/identity/doc.rs
+++ b/crates/radicle/src/identity/doc.rs
@@ -22,6 +22,7 @@ use crate::crypto;
use crate::crypto::Signature;
use crate::git;
use crate::git::canonical::rules;
+use crate::identity::crefs::GetCanonicalRefs;
use crate::identity::{project::Project, Did};
use crate::node::device::Device;
use crate::storage;
@@ -80,7 +81,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),
}
@@ -741,14 +742,25 @@ impl Doc {
}
/// Gets the qualified reference name of the default branch,
- /// according to the project payload in this document.
- pub fn default_branch(&self) -> Result<git::Qualified, PayloadError> {
- Ok(git::refs::branch(self.project()?.default_branch()))
+ /// according to the cref or project payload in this document.
+ pub fn default_branch(&self) -> Result<git::Qualified, DefaultBranchError> {
+ // Do not directly use `default_branch_symbolic`,
+ // because this eagerly accesses the project payload.
+
+ // Attempt to resolve the default branch from crefs payload via symbolic reference `HEAD`.
+ if let Ok(Some(crefs)) = self.canonical_refs() {
+ if let Some(target) = crefs.symbolic().get(&git::refname!("HEAD")) {
+ return Ok(target.to_owned());
+ }
+ }
+
+ // Attempt to resolve the default branch from project payload.
+ self.default_branch_symbolic().map(|(_head, target)| target)
}
pub fn default_branch_rule(
&self,
- ) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
+ ) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchError> {
let pattern = rules::Pattern::exact(self.project()?.default_branch());
let rule = rules::Rule::new(
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
@@ -757,6 +769,15 @@ impl Doc {
Ok((pattern, rule))
}
+ pub fn default_branch_symbolic(
+ &self,
+ ) -> Result<(git::RefString, git::Qualified<'static>), DefaultBranchError> {
+ Ok((
+ git::refname!("HEAD"),
+ git::refs::branch(self.project()?.default_branch()),
+ ))
+ }
+
/// Return the associated [`Visibility`] of this document.
pub fn visibility(&self) -> &Visibility {
&self.visibility
@@ -921,28 +942,22 @@ pub enum CanonicalRefsError {
#[error(transparent)]
CanonicalRefs(#[from] rules::ValidationError),
#[error(transparent)]
- DefaultBranch(#[from] DefaultBranchRuleError),
+ DefaultBranch(#[from] DefaultBranchError),
}
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(crefs) = self.raw_canonical_refs()? else {
+ return Ok(None);
+ };
+
+ let mut crefs = crefs.try_into_canonical_refs(&mut || self.delegates.clone())?;
+ crefs.extend([self.default_branch_rule()?]);
+ crefs.extend([self.default_branch_symbolic()?]);
+
+ Ok(Some(crefs))
}
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
@@ -1119,6 +1134,8 @@ mod test {
visibility: Visibility::Public,
}
);
+
+ assert_eq!(verified.default_branch().unwrap().as_str(), "refs/heads/master")
}
#[test]
diff --git a/crates/radicle/src/identity/doc/update.rs b/crates/radicle/src/identity/doc/update.rs
index 5fd2755c..1eab6f78 100644
--- a/crates/radicle/src/identity/doc/update.rs
+++ b/crates/radicle/src/identity/doc/update.rs
@@ -215,7 +215,14 @@ 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.raw_symbolic().get(&git::refname!("HEAD")) {
+ return Err(error::DocVerification::DisallowDefaultBranchSymbolic {
+ symbolic: symbolic.clone(),
+ default,
+ });
}
}
_ => { /* we validate below */ }
@@ -324,7 +331,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 a9e664f4..073aead4 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::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: git::Qualified<'static>,
+ default: git::Qualified<'static>,
+ },
}
#[derive(Clone, Debug)]
diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs
index 91bba1e4..20924395 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)]
@@ -523,9 +525,10 @@ pub trait ReadRepository: Sized + ValidateRepository {
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.
+ /// according to the crefs payload in the identity document.
fn default_branch(&self) -> Result<Qualified, RepositoryError> {
- Ok(self.identity_doc()?.default_branch()?.to_owned())
+ let (qualified, _oid) = self.canonical_head()?;
+ Ok(qualified.to_owned())
}
/// Compute the canonical head of this repository.
diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs
index cd710890..732513a4 100644
--- a/crates/radicle/src/storage/git.rs
+++ b/crates/radicle/src/storage/git.rs
@@ -786,13 +786,21 @@ 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(|| {
+ let mut crefs = CanonicalRefs::default();
+ crefs.extend([doc.default_branch_rule()?]);
+ crefs.extend([doc.default_branch_symbolic()?]);
+
+ Ok::<_, RepositoryError>(crefs)
+ })?;
+
+ let Some(refname) = crefs.symbolic().get(&git::refname!("HEAD")) else {
+ return Err(RepositoryError::MissingBranchSymbolic);
};
+
+ let refname = refname.to_owned();
+
Ok(crefs
.rules()
.canonical(refname, self)
@@ -880,7 +888,7 @@ 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()?;
+ let (branch_ref, _) = self.canonical_head()?;
match self.raw().find_reference(head_ref.as_str()) {
Ok(mut head_ref) => {
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 cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name bffb0275-43a9-497f-a492-499754f47148 -v /opt/radcis/ci.rad.levitte.org/cci/state/bffb0275-43a9-497f-a492-499754f47148/s:/bffb0275-43a9-497f-a492-499754f47148/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/bffb0275-43a9-497f-a492-499754f47148/w:/bffb0275-43a9-497f-a492-499754f47148/w -w /bffb0275-43a9-497f-a492-499754f47148/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /bffb0275-43a9-497f-a492-499754f47148/s/script.sh
time="2025-09-16T17:34:15+02:00" level=error msg="User-selected graph driver \"overlay\" overwritten by graph driver \"vfs\" from database - delete libpod local files (\"/opt/radcis/ci.rad.levitte.org/.local/share/containers/storage\") to resolve. May prevent use of images created by other tools"
time="2025-09-16T17:34:15+02:00" level=error msg="User-selected graph driver \"overlay\" overwritten by graph driver \"vfs\" from database - delete libpod local files (\"/opt/radcis/ci.rad.levitte.org/.local/share/containers/storage\") to resolve. May prevent use of images created by other tools"
+ export 'RUSTDOCFLAGS=-D warnings'
+ RUSTDOCFLAGS='-D warnings'
+ cargo --version
info: syncing channel updates for '1.88-x86_64-unknown-linux-gnu'
info: latest update on 2025-06-26, rust version 1.88.0 (6b00bc388 2025-06-23)
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.88.0 (873a06493 2025-05-10)
+ rustc --version
rustc 1.88.0 (6b00bc388 2025-06-23)
+ cargo fmt --check
Diff in /bffb0275-43a9-497f-a492-499754f47148/w/crates/radicle/src/identity/doc.rs:1135:
}
);
- assert_eq!(verified.default_branch().unwrap().as_str(), "refs/heads/master")
+ assert_eq!(
+ verified.default_branch().unwrap().as_str(),
+ "refs/heads/master"
+ )
}
#[test]
Exit code: 1
{
"response": "finished",
"result": "failure"
}