Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Set repository canonical refs
Alexis Sellier committed 3 years ago
commit 3bff9a9dc11764d9c5b5df18bec9b6e5be7c857f
parent 10d8519101e34475c24ecb1b870ad7e29feb74b2
7 files changed +135 -38
modified radicle-remote-helper/src/lib.rs
@@ -8,7 +8,7 @@ use thiserror::Error;
use radicle::crypto::{PublicKey, Signer};
use radicle::node::Handle;
use radicle::ssh;
-
use radicle::storage::{ReadRepository, WriteStorage};
+
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};

/// The service invoked by git on the remote repository, during a push.
const GIT_RECEIVE_PACK: &str = "git-receive-pack";
@@ -180,6 +180,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
                if child.wait()?.success() {
                    if *service == GIT_RECEIVE_PACK {
                        profile.storage.sign_refs(&proj, &profile.signer)?;
+
                        proj.set_head()?;
                        // Connect to local node and announce refs to the network.
                        // If our node is not running, we simply skip this step, as the
                        // refs will be announced eventually, when the node restarts.
modified radicle/src/git.rs
@@ -118,24 +118,47 @@ pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError>

    let refs = remote.list()?;
    for r in refs {
-
        let (id, refname) = parse_ref::<PublicKey>(r.name())?;
-
        let entry = remotes.entry(id).or_insert_with(Refs::default);
-

-
        entry.insert(refname.into(), r.oid().into());
+
        // Skip the `HEAD` reference, as it is untrusted.
+
        if r.name() == "HEAD" {
+
            continue;
+
        }
+
        // Nb. skip refs that don't have a public key namespace.
+
        if let (Some(id), refname) = parse_ref::<PublicKey>(r.name())? {
+
            let entry = remotes.entry(id).or_insert_with(Refs::default);
+
            entry.insert(refname.into(), r.oid().into());
+
        }
    }

    Ok(remotes)
}

-
/// Parse a ref string.
-
pub fn parse_ref<T>(s: &str) -> Result<(T, format::Qualified), RefError>
+
/// Parse a ref string. Returns an error if it isn't namespaced.
+
pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, format::Qualified), RefError>
+
where
+
    T: FromStr,
+
    T::Err: std::error::Error + Send + Sync + 'static,
+
{
+
    match parse_ref::<T>(s) {
+
        Ok((None, refname)) => Err(RefError::MissingNamespace(refname.to_ref_string())),
+
        Ok((Some(t), r)) => Ok((t, r)),
+
        Err(err) => Err(err),
+
    }
+
}
+

