Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
git-types: port librad::git::types and link_identities::urn
Fintan Halpenny committed 3 years ago
commit b379909610bce0edefa63891897b4d77ba82e421
parent 70b571b
9 files changed +1631 -0
modified Cargo.toml
@@ -3,6 +3,7 @@ members = [
  "git-ext",
  "git-ref-format",
  "git-trailers",
+
  "git-types",
  "link-git",
  "macros",
  # TODO: port gitd-lib over
added git-types/Cargo.toml
@@ -0,0 +1,40 @@
+
[package]
+
name = "radicle-git-types"
+
version = "0.1.0"
+
authors = ["Kim Altintop <kim@eagain.st>", "Fintan Halpenny <fintan.halpenny@gmail.com>"]
+
edition = "2021"
+
license = "GPL-3.0-or-later"
+

+
[lib]
+
doctest = false
+
test = false
+

+
[dependencies]
+
lazy_static = "1.4"
+
multibase = "0.9"
+
multihash = "0.11"
+
percent-encoding = "2"
+
thiserror = "1.0.30"
+
tracing = "0.1"
+

+
[dependencies.git2]
+
version = "0.13.24"
+
default-features = false
+
features = ["vendored-libgit2"]
+

+
[dependencies.minicbor]
+
version = "0.13"
+
features = ["std", "derive"]
+

+
[dependencies.radicle-git-ext]
+
path = "../git-ext"
+

+
[dependencies.radicle-macros]
+
path = "../macros"
+

+
[dependencies.radicle-std-ext]
+
path = "../std-ext"
+

+
[dependencies.serde]
+
version = "1.0"
+
features = ["derive"]
added git-types/src/lib.rs
@@ -0,0 +1,76 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
extern crate radicle_git_ext as git_ext;
+
extern crate radicle_std_ext as std_ext;
+

+
#[macro_use]
+
extern crate lazy_static;
+
#[macro_use]
+
extern crate radicle_macros;
+

+
pub mod namespace;
+
pub mod reference;
+
pub mod refspec;
+
pub mod remote;
+
pub mod urn;
+
pub use urn::Urn;
+

+
mod sealed;
+

+
pub use namespace::{AsNamespace, Namespace};
+
pub use reference::{
+
    AsRemote,
+
    Many,
+
    Multiple,
+
    One,
+
    Reference as GenericRef,
+
    RefsCategory,
+
    Single,
+
    SymbolicRef,
+
};
+
pub use refspec::{Fetchspec, Pushspec, Refspec};
+

+
/// Helper to aid type inference constructing a [`Reference`] without a
+
/// namespace.
+
pub struct Flat;
+

+
impl From<Flat> for Option<Namespace<git_ext::Oid>> {
+
    fn from(_flat: Flat) -> Self {
+
        None
+
    }
+
}
+

+
/// Type specialised reference for the most common use within this crate.
+
pub type Reference<C, R> = GenericRef<Namespace<git_ext::Oid>, R, C>;
+

+
/// Whether we should force the overwriting of a reference or not.
+
#[derive(Debug, Clone, Copy)]
+
pub enum Force {
+
    /// We should overwrite.
+
    True,
+
    /// We should not overwrite.
+
    False,
+
}
+

+
impl Force {
+
    /// Convert the Force to its `bool` equivalent.
+
    fn as_bool(&self) -> bool {
+
        match self {
+
            Force::True => true,
+
            Force::False => false,
+
        }
+
    }
+
}
+

+
impl From<bool> for Force {
+
    fn from(b: bool) -> Self {
+
        if b {
+
            Self::True
+
        } else {
+
            Self::False
+
        }
+
    }
+
}
added git-types/src/namespace.rs
@@ -0,0 +1,105 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::fmt::{self, Display};
+

+
use git_ext as ext;
+
use multihash::Multihash;
+

+
use crate::urn::{self, Urn};
+

+
pub trait AsNamespace: Into<ext::RefLike> {
+
    fn into_namespace(self) -> ext::RefLike {
+
        self.into()
+
    }
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Namespace<R>(Urn<R>);
+

+
impl<R> AsNamespace for Namespace<R>
+
where
+
    R: urn::HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
}
+

+
impl<'a, R> AsNamespace for &'a Namespace<R>
+
where
+
    R: urn::HasProtocol,
+
    &'a R: Into<Multihash>,
+
{
+
}
+

+
impl<R> From<Urn<R>> for Namespace<R> {
+
    fn from(urn: Urn<R>) -> Self {
+
        Self(Urn { path: None, ..urn })
+
    }
+
}
+

+
impl<R: Clone> From<&Urn<R>> for Namespace<R> {
+
    fn from(urn: &Urn<R>) -> Self {
+
        Self(Urn {
+
            path: None,
+
            id: urn.id.clone(),
+
        })
+
    }
+
}
+

+
impl<R> Display for Namespace<R>
+
where
+
    R: urn::HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(ext::RefLike::from(self).as_str())
+
    }
+
}
+

+
impl<R> From<Namespace<R>> for ext::RefLike
+
where
+
    R: urn::HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn from(ns: Namespace<R>) -> Self {
+
        Self::from(ns.0)
+
    }
+
}
+

+
impl<'a, R> From<&'a Namespace<R>> for ext::RefLike
+
where
+
    R: urn::HasProtocol,
+
    &'a R: Into<Multihash>,
+
{
+
    fn from(ns: &'a Namespace<R>) -> Self {
+
        Self::from(&ns.0)
+
    }
+
}
+

+
impl<R> From<Namespace<R>> for ext::RefspecPattern
+
where
+
    R: urn::HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn from(ns: Namespace<R>) -> Self {
+
        ext::RefLike::from(ns).into()
+
    }
+
}
+

+
impl<'a, R> From<&'a Namespace<R>> for ext::RefspecPattern
+
where
+
    R: urn::HasProtocol,
+
    &'a R: Into<Multihash>,
+
{
+
    fn from(ns: &'a Namespace<R>) -> Self {
+
        ext::RefLike::from(ns).into()
+
    }
+
}
+

+
impl<R> From<Namespace<R>> for Urn<R> {
+
    fn from(ns: Namespace<R>) -> Self {
+
        ns.0
+
    }
+
}
added git-types/src/reference.rs
@@ -0,0 +1,550 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    convert::TryFrom,
+
    fmt::{self, Display},
