Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: namespace peers
Fintan Halpenny committed 3 years ago
commit df15b497ab3e2333b18942da94464a9a8fdcdd6d
parent 50dd817762ae53ba45bc4eac0e308580223d8875
9 files changed +211 -131
modified Cargo.lock
@@ -216,12 +216,11 @@ dependencies = [

[[package]]
name = "crossbeam-utils"
-
version = "0.8.11"
+
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
+
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
dependencies = [
 "cfg-if",
-
 "once_cell",
]

[[package]]
@@ -287,9 +286,9 @@ dependencies = [

[[package]]
name = "ed25519-compact"
-
version = "1.0.12"
+
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c25036262e9b9c81fe4c6decb438f753f66a8f06aac5dbe9eb2b28355c85c3f5"
+
checksum = "bee9df587982575886a8682edcee11877894349a805f25629c27f63abe3e9ae8"
dependencies = [
 "ct-codecs",
 "getrandom",
@@ -417,14 +416,13 @@ dependencies = [

[[package]]
name = "iana-time-zone"
-
version = "0.1.48"
+
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0"
+
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
dependencies = [
 "android_system_properties",
 "core-foundation-sys",
 "js-sys",
-
 "once_cell",
 "wasm-bindgen",
 "winapi",
]
@@ -466,9 +464,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"

[[package]]
name = "jobserver"
-
version = "0.1.24"
+
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
+
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
dependencies = [
 "libc",
]
@@ -502,9 +500,9 @@ checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"

[[package]]
name = "libc"
-
version = "0.2.133"
+
version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
+
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"

[[package]]
name = "libgit2-sys"
@@ -641,9 +639,9 @@ dependencies = [

[[package]]
name = "olpc-cjson"
-
version = "0.1.1"
+
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "72ca49fe685014bbf124ee547da94ed7bb65a6eb9dc9c4711773c081af96a39c"
+
checksum = "87dc75cf72208cd853671c1abccc5d5d1e43b1e378dde67340ef933219a8c13c"
dependencies = [
 "serde",
 "serde_json",
@@ -670,9 +668,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"

[[package]]
name = "openssl-sys"
-
version = "0.9.75"
+
version = "0.9.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
+
checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
dependencies = [
 "autocfg",
 "cc",
@@ -728,9 +726,9 @@ dependencies = [

[[package]]
name = "proc-macro2"
-
version = "1.0.43"
+
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [
 "unicode-ident",
]
@@ -910,18 +908,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"

[[package]]
name = "serde"
-
version = "1.0.144"
+
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
-
version = "1.0.144"
+
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
 "proc-macro2",
 "quote",
@@ -1037,9 +1035,9 @@ dependencies = [

[[package]]
name = "syn"
-
version = "1.0.100"
+
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
+
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [
 "proc-macro2",
 "quote",
@@ -1062,18 +1060,18 @@ dependencies = [

[[package]]
name = "thiserror"
-
version = "1.0.35"
+
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
+
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [
 "thiserror-impl",
]

[[package]]
name = "thiserror-impl"
-
version = "1.0.35"
+
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
+
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
 "proc-macro2",
 "quote",
modified radicle-tools/src/rad-push.rs
@@ -8,7 +8,7 @@ fn main() -> anyhow::Result<()> {
    let profile = radicle::Profile::load()?;
    let (_, id) = radicle::rad::remote(&repo)?;

-
    let output = radicle::git::run(&cwd, &["push", "rad"])?;
+
    let output = radicle::git::run::<_, _, &str, &str>(&cwd, &["push", "rad"], None)?;
    println!("{}", output);

    let project = profile.storage.repository(id)?;
modified radicle/src/git.rs
@@ -18,7 +18,7 @@ pub use ext::NotFound;
pub use ext::Oid;
pub use git2 as raw;
pub use git_ref_format as fmt;
-
pub use git_ref_format::{refname, Component, Qualified, RefStr, RefString};
+
pub use git_ref_format::{lit, name, refname, Component, Namespaced, Qualified, RefStr, RefString};
pub use git_url as url;
pub use git_url::Url;
pub use radicle_git_ext as ext;
@@ -33,6 +33,14 @@ pub enum RefError {
    InvalidName(format::RefString),
    #[error("invalid ref format: {0}")]
    Format(#[from] format::Error),
+
    #[error("expected ref to begin with 'refs/namespaces' but found '{0}'")]
+
    MissingNamespace(format::RefString),
+
    #[error("ref name contains invalid namespace identifier '{name}'")]
+
    Id {
+
        name: format::RefString,
+
        #[source]
+
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
+
    },
}

#[derive(thiserror::Error, Debug)]
@@ -43,9 +51,11 @@ pub enum ListRefsError {
    InvalidRef(#[from] RefError),
}

-
impl From<&RemoteId> for RefString {
+
impl<'a> From<&RemoteId> for Component<'a> {
    fn from(id: &RemoteId) -> Self {
-
        RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings")
+
        let refstr =
+
            RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
+
        Component::from_refstring(refstr).expect("encoded public keys are valid refname components")
    }
}

@@ -56,17 +66,19 @@ pub mod refs {
    pub static IDENTITY_BRANCH: Lazy<RefString> = Lazy::new(|| refname!("radicle/id"));

    pub mod storage {
+

        use super::*;

-
        pub fn branch(remote: &RemoteId, branch: &RefStr) -> RefString {
-
            refname!("refs/remotes")
-
                .and::<RefString>(remote.into())
-
                .and(refname!("heads"))
-
                .and(branch)
+
        /// Create the [`Namespaced`] `branch` under the `remote` namespace, i.e.
+
        /// `refs/namespaces/<remote>/refs/heads/<branch>`
+
        pub fn branch<'a>(remote: &RemoteId, branch: &RefStr) -> Namespaced<'a> {
+
            Qualified::from(git_ref_format::lit::refs_heads(branch)).add_namespace(remote.into())
        }

        /// Get the branch used to track project information.
-
        pub fn id(remote: &RemoteId) -> RefString {
+
        ///
+
        /// `refs/namespaces/<remote>/refs/heads/radicle/id`
+
        pub fn id(remote: &RemoteId) -> Namespaced {
            branch(remote, &IDENTITY_BRANCH)
        }
    }
@@ -74,18 +86,22 @@ pub mod refs {
    pub mod workdir {
        use super::*;

+
        /// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
        pub fn branch(branch: &RefStr) -> RefString {
            refname!("refs/heads").join(branch)
        }

+
        /// Create a [`RefString`] that corresponds to `refs/notes/<name>`.
        pub fn note(name: &RefStr) -> RefString {
            refname!("refs/notes").join(name)
        }

+
        /// Create a [`RefString`] that corresponds to `refs/remotes/<remote>/<branch>`.
        pub fn remote_branch(remote: &RefStr, branch: &RefStr) -> RefString {
            refname!("refs/remotes").and(remote).and(branch)
        }

+
        /// Create a [`RefString`] that corresponds to `refs/tags/<branch>`.
        pub fn tag(name: &RefStr) -> RefString {
            refname!("refs/tags").join(name)
        }
@@ -105,27 +121,34 @@ pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError>
        let (id, refname) = parse_ref::<PublicKey>(r.name())?;
        let entry = remotes.entry(id).or_insert_with(Refs::default);

-
        entry.insert(refname, r.oid().into());
+
        entry.insert(refname.into(), r.oid().into());
    }

    Ok(remotes)
}

/// Parse a ref string.
-
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
+
pub fn parse_ref<T>(s: &str) -> Result<(T, format::Qualified), RefError>
+
where
+
    T: FromStr,
+
    T::Err: std::error::Error + Send + Sync + 'static,
+
{
    let input = format::RefStr::try_from_str(s)?;
-
    let suffix = input
-
        .strip_prefix(format::refname!("refs/remotes"))
-
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
-

-
    let mut components = suffix.components();
-
    let id = components
-
        .next()
-
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
-
    let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
-
    let refstr = components.collect::<format::RefString>();
-

-
    Ok((id, refstr))
+
    match input.namespaced() {
+
        None => Err(RefError::MissingNamespace(input.to_owned())),
+
        Some(ns) => {
+
            let id = ns
+
                .namespace()
+
                .as_str()
+
                .parse()
+
                .map_err(|err| RefError::Id {
+
                    name: input.to_owned(),
+
                    err: Box::new(err),
+
                })?;
+
            let rest = ns.strip_namespace();
+
            Ok((id, rest))
+
        }
+
    }
}

/// Create an initial empty commit.
@@ -158,24 +181,6 @@ pub fn commit<'a>(
    Ok(commit)
}

-
/// Push the refs to the radicle remote.
-
pub fn push(repo: &git2::Repository) -> Result<(), git2::Error> {
-
    // NOTE: This function is going away soon.
-
    #![allow(clippy::unwrap_used)]
-

-
    let mut remote = repo.find_remote("rad")?;
-
    let refspecs = remote.push_refspecs()?;
-
    let refspec = refspecs.into_iter().next().unwrap().unwrap();
-

-
    // The `git2` crate doesn't seem to support push refspecs with '*' in them,
-
    // so we manually replace it with the current branch.
-
    let head = repo.head()?;
-
    let branch = head.shorthand().unwrap();
-
    let refspec = refspec.replace('*', branch);
-

-
    remote.push::<&str>(&[&refspec], None)
-
}
-

/// Get the repository head.
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
    let head = repo.head()?.peel_to_commit()?;
@@ -201,22 +206,62 @@ pub fn write_tree<'r>(

/// Configure a repository's radicle remote.
///
-
/// Takes the repository in which to configure the remote, the name of the remote, the public
-
/// key of the remote, and the path to the remote repository on the filesystem.
+
/// The entry for this remote will be:
+
/// ```text
+
/// [remote.<name>]
+
///   url = <url>
+
///   fetch +refs/heads/*:refs/remotes/<name>/*
+
/// ```
pub fn configure_remote<'r>(
    repo: &'r git2::Repository,
-
    remote_name: &str,
-
    remote_id: &RemoteId,
-
    remote_url: &Url,
+
    name: &str,
+
    url: &Url,
) -> Result<git2::Remote<'r>, git2::Error> {
-
    let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*");
-
    let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*");
-
    let remote = repo.remote_with_fetch(remote_name, remote_url.to_string().as_str(), &fetch)?;
-
    repo.remote_add_push(remote_name, &push)?;
+
    let fetch = format!("+refs/heads/*:refs/remotes/{name}/*");
+
    let remote = repo.remote_with_fetch(name, url.to_string().as_str(), &fetch)?;

    Ok(remote)
}

+
/// Fetch from the given `remote` using the provided `namespace`.
+
///
+
/// This uses [`Command`] under the hood and is the equivalent to:
+
///
+
///  `GIT_NAMESPACE=<namespace> git fetch <remote>`
+
pub fn fetch(repo: &git2::Repository, remote: &str, namespace: &RemoteId) -> io::Result<String> {
+
    run(
+
        &repo.path(),
+
        ["fetch", remote],
+
        [("GIT_NAMESPACE", Component::from(namespace).as_str())],
+
    )
+
}
+

+
/// Push `refspecs` to the given `remote` using the provided `namespace`.
+
///
+
/// This uses [`Command`] under the hood and is the equivalent to:
+
///
+
/// `GIT_NAMESPACE=<namespace> git push <remote> [<refspecs>]`
+
pub fn push<Ref>(
+
    repo: &git2::Repository,
+
    remote: &str,
+
    namespace: &RemoteId,
+
    refspecs: impl IntoIterator<Item = (Ref, Ref)>,
+
) -> io::Result<String>
+
where
+
    Ref: AsRef<RefStr>,
+
{
+
    let mut args = vec!["push".to_owned(), remote.to_owned()];
+
    let refspecs = refspecs
+
        .into_iter()
+
        .map(|(src, dst)| format!("{}:{}", src.as_ref().as_str(), dst.as_ref().as_str()));
+
    args.extend(refspecs);
+
    run(
+
        &repo.path(),
+
        args,
+
        [("GIT_NAMESPACE", Component::from(namespace).as_str())],
+
    )
+
}
+

/// Set the upstream of the given branch to the given remote.
///
/// This writes to the `config` directly. The entry will look like the
@@ -258,11 +303,22 @@ pub fn set_upstream(
}

/// Execute a git command by spawning a child process.
-
pub fn run<P: AsRef<Path>, S: AsRef<std::ffi::OsStr>>(
+
pub fn run<P, S, K, V>(
    repo: &P,
    args: impl IntoIterator<Item = S>,
-
) -> Result<String, io::Error> {
-
    let output = Command::new("git").current_dir(repo).args(args).output()?;
+
    envs: impl IntoIterator<Item = (K, V)>,
+
) -> Result<String, io::Error>
+
where
+
    P: AsRef<Path>,
+
    S: AsRef<std::ffi::OsStr>,
+
    K: AsRef<std::ffi::OsStr>,
+
    V: AsRef<std::ffi::OsStr>,
+
{
+
    let output = Command::new("git")
+
        .current_dir(repo)
+
        .envs(envs)
+
        .args(args)
+
        .output()?;

    if output.status.success() {
        let out = if output.stdout.is_empty() {
modified radicle/src/identity/project.rs
@@ -358,8 +358,8 @@ impl Doc<Unverified> {

impl<V> Doc<V> {
    pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Oid, DocError> {
-
        let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
-
        repo.reference_oid(remote, head).map_err(DocError::from)
+
        let head = git::Qualified::from(git::lit::refs_heads(&*git::refs::IDENTITY_BRANCH));
+
        repo.reference_oid(remote, &head).map_err(DocError::from)
    }
}

modified radicle/src/rad.rs
@@ -3,6 +3,7 @@ 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;

@@ -68,20 +69,11 @@ pub fn init<G: Signer, S: storage::WriteStorage>(
        repo,
        &REMOTE_NAME,
        &default_branch,
-
        &git::refs::storage::branch(pk, &default_branch),
+
        &git::refs::workdir::branch(&default_branch),
    )?;

-
    // TODO: Note that you'll likely want to use `RemoteCallbacks` and set
-
    // `push_update_reference` to test whether all the references were pushed
-
    // successfully.
-
    git::configure_remote(repo, &REMOTE_NAME, pk, &url)?.push::<&str>(
-
        &[&format!(
-
            "{}:{}",
-
            &git::refs::workdir::branch(&default_branch),
-
            &git::refs::storage::branch(pk, &default_branch),
-
        )],
-
        None,
-
    )?;
+
    git::configure_remote(repo, &REMOTE_NAME, &url)?;
+
    git::push(repo, &REMOTE_NAME, pk, [(&default_branch, &default_branch)])?;
    let signed = storage.sign_refs(&project, signer)?;

    Ok((id, signed))
@@ -171,10 +163,8 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
    let canonical_head = {
        let mut heads = Vec::new();
        for delegate in project.delegates.iter() {
-
            let name = format!("heads/{}", &project.default_branch);
-
            let refname = git::RefString::try_from(name.as_str())?;
+
            let refname = Qualified::from(lit::refs_heads(&project.default_branch));
            let r = repository.reference_oid(&delegate.id, &refname)?.into();
-

            heads.push(r);
        }

@@ -258,6 +248,8 @@ pub fn clone_url<P: AsRef<Path>, G: Signer, S: storage::WriteStorage>(

#[derive(Error, Debug)]
pub enum CheckoutError {
+
    #[error("failed to fetch to working copy")]
+
    Fetch(#[source] io::Error),
    #[error("git: {0}")]
    Git(#[from] git2::Error),
    #[error("storage: {0}")]
@@ -289,12 +281,14 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
    let url = storage.url(&proj);

    // Configure and fetch all refs from remote.
-
    git::configure_remote(&repo, &REMOTE_NAME, remote, &url)?.fetch::<&str>(&[], None, None)?;
+
    git::configure_remote(&repo, &REMOTE_NAME, &url)?;
+
    git::fetch(&repo, &REMOTE_NAME, remote).map_err(CheckoutError::Fetch)?;

    {
        // Setup default branch.
        let remote_head_ref =
            git::refs::workdir::remote_branch(&REMOTE_NAME, &project.default_branch);
+

        let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
        let _ = repo.branch(&project.default_branch, &remote_head_commit, true)?;

@@ -303,7 +297,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
            &repo,
            &REMOTE_NAME,
            &project.default_branch,
-
            &git::refs::storage::branch(remote, &project.default_branch),
+
            &git::refs::workdir::branch(&project.default_branch),
        )?;
    }

modified radicle/src/storage.rs
@@ -17,7 +17,7 @@ use crate::crypto;
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
use crate::git::ext as git_ext;
use crate::git::Url;
-
use crate::git::{RefError, RefStr, RefString};
+
use crate::git::{Qualified, RefError, RefString};
use crate::identity;
use crate::identity::{Id, IdError};
use crate::storage::refs::Refs;
@@ -54,6 +54,8 @@ pub enum FetchError {
    Git(#[from] git2::Error),
    #[error("i/o: {0}")]
    Io(#[from] io::Error),
+
    #[error(transparent)]
+
    Refs(#[from] refs::Error),
    #[error("verify: {0}")]
    Verify(#[from] git::VerifyError),
    #[error(transparent)]
@@ -174,13 +176,16 @@ pub struct Remote<V> {
    pub id: PublicKey,
    /// Git references published under this remote, and their hashes.
    pub refs: SignedRefs<V>,
-
    /// Whether this remote is of a project delegate.
+
    /// Whether this remote is a delegate for the project.
    pub delegate: bool,
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
    verified: PhantomData<V>,
}

impl<V> Remote<V> {
+
    // TODO(finto): This function seems out of place in the API for a couple of reasons:
+
    // * The SignedRefs aren't guaranteed to be by the `id`
+
    // * I could write `Remote::<Verified>::new(id, refs) and because of the above, it's a LIE
    pub fn new(id: PublicKey, refs: impl Into<SignedRefs<V>>) -> Self {
        Self {
            id,
@@ -239,17 +244,34 @@ pub trait WriteStorage: ReadStorage {
}

pub trait ReadRepository {
+
    /// Returns `true` if there are no references in the repository.
    fn is_empty(&self) -> Result<bool, git2::Error>;
+

+
    /// The [`Path`] to the git repository.
    fn path(&self) -> &Path;
+

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

+
    /// Get the `reference` for the given `remote`.
+
    ///
+
    /// Returns `None` is the reference did not exist.
    fn reference(
        &self,
        remote: &RemoteId,
-
        reference: &RefStr,
+
        reference: &Qualified,
    ) -> Result<git2::Reference, git_ext::Error>;
+

+
    /// Get the [`git2::Commit`] found using its `oid`.
+
    ///
+
    /// Returns `None` if the commit did not exist.
    fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error>;
+

    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
-
    fn reference_oid(&self, remote: &RemoteId, reference: &RefStr) -> Result<Oid, git_ext::Error>;
+
    fn reference_oid(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &Qualified,
+
    ) -> Result<Oid, git_ext::Error>;
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
    fn remotes(&self) -> Result<Remotes<Verified>, refs::Error>;
modified radicle/src/storage/git.rs
@@ -24,9 +24,9 @@ pub use crate::git::*;
use super::{RefUpdate, RemoteId};

pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/remotes/*"));
+
    Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
+
    Lazy::new(|| refspec::pattern!("refs/namespaces/*/radicle/signature"));

#[derive(Error, Debug)]
pub enum ProjectError {
@@ -254,6 +254,7 @@ impl Repository {
            let remote = remotes
                .get_mut(&remote_id)
                .ok_or(VerifyError::InvalidRemote(remote_id))?;
+
            let refname = RefString::from(refname);
            let signed_oid = remote
                .remove(&refname)
                .ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
@@ -396,8 +397,7 @@ impl Repository {

impl ReadRepository for Repository {
    fn is_empty(&self) -> Result<bool, git2::Error> {
-
        let some = self.remotes()?.next().is_some();
-
        Ok(!some)
+
        Ok(self.remotes()?.next().is_none())
    }

    fn path(&self) -> &Path {
@@ -415,11 +415,9 @@ impl ReadRepository for Repository {
    fn reference(
        &self,
        remote: &RemoteId,
-
        name: &git::RefStr,
+
        name: &git::Qualified,
    ) -> Result<git2::Reference, git::Error> {
-
        let name = name.strip_prefix(git::refname!("refs")).unwrap_or(name);
-
        let name = format!("refs/remotes/{remote}/{name}");
-

+
        let name = name.add_namespace(remote.into());
        self.backend.find_reference(&name).map_err(git::Error::from)
    }

@@ -436,7 +434,11 @@ impl ReadRepository for Repository {
        Ok(revwalk)
    }

-
    fn reference_oid(&self, remote: &RemoteId, reference: &git::RefStr) -> Result<Oid, git::Error> {
+
    fn reference_oid(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &git::Qualified,
+
    ) -> Result<Oid, git::Error> {
        let oid = self
            .reference(remote, reference)?
            .target()
@@ -454,7 +456,7 @@ impl ReadRepository for Repository {
        // TODO: Only return known refs, eg. heads/ rad/ tags/ etc..
        let entries = self
            .backend
-
            .references_glob(format!("refs/remotes/{remote}/*").as_str())?;
+
            .references_glob(format!("refs/namespaces/{remote}/*").as_str())?;
        let mut refs = BTreeMap::new();

        for e in entries {
@@ -463,7 +465,7 @@ impl ReadRepository for Repository {
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
            let oid = e.target().ok_or(Error::InvalidRef)?;

-
            refs.insert(refname, oid.into());
+
            refs.insert(refname.into(), oid.into());
        }
        Ok(refs.into())
    }
@@ -523,7 +525,7 @@ impl WriteRepository for Repository {
        //     local <- git-fetch -- staging             # fetch from staging copy
        //
        let url = url.to_string();
-
        let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
+
        let refs: &[&str] = &["refs/namespaces/*:refs/namespaces/*"];
        let mut updates = Vec::new();
        let mut callbacks = git2::RemoteCallbacks::new();
        let tempdir = tempfile::tempdir()?;
@@ -692,7 +694,8 @@ mod tests {

        // Strip the remote refs of sigrefs so we can compare them.
        for remote in refs.values_mut() {
-
            remote.remove(&*SIGNATURE_REF).unwrap();
+
            let sigref = (*SIGNATURE_REF).to_ref_string();
+
            remote.remove(&sigref).unwrap();
        }

        let remotes = remotes
@@ -713,7 +716,7 @@ mod tests {
        let proj = *inventory.first().unwrap();
        let repo = alice.repository(proj).unwrap();
        let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
-
        let refname = git::refname!("heads/master");
+
        let refname = Qualified::from_refstr(git::refname!("refs/heads/master")).unwrap();

        // Have Bob fetch Alice's refs.
        let updates = bob
@@ -735,7 +738,7 @@ mod tests {
        for update in updates {
            assert_matches!(
                update,
-
                RefUpdate::Created { name, .. } if name.starts_with("refs/remotes")
+
                RefUpdate::Created { name, .. } if name.starts_with("refs/namespaces")
            );
        }

@@ -762,7 +765,7 @@ mod tests {
        let (proj_id, _, proj_repo, alice_head) =
            fixtures::project(tmp.path().join("alice/project"), &alice, &alice_signer).unwrap();

-
        let refname = git::refname!("refs/heads/master");
+
        let refname = Qualified::from_refstr(git::refname!("refs/heads/master")).unwrap();
        let alice_url = git::Url {
            scheme: git_url::Scheme::File,
            path: paths::repository(&alice, &proj_id)
@@ -782,7 +785,7 @@ mod tests {
        let alice_head = git::commit(&proj_repo, &alice_head, &refname, "Making changes", "Alice")
            .unwrap()
            .id();
-
        git::push(&proj_repo).unwrap();
+
        git::push(&proj_repo, "rad", alice_id, [(&refname, &refname)]).unwrap();
        alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();

        // Have Bob fetch Alice's new commit.
@@ -894,9 +897,9 @@ mod tests {
        assert_eq!(
            updates,
            vec![
-
                format!("refs/remotes/{remote}/heads/master"),
-
                format!("refs/remotes/{remote}/heads/radicle/id"),
-
                format!("refs/remotes/{remote}/radicle/signature")
+
                format!("refs/namespaces/{remote}/refs/heads/master"),
+
                format!("refs/namespaces/{remote}/refs/heads/radicle/id"),
+
                format!("refs/namespaces/{remote}/refs/radicle/signature")
            ]
        );
    }
@@ -928,7 +931,8 @@ mod tests {
        let mut unsigned = project.references(&alice).unwrap();

        // The signed refs doesn't contain the signature ref itself.
-
        unsigned.remove(&*SIGNATURE_REF).unwrap();
+
        let sigref = (*SIGNATURE_REF).to_ref_string();
+
        unsigned.remove(&sigref).unwrap();

        assert_eq!(remote.refs, signed);
        assert_eq!(*remote.refs, unsigned);
modified radicle/src/storage/refs.rs
@@ -18,7 +18,13 @@ use crate::git::Oid;
use crate::storage;
use crate::storage::{ReadRepository, RemoteId, WriteRepository};

-
pub static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("radicle/signature"));
+
pub static SIGNATURE_REF: Lazy<git::Qualified> = Lazy::new(|| {
+
    git::Qualified::from_components(
+
        git::name::component!("radicle"),
+
        git::name::component!("signature"),
+
        None,
+
    )
+
});
pub const REFS_BLOB_PATH: &str = "refs";
pub const SIGNATURE_BLOB_PATH: &str = "signature";

@@ -124,7 +130,7 @@ impl Refs {
        let mut buf = String::new();
        let refs = self
            .iter()
-
            .filter(|(name, oid)| *name != &*SIGNATURE_REF && !oid.is_zero());
+
            .filter(|(name, oid)| name.as_refstr() != (*SIGNATURE_REF).as_ref() && !oid.is_zero());

        for (name, oid) in refs {
            buf.push_str(&oid.to_string());
@@ -291,7 +297,7 @@ impl SignedRefs<Verified> {
            }
        }

-
        let sigref = format!("refs/remotes/{remote}/{sigref}");
+
        let sigref = sigref.add_namespace(remote.into());
        let author = repo.raw().signature()?;
        let commit = repo.raw().commit(
            Some(&sigref),
modified radicle/src/test/storage.rs
@@ -116,7 +116,7 @@ impl ReadRepository for MockRepository {
    fn reference(
        &self,
        _remote: &RemoteId,
-
        _reference: &git::RefStr,
+
        _reference: &git::Qualified,
    ) -> Result<git2::Reference, git_ext::Error> {
        todo!()
    }
@@ -124,7 +124,7 @@ impl ReadRepository for MockRepository {
    fn reference_oid(
        &self,
        _remote: &RemoteId,
-
        _reference: &git::RefStr,
+
        _reference: &git::Qualified,
    ) -> Result<git_ext::Oid, git_ext::Error> {
        todo!()
    }