+
/// Parse a ref string. Optionally returns a namespace.
+
pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, format::Qualified), RefError>
where
    T: FromStr,
    T::Err: std::error::Error + Send + Sync + 'static,
{
    let input = format::RefStr::try_from_str(s)?;
    match input.to_namespaced() {
-
        None => Err(RefError::MissingNamespace(input.to_owned())),
+
        None => {
+
            let refname = Qualified::from_refstr(input)
+
                .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
+

+
            Ok((None, refname))
+
        }
        Some(ns) => {
            let id = ns
                .namespace()
@@ -146,7 +169,8 @@ where
                    err: Box::new(err),
                })?;
            let rest = ns.strip_namespace();
-
            Ok((id, rest))
+

+
            Ok((Some(id), rest))
        }
    }
}
modified radicle/src/identity/project.rs
@@ -509,7 +509,7 @@ mod test {
        let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();

        let err = Doc::<Unverified>::head(&remote, &repo).unwrap_err();
-
        assert!(dbg!(err).is_not_found());
+
        assert!(err.is_not_found());

        let err = Doc::load_at(oid.into(), &repo).unwrap_err();
        assert!(err.is_not_found());
modified radicle/src/rad.rs
@@ -3,7 +3,6 @@ use std::io;
use std::path::Path;
use std::str::FromStr;

-
use git_ref_format::{lit, Qualified};
use once_cell::sync::Lazy;
use thiserror::Error;

@@ -23,6 +22,8 @@ pub static REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad")
pub enum InitError {
    #[error("doc: {0}")]
    Doc(#[from] identity::project::DocError),
+
    #[error("project: {0}")]
+
    Project(#[from] storage::git::ProjectError),
    #[error("doc: {0}")]
    DocVerification(#[from] identity::project::VerificationError),
    #[error("git: {0}")]
@@ -76,6 +77,7 @@ pub fn init<G: Signer, S: storage::WriteStorage>(
    git::configure_remote(repo, &REMOTE_NAME, &url)?;
    git::push(repo, &REMOTE_NAME, pk, [(&default_branch, &default_branch)])?;
    let signed = storage.sign_refs(&project, signer)?;
+
    let _head = project.set_head()?;

    Ok((id, signed))
}
@@ -158,27 +160,15 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
    let me = signer.public_key();
    let repository = storage.repository(proj)?;
    let (canonical_id, project) = repository.project_identity()?;
+
    let (canonical_head, _) = repository.head()?;
    let raw = repository.raw();
-
    // TODO: Test the fork functions in isolation.
-
    // TODO: Move to function on `Repository`.
-
    let canonical_head = {
-
        let mut heads = Vec::new();
-
        for delegate in project.delegates.iter() {
-
            let refname = Qualified::from(lit::refs_heads(&project.default_branch));
-
            let r = repository.reference_oid(&delegate.id, &refname)?.into();
-
            heads.push(r);
-
        }
-

-
        match heads.as_slice() {
-
            [head] => Ok(*head),
-
            // FIXME: This branch is not tested.
-
            heads => raw.merge_base_many(heads),
-
        }
-
    }?;
+

+
    // TODO: We should only get the project HEAD once we've stored the canonical identity
+
    // branch on disk. This way it can use what we stored, instead of recomputing it.

    raw.reference(
        &git::refs::storage::branch(me, &project.default_branch),
-
        canonical_head,
+
        *canonical_head,
        false,
        &format!("creating default branch for {me}"),
    )?;
modified radicle/src/storage.rs
@@ -59,6 +59,9 @@ pub enum FetchError {
    Verify(#[from] git::VerifyError),
    #[error(transparent)]
    Storage(#[from] Error),
+
    // TODO: This should wrap a more specific error.
+
    #[error("repository head: {0}")]
+
    SetHead(#[from] ProjectError),
}

pub type RemoteId = PublicKey;
@@ -229,6 +232,7 @@ pub trait WriteStorage: ReadStorage {
    type Repository: WriteRepository;

    fn repository(&self, proj: Id) -> Result<Self::Repository, Error>;
+
    // TODO: Move this to `WriteRepository`.
    fn sign_refs<G: Signer>(
        &self,
        repository: &Self::Repository,
@@ -246,6 +250,11 @@ pub trait ReadRepository {

    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;

+
    /// Get the canonical head of this repository.
+
    ///
+
    /// Returns the [`Oid`] as well as the qualified reference name.
+
    fn head(&self) -> Result<(Oid, Qualified), ProjectError>;
+

    /// Get the `reference` for the given `remote`.
    ///
    /// Returns `None` is the reference did not exist.
@@ -276,6 +285,7 @@ pub trait ReadRepository {

pub trait WriteRepository: ReadRepository {
    fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
+
    fn set_head(&self) -> Result<Oid, ProjectError>;
    fn raw(&self) -> &git2::Repository;
}

modified radicle/src/storage/git.rs
@@ -28,6 +28,7 @@ pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
    Lazy::new(|| refspec::pattern!("refs/namespaces/*/radicle/signature"));

+
// TODO: Is this is the wrong place for this type?
#[derive(Error, Debug)]
pub enum ProjectError {
    #[error("identity branches diverge from each other")]
@@ -228,12 +229,9 @@ impl Repository {
        Ok(Self { id, backend })
    }

-
    pub fn head(&self) -> Result<git2::Commit, git2::Error> {
-
        // TODO: Find longest history, get document and get head.
-
        // Perhaps we should even set a local `HEAD` or at least `refs/heads/master`
-
        todo!();
-
    }
-

+
    /// Verify all references in the repository, checking that they are signed
+
    /// as part of 'sigrefs'. Also verify that no signed reference is missing
+
    /// from the repository.
    pub fn verify(&self) -> Result<(), VerifyError> {
        let mut remotes: HashMap<RemoteId, Refs> = self
            .remotes()?
@@ -243,15 +241,32 @@ impl Repository {
            })
            .collect::<Result<_, VerifyError>>()?;

+
        // TODO: We could create a higher level `references` method that skips
+
        // canonical/unverifiable refs.
        for r in self.backend.references()? {
            let r = r?;
+

            let name = r.name().ok_or(VerifyError::InvalidRef)?;
-
            let oid = r.target().ok_or(VerifyError::InvalidRef)?;
+
            let oid = if let Some(oid) = r.target() {
+
                oid
+
            } else {
+
                // Ignore symbolic refs, eg. `HEAD`.
+
                continue;
+
            };
+

            let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
+
            let remote_id = if let Some(remote_id) = remote_id {
+
                remote_id
+
            } else {
+
                // Ignore refs that aren't part of a namespace, eg. canonical refs.
+
                continue;
+
            };

            if refname == *refs::SIGNATURE_REF {
+
                // Ignore the signed-refs reference, as this is what we're verifying.
                continue;
            }
+

            let remote = remotes
                .get_mut(&remote_id)
                .ok_or(VerifyError::InvalidRemote(remote_id))?;
@@ -358,7 +373,7 @@ impl Repository {
            |reference| -> Result<RemoteId, refs::Error> {
                let r = reference?;
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
-
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
+
                let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;

                Ok(id)
            },
@@ -376,7 +391,7 @@ impl Repository {
            |reference| -> Result<_, _> {
                let r = reference?;
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
-
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
+
                let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
                let remote = self.remote(&id)?;

                Ok((id, remote))
@@ -440,6 +455,7 @@ impl ReadRepository for Repository {
        remote: &RemoteId,
        reference: &git::Qualified,
    ) -> Result<Oid, git::Error> {
+
        // TODO: Use native git2 function for this.
        let oid = self
            .reference(remote, reference)?
            .target()
@@ -486,6 +502,31 @@ impl ReadRepository for Repository {
    fn project_identity(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
        Repository::project(self)
    }
+

+
    fn head(&self) -> Result<(Oid, Qualified), ProjectError> {
+
        // TODO: We shouldn't need to re-construct the history here; we should use the cached
+
        // document head of the "trusted" (local) user for this project.
+
        // In the `fork` function for example, we call Repository::project_identity again,
+
        // This should only be necessary once.
+
        let (_, project) = self.project_identity()?;
+
        let branch_ref = Qualified::from(lit::refs_heads(&project.default_branch));
+
        let raw = self.raw();
+

+
        let mut heads = Vec::new();
+
        for delegate in project.delegates.iter() {
+
            let r = self.reference_oid(&delegate.id, &branch_ref)?.into();
+

+
            heads.push(r);
+
        }
+

+
        let oid = match heads.as_slice() {
+
            [head] => Ok(*head),
+
            // FIXME: This branch is not tested.
+
            heads => raw.merge_base_many(heads),
+
        }?;
+

+
        Ok((oid.into(), branch_ref))
+
    }
}

impl WriteRepository for Repository {
@@ -598,10 +639,27 @@ impl WriteRepository for Repository {
            // Fetch from the staging copy into the canonical repo.
            remote.fetch(refs, Some(&mut opts), None)?;
        }
+
        // Set repository HEAD for git cloning support.
+
        self.set_head()?;

        Ok(updates)
    }

+
    fn set_head(&self) -> Result<Oid, ProjectError> {
+
        let head_ref = refname!("HEAD");
+
        let (head, branch_ref) = self.head()?;
+

+
        log::debug!("Setting ref {:?} -> {:?}", &branch_ref, head);
+
        self.raw()
+
            .reference(&branch_ref, *head, true, "set-local-branch (radicle)")?;
+

+
        log::debug!("Setting ref {:?} -> {:?}", head_ref, branch_ref);
+
        self.raw()
+
            .reference_symbolic(&head_ref, &branch_ref, true, "set-head (radicle)")?;
+

+
        Ok(head)
+
    }
+

    fn raw(&self) -> &git2::Repository {
        &self.backend
    }
@@ -788,6 +846,7 @@ mod tests {
            .id();
        git::push(&proj_repo, "rad", alice_id, [(&refname, &refname)]).unwrap();
        alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
+
        alice_proj_storage.set_head().unwrap();

        // Have Bob fetch Alice's new commit.
        let updates = bob.repository(proj_id).unwrap().fetch(&alice_url).unwrap();
@@ -887,7 +946,11 @@ mod tests {
            target
                .remote_anonymous(&format!("rad://{}", proj))
                .unwrap()
-
                .fetch(&["refs/*:refs/*"], Some(&mut opts), None)
+
                .fetch(
+
                    &["refs/namespaces/*:refs/namespaces/*"],
+
                    Some(&mut opts),
+
                    None,
+
                )
                .unwrap();

            stream.shutdown(net::Shutdown::Both).unwrap();
modified radicle/src/test/storage.rs
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

+
use git_ref_format as fmt;
use git_url::Url;
use radicle_git_ext as git_ext;

@@ -85,6 +86,10 @@ impl ReadRepository for MockRepository {
        Ok(true)
    }

+
    fn head(&self) -> Result<(Oid, fmt::Qualified), ProjectError> {
+
        todo!()
+
    }
+

    fn path(&self) -> &std::path::Path {
        todo!()
    }
@@ -152,4 +157,8 @@ impl WriteRepository for MockRepository {
    fn raw(&self) -> &git2::Repository {
        todo!()
    }
+

+
    fn set_head(&self) -> Result<Oid, ProjectError> {
+
        todo!()
+
    }
}