+
    str::FromStr,
+
};
+

+
use git_ext as ext;
+

+
use super::{AsNamespace, Force};
+

+
/// Type witness for a [`Reference`] that should point to a single reference.
+
pub type One = ext::RefLike;
+

+
/// Alias for [`One`].
+
pub type Single = One;
+

+
/// Type witness for a [`Reference`] that should point to multiple references.
+
pub type Many = ext::RefspecPattern;
+

+
/// Alias for [`Many`].
+
pub type Multiple = Many;
+

+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub enum RefsCategory {
+
    Heads,
+
    Rad,
+
    Tags,
+
    Notes,
+
    /// Collaborative objects
+
    Cobs,
+
    Unknown(ext::RefLike),
+
}
+

+
impl RefsCategory {
+
    /// The categories that are present in a default git repository
+
    pub const fn default_categories() -> [RefsCategory; 3] {
+
        [Self::Heads, Self::Tags, Self::Notes]
+
    }
+
}
+

+
impl FromStr for RefsCategory {
+
    type Err = ext::reference::name::Error;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Ok(match s {
+
            "heads" => Self::Heads,
+
            "rad" => Self::Rad,
+
            "tags" => Self::Tags,
+
            "notes" => Self::Notes,
+
            "cobs" => Self::Cobs,
+
            other => {
+
                let reflike = ext::RefLike::try_from(other)?;
+
                Self::Unknown(reflike)
+
            },
+
        })
+
    }
+
}
+

+
impl Display for RefsCategory {
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        match self {
+
            Self::Heads => f.write_str("heads"),
+
            Self::Rad => f.write_str("rad"),
+
            Self::Tags => f.write_str("tags"),
+
            Self::Notes => f.write_str("notes"),
+
            Self::Cobs => f.write_str("cobs"),
+
            Self::Unknown(cat) => f.write_str(cat),
+
        }
+
    }
+
}
+

+
impl From<RefsCategory> for ext::RefLike {
+
    fn from(cat: RefsCategory) -> Self {
+
        ext::RefLike::try_from(cat.to_string()).unwrap()
+
    }
+
}
+

+
impl From<&RefsCategory> for ext::RefLike {
+
    fn from(cat: &RefsCategory) -> Self {
+
        ext::RefLike::try_from(cat.to_string()).unwrap()
+
    }
+
}
+

+
impl From<ext::RefLike> for RefsCategory {
+
    fn from(r: ext::RefLike) -> Self {
+
        (&r).into()
+
    }
+
}
+

+
impl From<&ext::RefLike> for RefsCategory {
+
    fn from(r: &ext::RefLike) -> Self {
+
        match r.as_str() {
+
            "heads" => Self::Heads,
+
            "rad" => Self::Rad,
+
            "tags" => Self::Tags,
+
            "notes" => Self::Notes,
+
            "cobs" => Self::Cobs,
+
            _ => Self::Unknown(r.clone()),
+
        }
+
    }
+
}
+

+
/// Ad-hoc trait to prevent the typechecker from recursing.
+
///
+
/// Morally, we can convert `Reference<N, R, C>` into `ext::RefLike` for any `R:
+
/// Into<ext::RefLike>`. However, the typechecker may then attempt to unify `R`
+
/// with `Reference<_, Reference<_, ...` recursively, leading to
+
/// non-termination. Hence, we restrict the types which can be used as
+
/// `Reference::remote` artificially.
+
pub trait AsRemote: Into<ext::RefLike> {}
+

+
impl AsRemote for ext::RefLike {}
+
impl AsRemote for &ext::RefLike {}
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Reference<Namespace, Remote, Cardinality> {
+
    /// The remote portion of this reference.
+
    pub remote: Option<Remote>,
+
    /// Where this reference falls under, i.e. `heads`, `tags`, `cob`, or`rad`.
+
    pub category: RefsCategory,
+
    /// The path of the reference, e.g. `feature/123`, `dev`, `heads/*`.
+
    pub name: Cardinality,
+
    /// The namespace of this reference.
+
    pub namespace: Option<Namespace>,
+
}
+

+
// Polymorphic definitions
+
impl<N, R, C> Reference<N, R, C>
+
where
+
    N: Clone,
+
    R: Clone,
+
    C: Clone,
+
{
+
    pub fn with_remote(self, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            ..self
+
        }
+
    }
+

+
    pub fn set_remote(&mut self, remote: impl Into<Option<R>>) {
+
        self.remote = remote.into();
+
    }
+

+
    pub fn remote(&mut self, remote: impl Into<Option<R>>) -> &mut Self {
+
        self.set_remote(remote);
+
        self
+
    }
+

+
    /// Set the namespace of this reference to another one. Note that the
+
    /// namespace does not have to be of the original namespace's type.
+
    pub fn with_namespace<NN, Other>(self, namespace: NN) -> Reference<Other, R, C>
+
    where
+
        NN: Into<Option<Other>>,
+
        Other: AsNamespace,
