Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle/crefs: Support symrefs
✗ CI failure Lorenz Leutgeb committed 7 months ago
commit 0d8ccb51101df44aa643c14e421f0a8f5a1aca53
parent dcb39974eb51db8214787d94e3db87ba64cd9466
3 failed (3 total) View logs
10 files changed +205 -70
modified 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}"
modified 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();
modified 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};

added 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()
+
    }
+
}
modified 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 {
modified 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]
modified 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"
        )
modified 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)]
modified 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.
modified 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) => {