+
    {
+
        Reference {
+
            name: self.name,
+
            remote: self.remote,
+
            category: self.category,
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Set the named portion of this path.
+
    pub fn with_name<S: Into<C>>(self, name: S) -> Self {
+
        Self {
+
            name: name.into(),
+
            ..self
+
        }
+
    }
+

+
    /// Set the named portion of this path.
+
    pub fn set_name<S: Into<C>>(&mut self, name: S) {
+
        self.name = name.into();
+
    }
+

+
    pub fn name<S: Into<C>>(&mut self, name: S) -> &mut Self {
+
        self.set_name(name);
+
        self
+
    }
+
}
+

+
// References with a `One` cardinality
+
impl<N, R> Reference<N, R, One> {
+
    /// Find this particular reference.
+
    pub fn find<'a>(&self, repo: &'a git2::Repository) -> Result<git2::Reference<'a>, git2::Error>
+
    where
+
        Self: ToString,
+
    {
+
        repo.find_reference(&self.to_string())
+
    }
+

+
    /// Resolve the [`git2::Oid`] the reference points to (if it exists).
+
    ///
+
    /// Avoids allocating a [`git2::Reference`].
+
    pub fn oid(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error>
+
    where
+
        Self: ToString,
+
    {
+
        repo.refname_to_id(&self.to_string())
+
    }
+

+
    pub fn create<'a>(
+
        &self,
+
        repo: &'a git2::Repository,
+
        target: git2::Oid,
+
        force: super::Force,
+
        log_message: &str,
+
    ) -> Result<git2::Reference<'a>, git2::Error>
+
    where
+
        Self: ToString,
+
    {
+
        tracing::debug!(
+
            "creating direct reference {} -> {} (force: {}, reflog: '{}')",
+
            self.to_string(),
+
            target,
+
            force.as_bool(),
+
            log_message
+
        );
+
        let name = self.to_string();
+
        repo.reference_ensure_log(&name)?;
+
        repo.reference(&name, target, force.as_bool(), log_message)
+
    }
+

+
    /// Create a [`SymbolicRef`] from `source` to `self` as the `target`.
+
    pub fn symbolic_ref<SN, SR>(
+
        self,
+
        source: Reference<SN, SR, Single>,
+
        force: Force,
+
    ) -> SymbolicRef<Reference<SN, SR, Single>, Self>
+
    where
+
        R: Clone,
+
        N: Clone,
+
    {
+
        SymbolicRef {
+
            source,
+
            target: self,
+
            force,
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs/namespaces/<namespace>/refs/rad/id`
+
    pub fn rad_id(namespace: impl Into<Option<N>>) -> Self {
+
        Self {
+
            remote: None,
+
            category: RefsCategory::Rad,
+
            name: reflike!("id"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs/namespaces/<namespace>/refs/rad/signed_refs`
+
    ///     * `refs/namespaces/<namespace>/refs/remote/<peer_id>/rad/
+
    ///       signed_refs`
+
    pub fn rad_signed_refs(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Rad,
+
            name: reflike!("signed_refs"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs/namespaces/<namespace>/refs/rad/self`
+
    ///     * `refs/namespaces/<namespace>/refs/remote/<peer_id>/rad/self`
+
    pub fn rad_self(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Rad,
+
            name: reflike!("self"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs/namespaces/<namespace>/refs/heads/<name>`
+
    ///     * `refs/namespaces/<namespace>/refs/remote/<peer_id>/heads/<name>
+
    pub fn head(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>, name: One) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Heads,
+
            name,
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    /// * `refs/namespaces/<namespace>/refs/tags/<name>`
+
    /// * `refs/namespaces/<namespace>/refs/remote/<peer_id>/tags/<name>
+
    pub fn tag(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>, name: One) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Tags,
+
            name,
+
            namespace: namespace.into(),
+
        }
+
    }
+
}
+

+
impl<N, R> Display for Reference<N, R, One>
+
where
+
    for<'a> &'a N: AsNamespace,
+
    for<'a> &'a R: AsRemote,
+
{
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(Into::<ext::RefLike>::into(self).as_str())
+
    }
+
}
+

+
impl<N, R> From<Reference<N, R, One>> for ext::RefLike
+
where
+
    for<'a> &'a N: AsNamespace,
+
    for<'a> &'a R: AsRemote,
+
{
+
    fn from(r: Reference<N, R, One>) -> Self {
+
        Self::from(&r)
+
    }
+
}
+

+
impl<'a, N, R> From<&'a Reference<N, R, One>> for ext::RefLike
+
where
+
    &'a N: AsNamespace,
+
    &'a R: AsRemote,
+
{
+
    fn from(r: &'a Reference<N, R, One>) -> Self {
+
        let mut refl = reflike!("refs");
+

+
        if let Some(ref namespace) = r.namespace {
+
            refl = refl
+
                .join(reflike!("namespaces"))
+
                .join(namespace)
+
                .join(reflike!("refs"));
+
        }
+
        if let Some(ref remote) = r.remote {
+
            refl = refl.join(reflike!("remotes")).join(remote);
+
        }
+

+
        refl.join(&r.category)
+
            .join(ext::OneLevel::from(r.name.to_owned()))
+
    }
+
}
+

+
impl<N, R> From<Reference<N, R, One>> for ext::RefspecPattern
+
where
+
    for<'a> &'a N: AsNamespace,
+
    for<'a> &'a R: AsRemote,
+
{
+
    fn from(r: Reference<N, R, One>) -> Self {
+
        Self::from(&r)
+
    }
+
}
+

+
impl<'a, N, R> From<&'a Reference<N, R, One>> for ext::RefspecPattern
+
where
+
    &'a N: AsNamespace,
+
    &'a R: AsRemote,
+
{
+
    fn from(r: &'a Reference<N, R, One>) -> Self {
+
        Into::<ext::RefLike>::into(r).into()
+
    }
+
}
+

+
// TODO(kim): what is this for?
+
#[allow(clippy::from_over_into)]
+
impl<'a, N, R> Into<ext::blob::Branch<'a>> for &'a Reference<N, R, Single>
+
where
+
    Self: ToString,
+
{
+
    fn into(self) -> ext::blob::Branch<'a> {
+
        ext::blob::Branch::from(self.to_string())
+
    }
+
}
+

+
// References with a `Many` cardinality
+
impl<N, R> Reference<N, R, Many> {
+
    /// Get the iterator for these references.
+
    pub fn references<'a>(
+
        &self,
+
        repo: &'a git2::Repository,
+
    ) -> Result<ext::References<'a>, git2::Error>
+
    where
+
        Self: ToString,
+
    {
+
        ext::References::from_globs(repo, &[self.to_string()])
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs[/namespaces/<namespace>/refs]/rad/ids/*`
+
    pub fn rad_ids_glob(namespace: impl Into<Option<N>>) -> Self {
+
        Self {
+
            remote: None,
+
            category: RefsCategory::Rad,
+
            name: refspec_pattern!("ids/*"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs[/namespaces/<namespace>/refs][/remotes/<remote>]/heads/*`
+
    pub fn heads(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Heads,
+
            name: refspec_pattern!("*"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs[/namespaces/<namespace>]/refs[/remotes/<remote>]/rad/*`
+
    pub fn rads(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Rad,
+
            name: refspec_pattern!("*"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs[/namespaces/<namespace>]/refs[/remotes/<remote>]/tags/*`
+
    pub fn tags(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Tags,
+
            name: refspec_pattern!("*"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to:
+
    ///     * `refs[/namespaces/<namespace>]/refs[/remotes/<remote>]/notes/*`
+
    pub fn notes(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Notes,
+
            name: refspec_pattern!("*"),
+
            namespace: namespace.into(),
+
        }
+
    }
+

+
    /// Build a reference that points to
+
    ///     * `refs[/namespaces/namespace]/refs[/remotes/<remote>]/cobs/*`
+
    pub fn cob(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
+
        Self {
+
            remote: remote.into(),
+
            category: RefsCategory::Cobs,
+
            name: refspec_pattern!("*"),
+
            namespace: namespace.into(),
+
        }
+
    }
+
}
+

+
impl<N, R> Display for Reference<N, R, Many>
+
where
+
    for<'a> &'a N: AsNamespace,
+
    for<'a> &'a R: AsRemote,
+
{
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        f.write_str(Into::<ext::RefspecPattern>::into(self).as_str())
+
    }
+
}
+

+
impl<N, R> From<Reference<N, R, Many>> for ext::RefspecPattern
+
where
+
    for<'a> &'a N: AsNamespace,
+
    for<'a> &'a R: AsRemote,
+
{
+
    fn from(r: Reference<N, R, Many>) -> Self {
+
        Self::from(&r)
+
    }
+
}
+

+
impl<'a, N, R> From<&'a Reference<N, R, Many>> for ext::RefspecPattern
+
where
+
    &'a N: AsNamespace,
+
    &'a R: AsRemote,
+
{
+
    fn from(r: &'a Reference<N, R, Many>) -> Self {
+
        let mut refl = reflike!("refs");
+

+
        if let Some(ref namespace) = r.namespace {
+
            refl = refl
+
                .join(reflike!("namespaces"))
+
                .join(namespace)
+
                .join(reflike!("refs"));
+
        }
+
        if let Some(ref remote) = r.remote {
+
            refl = refl.join(reflike!("remotes")).join(remote);
+
        }
+

+
        refl.join(&r.category)
+
            .with_pattern_suffix(r.name.to_owned())
+
    }
+
}
+

+
////////////////////////////////////////////////////////////////////////////////
+

+
/// The data for creating a symbolic reference in a git repository.
+
pub struct SymbolicRef<S, T> {
+
    /// The new symbolic reference.
+
    pub source: S,
+
    /// The reference that already exists and we want to create symbolic
+
    /// reference of.
+
    pub target: T,
+
    /// Whether we should overwrite any pre-existing `source`.
+
    pub force: Force,
+
}
+

+
impl<S, T> SymbolicRef<S, T> {
+
    /// Create a symbolic reference of `target`, where the `source` is the newly
+
    /// created reference.
+
    ///
+
    /// # Errors
+
    ///
+
    ///   * If the `target` does not exist we won't create the symbolic
+
    ///     reference and we error early.
+
    ///   * If we could not create the new symbolic reference since the name
+
    ///     already exists. Note that this will not be the case if `Force::True`
+
    ///     is passed.
+
    pub fn create<'a>(&self, repo: &'a git2::Repository) -> Result<git2::Reference<'a>, git2::Error>
+
    where
+
        for<'b> &'b S: Into<ext::RefLike>,
+
        for<'b> &'b T: Into<ext::RefLike>,
+
    {
+
        let source = Into::<ext::RefLike>::into(&self.source);
+
        let target = Into::<ext::RefLike>::into(&self.target);
+

+
        let reflog_msg = &format!("creating symbolic ref {} -> {}", source, target);
+
        tracing::debug!("{}", reflog_msg);
+

+
        let _ = repo.refname_to_id(target.as_str())?;
+
        repo.reference_ensure_log(source.as_str())?;
+
        repo.reference_symbolic(
+
            source.as_str(),
+
            target.as_str(),
+
            self.force.as_bool(),
+
            reflog_msg,
+
        )
+
    }
+
}
added git-types/src/refspec.rs
@@ -0,0 +1,201 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    convert::TryFrom,
+
    fmt::{self, Display},
+
    str::FromStr,
+
};
+

+
use git_ext as ext;
+

+
use super::Force;
+

+
#[derive(Debug)]
+
pub struct Refspec<S, D> {
+
    /// The source spec (LHS of the `:`).
+
    ///
+
    /// When used as a fetch spec, it refers to the remote side, while as a push
+
    /// spec it refers to the local side.
+
    pub src: S,
+

+
    /// The destination spec (RHS of the `:`).
+
    ///
+
    /// When used as a fetch spec, it refers to the local side, while as a push
+
    /// spec it refers to the remote side.
+
    pub dst: D,
+

+
    /// Whether to allow history rewrites.
+
    pub force: Force,
+
}
+

+
impl<S, D> Refspec<S, D> {
+
    pub fn into_fetchspec(self) -> Fetchspec
+
    where
+
        S: Into<ext::RefspecPattern>,
+
        D: Into<ext::RefspecPattern>,
+
    {
+
        self.into()
+
    }
+

+
    pub fn into_pushspec(self) -> Pushspec
+
    where
+
        S: Into<ext::RefLike>,
+
        D: Into<ext::RefLike>,
+
    {
+
        self.into()
+
    }
+
}
+

+
impl<S, D> Display for Refspec<S, D>
+
where
+
    for<'a> &'a S: Into<ext::RefspecPattern>,
+
    for<'a> &'a D: Into<ext::RefspecPattern>,
+
{
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        if self.force.as_bool() {
+
            f.write_str("+")?;
+
        }
+

+
        let src = Into::<ext::RefspecPattern>::into(&self.src);
+
        let dst = Into::<ext::RefspecPattern>::into(&self.dst);
+

+
        write!(f, "{}:{}", src, dst)
+
    }
+
}
+

+
impl TryFrom<&str> for Refspec<ext::RefspecPattern, ext::RefspecPattern> {
+
    type Error = ext::reference::name::Error;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        let force = s.starts_with('+').into();
+
        let specs = s.trim_start_matches('+');
+
        let mut iter = specs.split(':');
+
        let src = iter
+
            .next()
+
            .ok_or_else(ext::reference::name::Error::empty)
+
            .and_then(ext::RefspecPattern::try_from)?;
+
        let dst = iter
+
            .next()
+
            .ok_or_else(ext::reference::name::Error::empty)
+
            .and_then(ext::RefspecPattern::try_from)?;
+

+
        Ok(Self { src, dst, force })
+
    }
+
}
+

+
impl FromStr for Refspec<ext::RefspecPattern, ext::RefspecPattern> {
+
    type Err = ext::reference::name::Error;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::try_from(s)
+
    }
+
}
+

+
impl TryFrom<&str> for Refspec<ext::RefLike, ext::RefLike> {
+
    type Error = ext::reference::name::Error;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        let force = s.starts_with('+').into();
+
        let specs = s.trim_start_matches('+');
+
        let mut iter = specs.split(':');
+
        let src = iter
+
            .next()
+
            .ok_or_else(ext::reference::name::Error::empty)
+
            .and_then(ext::RefLike::try_from)?;
+
        let dst = iter
+
            .next()
+
            .ok_or_else(ext::reference::name::Error::empty)
+
            .and_then(ext::RefLike::try_from)?;
+

+
        Ok(Self { src, dst, force })
+
    }
+
}
+

+
impl FromStr for Refspec<ext::RefLike, ext::RefLike> {
+
    type Err = ext::reference::name::Error;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::try_from(s)
+
    }
+
}
+

+
#[derive(Debug)]
+
pub struct Fetchspec(Refspec<ext::RefspecPattern, ext::RefspecPattern>);
+

+
impl<S, D> From<Refspec<S, D>> for Fetchspec
+
where
+
    S: Into<ext::RefspecPattern>,
+
    D: Into<ext::RefspecPattern>,
+
{
+
    fn from(spec: Refspec<S, D>) -> Self {
+
        Self(Refspec {
+
            src: spec.src.into(),
+
            dst: spec.dst.into(),
+
            force: spec.force,
+
        })
+
    }
+
}
+

+
impl TryFrom<&str> for Fetchspec {
+
    type Error = ext::reference::name::Error;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        Refspec::try_from(s).map(Self)
+
    }
+
}
+

+
impl FromStr for Fetchspec {
+
    type Err = ext::reference::name::Error;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::try_from(s)
+
    }
+
}
+

+
impl Display for Fetchspec {
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
+

+
#[derive(Debug)]
+
pub struct Pushspec(Refspec<ext::RefLike, ext::RefLike>);
+

+
impl<S, D> From<Refspec<S, D>> for Pushspec
+
where
+
    S: Into<ext::RefLike>,
+
    D: Into<ext::RefLike>,
+
{
+
    fn from(spec: Refspec<S, D>) -> Self {
+
        Self(Refspec {
+
            src: spec.src.into(),
+
            dst: spec.dst.into(),
+
            force: spec.force,
+
        })
+
    }
+
}
+

+
impl TryFrom<&str> for Pushspec {
+
    type Error = ext::reference::name::Error;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        Refspec::try_from(s).map(Self)
+
    }
+
}
+

+
impl FromStr for Pushspec {
+
    type Err = ext::reference::name::Error;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::try_from(s)
+
    }
+
}
+

+
impl Display for Pushspec {
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
added git-types/src/remote.rs
@@ -0,0 +1,203 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{convert::TryFrom, str::FromStr};
+

+
use git_ext::{
+
    error::{is_exists_err, is_not_found_err},
+
    reference::{self, RefLike},
+
};
+
use std_ext::result::ResultExt as _;
+
use thiserror::Error;
+

+
use super::{Fetchspec, Pushspec};
+

+
#[derive(Debug, Error)]
+
pub enum FindError {
+
    #[error("missing {0}")]
+
    Missing(&'static str),
+

+
    #[error("failed to parse URL")]
+
    ParseUrl(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
+

+
    #[error("failed to parse refspec")]
+
    Refspec(#[from] reference::name::Error),
+

+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
}
+

+
#[derive(Debug)]
+
pub struct Remote<Url> {
+
    /// The file path to the git monorepo.
+
    pub url: Url,
+
    /// Name of the remote, e.g. `"rad"`, `"origin"`.
+
    pub name: RefLike,
+
    /// The set of fetch specs to add upon creation.
+
    ///
+
    /// **Note**: empty fetch specs do not denote the default fetch spec
+
    /// (`refs/heads/*:refs/remote/<name>/*`), but ... empty fetch specs.
+
    pub fetchspecs: Vec<Fetchspec>,
+
    /// The set of push specs to add upon creation.
+
    pub pushspecs: Vec<Pushspec>,
+
}
+

+
impl<Url> Remote<Url> {
+
    /// Create a `"rad"` remote with a single fetch spec.
+
    pub fn rad_remote<Ref, Spec>(url: Url, fetch_spec: Ref) -> Self
+
    where
+
        Ref: Into<Option<Spec>>,
+
        Spec: Into<Fetchspec>,
+
    {
+
        Self {
+
            url,
+
            name: reflike!("rad"),
+
            fetchspecs: fetch_spec.into().into_iter().map(Into::into).collect(),
+
            pushspecs: vec![],
+
        }
+
    }
+

+
    /// Create a new `Remote` with the given `url` and `name`, while making the
+
    /// `fetch_spec` and `pushspecs` empty.
+
    pub fn new<R>(url: Url, name: R) -> Self
+
    where
+
        R: Into<RefLike>,
+
    {
+
        Self {
+
            url,
+
            name: name.into(),
+
            fetchspecs: vec![],
+
            pushspecs: vec![],
+
        }
+
    }
+

+
    /// Override the fetch specs.
+
    pub fn with_fetchspecs<I>(self, specs: I) -> Self
+
    where
+
        I: IntoIterator,
+
        <I as IntoIterator>::Item: Into<Fetchspec>,
+
    {
+
        Self {
+
            fetchspecs: specs.into_iter().map(Into::into).collect(),
+
            ..self
+
        }
+
    }
+

+
    /// Add a fetch spec.
+
    pub fn add_fetchspec(&mut self, spec: impl Into<Fetchspec>) {
+
        self.fetchspecs.push(spec.into())
+
    }
+

+
    /// Override the push specs.
+
    pub fn with_pushspecs<I>(self, specs: I) -> Self
+
    where
+
        I: IntoIterator,
+
        <I as IntoIterator>::Item: Into<Pushspec>,
+
    {
+
        Self {
+
            pushspecs: specs.into_iter().map(Into::into).collect(),
+
            ..self
+
        }
+
    }
+

+
    /// Add a push spec.
+
    pub fn add_pushspec(&mut self, spec: impl Into<Pushspec>) {
+
        self.pushspecs.push(spec.into())
+
    }
+

+
    /// Persist the remote in the `repo`'s config.
+
    ///
+
    /// If a remote with the same name already exists, previous values of the
+
    /// configuration keys `url`, `fetch`, and `push` will be overwritten.
+
    /// Note that this means that _other_ configuration keys are left
+
    /// untouched, if present.
+
    #[allow(clippy::unit_arg)]
+
    #[tracing::instrument(skip(self, repo), fields(name = self.name.as_str()))]
+
    pub fn save(&mut self, repo: &git2::Repository) -> Result<(), git2::Error>
+
    where
+
        Url: ToString,
+
    {
+
        let url = self.url.to_string();
+
        repo.remote(self.name.as_str(), &url)
+
            .and(Ok(()))
+
            .or_matches::<git2::Error, _, _>(is_exists_err, || Ok(()))?;
+

+
        {
+
            let mut config = repo.config()?;
+
            config
+
                .remove_multivar(&format!("remote.{}.url", self.name), ".*")
+
                .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(()))?;
+
            config
+
                .remove_multivar(&format!("remote.{}.fetch", self.name), ".*")
+
                .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(()))?;
+
            config
+
                .remove_multivar(&format!("remote.{}.push", self.name), ".*")
+
                .or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(()))?;
+
        }
+

+
        repo.remote_set_url(self.name.as_str(), &url)?;
+

+
        for spec in self.fetchspecs.iter() {
+
            repo.remote_add_fetch(self.name.as_str(), &spec.to_string())?;
+
        }
+
        for spec in self.pushspecs.iter() {
+
            repo.remote_add_push(self.name.as_str(), &spec.to_string())?;
+
        }
+

+
        debug_assert!(repo.find_remote(self.name.as_str()).is_ok());
+

+
        Ok(())
+
    }
+

+
    /// Find a persisted remote by name.
+
    #[allow(clippy::unit_arg)]
+
    #[tracing::instrument(skip(repo))]
+
    pub fn find(repo: &git2::Repository, name: RefLike) -> Result<Option<Self>, FindError>
+
    where
+
        Url: FromStr,
+
        <Url as FromStr>::Err: std::error::Error + Send + Sync + 'static,
+
    {
+
        let git_remote = repo
+
            .find_remote(name.as_str())
+
            .map(Some)
+
            .or_matches::<FindError, _, _>(is_not_found_err, || Ok(None))?;
+

+
        match git_remote {
+
            None => Ok(None),
+
            Some(remote) => {
+
                let url = remote
+
                    .url()
+
                    .ok_or(FindError::Missing("url"))?
+
                    .parse()
+
                    .map_err(|e| FindError::ParseUrl(Box::new(e)))?;
+
                let fetchspecs = remote
+
                    .fetch_refspecs()?
+
                    .into_iter()
+
                    .flatten()
+
                    .map(Fetchspec::try_from)
+
                    .collect::<Result<_, _>>()?;
+
                let pushspecs = remote
+
                    .push_refspecs()?
+
                    .into_iter()
+
                    .flatten()
+
                    .map(Pushspec::try_from)
+
                    .collect::<Result<_, _>>()?;
+

+
                Ok(Some(Self {
+
                    url,
+
                    name,
+
                    fetchspecs,
+
                    pushspecs,
+
                }))
+
            },
+
        }
+
    }
+
}
+

+
impl<Url> AsRef<Url> for Remote<Url> {
+
    fn as_ref(&self) -> &Url {
+
        &self.url
+
    }
+
}
added git-types/src/sealed.rs
@@ -0,0 +1,8 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
pub trait Sealed {}
+

+
impl Sealed for git_ext::Oid {}
added git-types/src/urn.rs
@@ -0,0 +1,447 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
use std::{
+
    borrow::Cow,
+
    convert::TryFrom,
+
    fmt::{self, Debug, Display},
+
    str::FromStr,
+
};
+

+
use git_ext as ext;
+
use multihash::{Multihash, MultihashRef};
+
use percent_encoding::percent_decode_str;
+
use thiserror::Error;
+

+
use super::sealed;
+

+
lazy_static! {
+
    pub static ref DEFAULT_PATH: ext::Qualified = ext::Qualified::from(reflike!("refs/rad/id"));
+
}
+

+
pub mod error {
+
    use super::*;
+

+
    #[derive(Debug, Error)]
+
    #[non_exhaustive]
+
    pub enum DecodeId<E: std::error::Error + Send + Sync + 'static> {
+
        #[error("invalid id")]
+
        InvalidId(#[source] E),
+

+
        #[error(transparent)]
+
        Encoding(#[from] multibase::Error),
+

+
        #[error(transparent)]
+
        Multihash(#[from] multihash::DecodeOwnedError),
+
    }
+

+
    #[derive(Debug, Error)]
+
    #[non_exhaustive]
+
    pub enum FromRefLike<E: std::error::Error + Send + Sync + 'static> {
+
        #[error("missing {0}")]
+
        Missing(&'static str),
+

+
        #[error("must be a fully-qualified ref, ie. start with `refs/namespaces`")]
+
        Namespaced(#[from] ext::reference::name::StripPrefixError),
+

+
        #[error("invalid id")]
+
        InvalidId(#[source] DecodeId<E>),
+

+
        #[error(transparent)]
+
        Path(#[from] ext::reference::name::Error),
+
    }
+

+
    #[derive(Debug, Error)]
+
    #[non_exhaustive]
+
    pub enum FromStr<E: std::error::Error + Send + Sync + 'static> {
+
        #[error("missing {0}")]
+
        Missing(&'static str),
+

+
        #[error("invalid namespace identifier: {0}, expected `rad:<protocol>:<id>`")]
+
        InvalidNID(String),
+

+
        #[error("invalid protocol: {0}, expected `rad:<protocol>:<id>`")]
+
        InvalidProto(String),
+

+
        #[error("invalid id: {id}, expected `rad:<protocol>:<id>`")]
+
        InvalidId {
+
            id: String,
+
            #[source]
+
            source: DecodeId<E>,
+
        },
+

+
        #[error(transparent)]
+
        Path(#[from] ext::reference::name::Error),
+

+
        #[error(transparent)]
+
        Utf8(#[from] std::str::Utf8Error),
+
    }
+
}
+

+
pub trait HasProtocol: sealed::Sealed {
+
    const PROTOCOL: &'static str;
+
}
+

+
impl HasProtocol for git_ext::Oid {
+
    const PROTOCOL: &'static str = "git";
+
}
+

+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+
pub enum SomeProtocol {
+
    Git,
+
}
+

+
impl minicbor::Encode for SomeProtocol {
+
    fn encode<W: minicbor::encode::Write>(
+
        &self,
+
        e: &mut minicbor::Encoder<W>,
+
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
+
        match self {
+
            Self::Git => e.u8(0),
+
        }?;
+

+
        Ok(())
+
    }
+
}
+

+
impl<'de> minicbor::Decode<'de> for SomeProtocol {
+
    fn decode(d: &mut minicbor::Decoder) -> Result<Self, minicbor::decode::Error> {
+
        match d.u8()? {
+
            0 => Ok(Self::Git),
+
            _ => Err(minicbor::decode::Error::Message("unknown protocol")),
+
        }
+
    }
+
}
+

+
impl TryFrom<&str> for SomeProtocol {
+
    type Error = &'static str;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        match s {
+
            "git" => Ok(SomeProtocol::Git),
+
            _ => Err("unknown protocol"),
+
        }
+
    }
+
}
+

+
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+
pub struct Urn<R> {
+
    pub id: R,
+
    pub path: Option<ext::RefLike>,
+
}
+

+
impl<R> Urn<R> {
+
    pub const fn new(id: R) -> Self {
+
        Self { id, path: None }
+
    }
+

+
    /// Render [`Self::id`] into the canonical string encoding.
+
    pub fn encode_id<'a>(&'a self) -> String
+
    where
+
        &'a R: Into<Multihash>,
+
    {
+
        multibase::encode(multibase::Base::Base32Z, (&self.id).into())
+
    }
+

+
    pub fn try_from_id(s: impl AsRef<str>) -> Result<Self, error::DecodeId<R::Error>>
+
    where
+
        R: TryFrom<Multihash>,
+
        R::Error: std::error::Error + Send + Sync + 'static,
+
    {
+
        let bytes = multibase::decode(s.as_ref()).map(|x| x.1)?;
+
        let mhash = Multihash::from_bytes(bytes)?;
+
        let id = R::try_from(mhash).map_err(error::DecodeId::InvalidId)?;
+
        Ok(Self::new(id))
+
    }
+

+
    pub fn map<F, S>(self, f: F) -> Urn<S>
+
    where
+
        F: FnOnce(R) -> S,
+
    {
+
        Urn {
+
            id: f(self.id),
+
            path: self.path,
+
        }
+
    }
+

+
    pub fn map_path<F>(self, f: F) -> Self
+
    where
+
        F: FnOnce(Option<ext::RefLike>) -> Option<ext::RefLike>,
+
    {
+
        Self {
+
            id: self.id,
+
            path: f(self.path),
+
        }
+
    }
+

+
    pub fn with_path<P>(self, path: P) -> Self
+
    where
+
        P: Into<Option<ext::RefLike>>,
+
    {
+
        self.map_path(|_| path.into())
+
    }
+
}
+

+
impl<R> From<R> for Urn<R> {
+
    fn from(r: R) -> Self {
+
        Self::new(r)
+
    }
+
}
+

+
impl<'a, R: Clone> From<Urn<R>> for Cow<'a, Urn<R>> {
+
    fn from(urn: Urn<R>) -> Self {
+
        Cow::Owned(urn)
+
    }
+
}
+

+
impl<'a, R: Clone> From<&'a Urn<R>> for Cow<'a, Urn<R>> {
+
    fn from(urn: &'a Urn<R>) -> Self {
+
        Cow::Borrowed(urn)
+
    }
+
}
+

+
// FIXME: For some inexplicable reason, rustc rejects an impl for Urn<R>,
+
// claiming that the blanket impl `impl<T, U> TryFrom<U> for T where U: Into<T>`
+
// overlaps. We absolutely do not have `Into<Urn<R>> for ext::RefLike`.
+
impl TryFrom<ext::RefLike> for Urn<ext::Oid> {
+
    type Error = error::FromRefLike<ext::oid::FromMultihashError>;
+

+
    fn try_from(refl: ext::RefLike) -> Result<Self, Self::Error> {
+
        let refl = refl.strip_prefix("refs/namespaces/")?;
+
        let mut suf = refl.split('/');
+
        let ns = suf.next().ok_or(Self::Error::Missing("namespace"))?;
+
        let urn = Self::try_from_id(ns).map_err(Self::Error::InvalidId)?;
+
        let path = {
+
            let path = suf.collect::<Vec<_>>().join("/");
+
            if path.is_empty() {
+
                Ok(None)
+
            } else {
+
                ext::RefLike::try_from(path).map(Some)
+
            }
+
        }?;
+

+
        Ok(urn.with_path(path))
+
    }
+
}
+

+
impl<R> From<Urn<R>> for ext::RefLike
+
where
+
    R: HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn from(urn: Urn<R>) -> Self {
+
        Self::from(&urn)
+
    }
+
}
+

+
// FIXME: this is not kosher -- doesn't include `refs/namespaces`, but
+
// everything after that. Should have a better type for that.
+
impl<'a, R> From<&'a Urn<R>> for ext::RefLike
+
where
+
    R: HasProtocol,
+
    &'a R: Into<Multihash>,
+
{
+
    fn from(urn: &'a Urn<R>) -> Self {
+
        let refl = Self::try_from(urn.encode_id()).unwrap();
+
        match &urn.path {
+
            None => refl,
+
            Some(path) => refl.join(ext::Qualified::from(path.clone())),
+
        }
+
    }
+
}
+

+
#[cfg(feature = "git-ref-format")]
+
impl<'a, R> From<&'a Urn<R>> for git_ref_format::Component<'_>
+
where
+
    R: HasProtocol,
+
    &'a R: Into<Multihash>,
+
{
+
    #[inline]
+
    fn from(urn: &'a Urn<R>) -> Self {
+
        use git_ref_format::RefString;
+

+
        let rs = RefString::try_from(urn.encode_id()).expect("urn id is a valid ref string");
+
        Self::from_refstring(rs).expect("urn id is a valid ref component")
+
    }
+
}
+

+
impl<R> Display for Urn<R>
+
where
+
    R: HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
        write!(f, "rad:{}:{}", R::PROTOCOL, self.encode_id())?;
+

+
        if let Some(path) = &self.path {
+
            write!(f, "/{}", path.percent_encode())?;
+
        }
+

+
        Ok(())
+
    }
+
}
+

+
impl<R, E> FromStr for Urn<R>
+
where
+
    R: HasProtocol + TryFrom<Multihash, Error = E>,
+
    E: std::error::Error + Send + Sync + 'static,
+
{
+
    type Err = error::FromStr<E>;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let mut components = s.split(':');
+

+
        components
+
            .next()
+
            .ok_or(Self::Err::Missing("namespace"))
+
            .and_then(|nid| {
+
                (nid == "rad")
+
                    .then(|| ())
+
                    .ok_or_else(|| Self::Err::InvalidNID(nid.to_string()))
+
            })?;
+

+
        components
+
            .next()
+
            .ok_or(Self::Err::Missing("protocol"))
+
            .and_then(|proto| {
+
                (R::PROTOCOL == proto)
+
                    .then(|| ())
+
                    .ok_or_else(|| Self::Err::InvalidProto(proto.to_string()))
+
            })?;
+

+
        components
+
            .next()
+
            .ok_or(Self::Err::Missing("id[/path]"))
+
            .and_then(|s| {
+
                let decoded = percent_decode_str(s).decode_utf8()?;
+
                let mut iter = decoded.splitn(2, '/');
+

+
                let id = iter.next().ok_or(Self::Err::Missing("id"))?;
+
                let urn = Self::try_from_id(id).map_err(|err| Self::Err::InvalidId {
+
                    id: id.to_string(),
+
                    source: err,
+
                })?;
+
                let path = iter.next().map(ext::RefLike::try_from).transpose()?;
+
                Ok(urn.with_path(path))
+
            })
+
    }
+
}
+

+
impl<R> serde::Serialize for Urn<R>
+
where
+
    R: HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: serde::Serializer,
+
    {
+
        self.to_string().serialize(serializer)
+
    }
+
}
+

+
impl<'de, R, E> serde::Deserialize<'de> for Urn<R>
+
where
+
    R: HasProtocol + TryFrom<Multihash, Error = E>,
+
    E: std::error::Error + Send + Sync + 'static,
+
{
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: serde::Deserializer<'de>,
+
    {
+
        let s: &str = serde::Deserialize::deserialize(deserializer)?;
+
        s.parse().map_err(serde::de::Error::custom)
+
    }
+
}
+

+
#[derive(Debug, PartialEq, minicbor::Encode, minicbor::Decode)]
+
#[cbor(array)]
+
struct AsCbor<'a> {
+
    #[b(0)]
+
    #[cbor(with = "minicbor::bytes")]
+
    id: &'a [u8],
+

+
    #[n(1)]
+
    proto: SomeProtocol,
+

+
    #[b(2)]
+
    path: Option<&'a str>,
+
}
+

+
impl<R> minicbor::Encode for Urn<R>
+
where
+
    R: HasProtocol,
+
    for<'a> &'a R: Into<Multihash>,
+
{
+
    fn encode<W: minicbor::encode::Write>(
+
        &self,
+
        e: &mut minicbor::Encoder<W>,
+
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
+
        let id: Multihash = (&self.id).into();
+
        e.encode(AsCbor {
+
            id: id.as_bytes(),
+
            proto: SomeProtocol::try_from(R::PROTOCOL).unwrap(),
+
            path: self.path.as_ref().map(|path| path.as_str()),
+
        })?;
+

+
        Ok(())
+
    }
+
}
+

+
impl<'de, R> minicbor::Decode<'de> for Urn<R>
+
where
+
    for<'a> R: HasProtocol + TryFrom<MultihashRef<'a>>,
+
{
+
    fn decode(d: &mut minicbor::Decoder) -> Result<Self, minicbor::decode::Error> {
+
        use minicbor::decode::Error::Message as Error;
+

+
        let AsCbor { id, path, .. } = d.decode()?;
+

+
        let id = {
+
            let mhash = MultihashRef::from_slice(id).or(Err(Error("invalid multihash")))?;
+
            R::try_from(mhash).or(Err(Error("invalid id")))
+
        }?;
+
        let path = path
+
            .map(ext::RefLike::try_from)
+
            .transpose()
+
            .or(Err(Error("invalid path")))?;
+

+
        Ok(Self { id, path })
+
    }
+
}
+

+
pub mod test {
+
    use super::*;
+

+
    /// Fake `id` of a `Urn<FakeId>`.
+
    ///
+
    /// Not cryptographically secure, but cheap to create for tests.
+
    #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+
    pub struct FakeId(pub usize);
+

+
    impl sealed::Sealed for FakeId {}
+

+
    impl HasProtocol for FakeId {
+
        const PROTOCOL: &'static str = "test";
+
    }
+

+
    impl From<usize> for FakeId {
+
        fn from(sz: usize) -> FakeId {
+
            Self(sz)
+
        }
+
    }
+

+
    impl From<FakeId> for Multihash {
+
        fn from(id: FakeId) -> Self {
+
            Self::from(&id)
+
        }
+
    }
+

+
    impl From<&FakeId> for Multihash {
+
        fn from(id: &FakeId) -> Self {
+
            multihash::wrap(multihash::Code::Identity, &id.0.to_be_bytes())
+
        }
+
    }
+
}