Radish alpha
r
rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt
Git libraries for Radicle
Radicle
Git
meta: use long crate names
Fintan Halpenny committed 3 years ago
commit ce7dc3890a3e5c0648feac2f072256c197cdfbca
parent ff300c2
64 files changed +3752 -4253
modified Cargo.toml
@@ -1,13 +1,13 @@
[workspace]
members = [
-
  "git-ext",
  "git-ref-format",
  "git-trailers",
-
  "git-types",
  "link-git",
-
  "macros",
+
  "radicle-git-ext",
+
  "radicle-git-types",
+
  "radicle-macros",
+
  "radicle-std-ext",
  # TODO: port gitd-lib over
  # "cli/gitd-lib",
-
  "std-ext",
  "test",
]
deleted git-ext/Cargo.toml
@@ -1,41 +0,0 @@
-
[package]
-
name = "radicle-git-ext"
-
version = "0.1.0"
-
authors = ["The Radicle Team <dev@radicle.xyz>"]
-
edition = "2018"
-
license = "GPL-3.0-or-later"
-
description = "Utilities and extensions to the git2 crate"
-

-
[lib]
-
doctest = false
-
test = false
-

-
[dependencies]
-
multihash = "0.11"
-
percent-encoding = "2"
-
thiserror = "1"
-

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

-
[dependencies.git-ref-format]
-
path = "../git-ref-format"
-

-
[dependencies.link-git]
-
path = "../link-git"
-
optional = true
-

-
[dependencies.minicbor]
-
version = "0.13"
-
features = ["std"]
-
optional = true
-

-
[dependencies.serde]
-
version = "1"
-
features = ["derive"]
-
optional = true
-

-
[dependencies.radicle-std-ext]
-
path = "../std-ext"
deleted git-ext/src/blob.rs
@@ -1,146 +0,0 @@
-
// 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, path::Path};
-

-
use radicle_std_ext::result::ResultExt as _;
-
use thiserror::Error;
-

-
use crate::{error::is_not_found_err, revwalk};
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum Error {
-
    #[error(transparent)]
-
    NotFound(#[from] NotFound),
-

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

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum NotFound {
-
    #[error("blob with path {0} not found")]
-
    NoSuchBlob(String),
-

-
    #[error("branch {0} not found")]
-
    NoSuchBranch(String),
-

-
    #[error("object {0} not found")]
-
    NoSuchObject(git2::Oid),
-

-
    #[error("the supplied git2::Reference does not have a target")]
-
    NoRefTarget,
-
}
-

-
pub enum Branch<'a> {
-
    Name(Cow<'a, str>),
-
    Ref(git2::Reference<'a>),
-
}
-

-
impl<'a> From<&'a str> for Branch<'a> {
-
    fn from(s: &'a str) -> Self {
-
        Self::Name(Cow::Borrowed(s))
-
    }
-
}
-

-
impl<'a> From<String> for Branch<'a> {
-
    fn from(s: String) -> Self {
-
        Self::Name(Cow::Owned(s))
-
    }
-
}
-

-
impl<'a> From<git2::Reference<'a>> for Branch<'a> {
-
    fn from(r: git2::Reference<'a>) -> Self {
-
        Self::Ref(r)
-
    }
-
}
-

-
/// Conveniently read a [`git2::Blob`] from a starting point.
-
pub enum Blob<'a> {
-
    /// Look up the tip of the reference specified by [`Branch`], peel until a
-
    /// tree is found, and traverse the tree along the given [`Path`] until
-
    /// the blob is found.
-
    Tip { branch: Branch<'a>, path: &'a Path },
-
    /// Traverse the history from the tip of [`Branch`] along the first parent
-
    /// until a commit without parents is found. Try to get the blob in that
-
    /// commit's tree at [`Path`].
-
    Init { branch: Branch<'a>, path: &'a Path },
-
    /// Look up `object`, peel until a tree is found, and try to get at the blob
-
    /// at [`Path`].
-
    At { object: git2::Oid, path: &'a Path },
-
}
-

-
impl<'a> Blob<'a> {
-
    pub fn get(self, git: &'a git2::Repository) -> Result<git2::Blob<'a>, Error> {
-
        match self {
-
            Self::Tip { branch, path } => {
-
                let reference = match branch {
-
                    Branch::Name(name) => {
-
                        git.find_reference(&name).or_matches(is_not_found_err, || {
-
                            Err(Error::NotFound(NotFound::NoSuchBranch(
-
                                name.to_owned().to_string(),
-
                            )))
-
                        })
-
                    },
-

-
                    Branch::Ref(reference) => Ok(reference),
-
                }?;
-
                let tree = reference.peel_to_tree()?;
-
                blob(git, tree, path)
-
            },
-

-
            Self::Init { branch, path } => {
-
                let start = match branch {
-
                    Branch::Name(name) => Ok(revwalk::Start::Ref(name.to_string())),
-
                    Branch::Ref(reference) => {
-
                        match (reference.target(), reference.symbolic_target()) {
-
                            (Some(oid), _) => Ok(revwalk::Start::Oid(oid)),
-
                            (_, Some(sym)) => Ok(revwalk::Start::Ref(sym.to_string())),
-
                            (_, _) => Err(Error::NotFound(NotFound::NoRefTarget)),
-
                        }
-
                    },
-
                }?;
-

-
                let revwalk = revwalk::FirstParent::new(git, start)?.reverse()?;
-
                match revwalk.into_iter().next() {
-
                    None => Err(Error::NotFound(NotFound::NoSuchBlob(
-
                        path.display().to_string(),
-
                    ))),
-
                    Some(oid) => {
-
                        let oid = oid?;
-
                        let tree = git.find_commit(oid)?.tree()?;
-
                        blob(git, tree, path)
-
                    },
-
                }
-
            },
-

-
            Self::At { object, path } => {
-
                let tree = git
-
                    .find_object(object, None)
-
                    .or_matches(is_not_found_err, || {
-
                        Err(Error::NotFound(NotFound::NoSuchObject(object)))
-
                    })
-
                    .and_then(|obj| Ok(obj.peel_to_tree()?))?;
-
                blob(git, tree, path)
-
            },
-
        }
-
    }
-
}
-

-
fn blob<'a>(
-
    repo: &'a git2::Repository,
-
    tree: git2::Tree<'a>,
-
    path: &'a Path,
-
) -> Result<git2::Blob<'a>, Error> {
-
    let entry = tree.get_path(path).or_matches(is_not_found_err, || {
-
        Err(Error::NotFound(NotFound::NoSuchBlob(
-
            path.display().to_string(),
-
        )))
-
    })?;
-

-
    entry.to_object(repo)?.peel_to_blob().map_err(Error::from)
-
}
deleted git-ext/src/error.rs
@@ -1,22 +0,0 @@
-
// 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::Display, io};
-

-
pub fn is_not_found_err(e: &git2::Error) -> bool {
-
    e.code() == git2::ErrorCode::NotFound
-
}
-

-
pub fn is_exists_err(e: &git2::Error) -> bool {
-
    e.code() == git2::ErrorCode::Exists
-
}
-

-
pub fn into_git_err<E: Display>(e: E) -> git2::Error {
-
    git2::Error::from_str(&e.to_string())
-
}
-

-
pub fn into_io_err(e: git2::Error) -> io::Error {
-
    io::Error::new(io::ErrorKind::Other, e)
-
}
deleted git-ext/src/lib.rs
@@ -1,22 +0,0 @@
-
// 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.
-

-
//! Extensions and wrappers for `git2` types
-

-
pub mod blob;
-
pub mod error;
-
pub mod oid;
-
pub mod reference;
-
pub mod revwalk;
-
pub mod transport;
-
pub mod tree;
-

-
pub use blob::*;
-
pub use error::*;
-
pub use oid::*;
-
pub use reference::*;
-
pub use revwalk::*;
-
pub use transport::*;
-
pub use tree::Tree;
deleted git-ext/src/oid.rs
@@ -1,233 +0,0 @@
-
// 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},
-
    ops::Deref,
-
    str::FromStr,
-
};
-

-
use multihash::{Multihash, MultihashRef};
-
use thiserror::Error;
-

-
#[cfg(feature = "link-git")]
-
use link_git::hash as git_hash;
-

-
/// Serializable [`git2::Oid`]
-
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
-
pub struct Oid(git2::Oid);
-

-
impl Oid {
-
    pub fn into_multihash(self) -> Multihash {
-
        self.into()
-
    }
-
}
-

-
#[cfg(feature = "serde")]
-
mod serde_impls {
-
    use super::*;
-
    use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
-

-
    impl Serialize for Oid {
-
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
        where
-
            S: Serializer,
-
        {
-
            self.0.to_string().serialize(serializer)
-
        }
-
    }
-

-
    impl<'de> Deserialize<'de> for Oid {
-
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
        where
-
            D: Deserializer<'de>,
-
        {
-
            struct OidVisitor;
-

-
            impl<'de> Visitor<'de> for OidVisitor {
-
                type Value = Oid;
-

-
                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
                    write!(f, "a hexidecimal git2::Oid")
-
                }
-

-
                fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
-
                where
-
                    E: serde::de::Error,
-
                {
-
                    s.parse().map_err(serde::de::Error::custom)
-
                }
-
            }
-

-
            deserializer.deserialize_str(OidVisitor)
-
        }
-
    }
-
}
-

-
#[cfg(feature = "minicbor")]
-
mod minicbor_impls {
-
    use super::*;
-
    use minicbor::{
-
        decode,
-
        encode::{self, Write},
-
        Decode,
-
        Decoder,
-
        Encode,
-
        Encoder,
-
    };
-

-
    impl Encode for Oid {
-
        fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
            e.bytes(Multihash::from(self).as_bytes())?;
-
            Ok(())
-
        }
-
    }
-

-
    impl<'b> Decode<'b> for Oid {
-
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
-
            let bytes = d.bytes()?;
-
            let mhash = MultihashRef::from_slice(bytes)
-
                .or(Err(decode::Error::Message("not a multihash")))?;
-
            Self::try_from(mhash).or(Err(decode::Error::Message("not a git oid")))
-
        }
-
    }
-
}
-

-
impl Deref for Oid {
-
    type Target = git2::Oid;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<git2::Oid> for Oid {
-
    fn as_ref(&self) -> &git2::Oid {
-
        self
-
    }
-
}
-

-
impl AsRef<[u8]> for Oid {
-
    fn as_ref(&self) -> &[u8] {
-
        self.as_bytes()
-
    }
-
}
-

-
#[cfg(feature = "link-git")]
-
impl AsRef<git_hash::oid> for Oid {
-
    fn as_ref(&self) -> &git_hash::oid {
-
        // SAFETY: checks the length of the slice, which we know is correct
-
        git_hash::oid::try_from(self.as_bytes()).unwrap()
-
    }
-
}
-

-
impl From<git2::Oid> for Oid {
-
    fn from(oid: git2::Oid) -> Self {
-
        Self(oid)
-
    }
-
}
-

-
impl From<Oid> for git2::Oid {
-
    fn from(oid: Oid) -> Self {
-
        oid.0
-
    }
-
}
-

-
#[cfg(feature = "link-git")]
-
impl From<git_hash::ObjectId> for Oid {
-
    fn from(git_hash::ObjectId::Sha1(bs): git_hash::ObjectId) -> Self {
-
        // SAFETY: checks the length of the slice, which we statically know
-
        Self(git2::Oid::from_bytes(&bs).unwrap())
-
    }
-
}
-

-
#[cfg(feature = "link-git")]
-
impl From<Oid> for git_hash::ObjectId {
-
    fn from(oid: Oid) -> Self {
-
        Self::from_20_bytes(oid.as_ref())
-
    }
-
}
-

-
#[cfg(feature = "link-git")]
-
impl<'a> From<&'a Oid> for &'a git_hash::oid {
-
    fn from(oid: &'a Oid) -> Self {
-
        oid.as_ref()
-
    }
-
}
-

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

-
impl TryFrom<&str> for Oid {
-
    type Error = git2::Error;
-

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

-
impl FromStr for Oid {
-
    type Err = git2::Error;
-

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

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum FromMultihashError {
-
    #[error("invalid hash algorithm: expected Sha1, got {actual:?}")]
-
    AlgorithmMismatch { actual: multihash::Code },
-

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

-
impl TryFrom<Multihash> for Oid {
-
    type Error = FromMultihashError;
-

-
    fn try_from(mhash: Multihash) -> Result<Self, Self::Error> {
-
        Self::try_from(mhash.as_ref())
-
    }
-
}
-

-
impl TryFrom<MultihashRef<'_>> for Oid {
-
    type Error = FromMultihashError;
-

-
    fn try_from(mhash: MultihashRef) -> Result<Self, Self::Error> {
-
        if mhash.algorithm() != multihash::Code::Sha1 {
-
            return Err(Self::Error::AlgorithmMismatch {
-
                actual: mhash.algorithm(),
-
            });
-
        }
-

-
        Self::try_from(mhash.digest()).map_err(Self::Error::from)
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Oid {
-
    type Error = git2::Error;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-
        git2::Oid::from_bytes(bytes).map(Self)
-
    }
-
}
-

-
impl From<Oid> for Multihash {
-
    fn from(oid: Oid) -> Self {
-
        Self::from(&oid)
-
    }
-
}
-

-
impl From<&Oid> for Multihash {
-
    fn from(oid: &Oid) -> Self {
-
        multihash::wrap(multihash::Code::Sha1, oid.as_ref())
-
    }
-
}
deleted git-ext/src/reference.rs
@@ -1,26 +0,0 @@
-
// 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 as _;
-

-
mod iter;
-
pub use iter::{ReferenceNames, References};
-

-
pub mod name;
-
pub use name::{OneLevel, Qualified, RefLike, RefspecPattern};
-

-
pub mod check {
-
    pub use git_ref_format::{check_ref_format as ref_format, Error, Options};
-
}
-

-
pub fn peeled(head: git2::Reference) -> Option<(String, git2::Oid)> {
-
    head.name()
-
        .and_then(|name| head.target().map(|target| (name.to_owned(), target)))
-
}
-

-
pub fn refined((name, oid): (&str, git2::Oid)) -> Result<(OneLevel, crate::Oid), name::Error> {
-
    let name = RefLike::try_from(name)?;
-
    Ok((OneLevel::from(name), oid.into()))
-
}
deleted git-ext/src/reference/iter.rs
@@ -1,87 +0,0 @@
-
// 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.
-

-
/// Iterator chaining multiple [`git2::References`]
-
#[must_use = "iterators are lazy and do nothing unless consumed"]
-
pub struct References<'a> {
-
    inner: Vec<git2::References<'a>>,
-
}
-

-
impl<'a> References<'a> {
-
    pub fn new(refs: impl IntoIterator<Item = git2::References<'a>>) -> Self {
-
        Self {
-
            inner: refs.into_iter().collect(),
-
        }
-
    }
-

-
    pub fn from_globs(
-
        repo: &'a git2::Repository,
-
        globs: impl IntoIterator<Item = impl AsRef<str>>,
-
    ) -> Result<Self, git2::Error> {
-
        let globs = globs.into_iter();
-
        let mut iters = globs
-
            .size_hint()
-
            .1
-
            .map(Vec::with_capacity)
-
            .unwrap_or_else(Vec::new);
-
        for glob in globs {
-
            let iter = repo.references_glob(glob.as_ref())?;
-
            iters.push(iter);
-
        }
-

-
        Ok(Self::new(iters))
-
    }
-

-
    pub fn names<'b>(&'b mut self) -> ReferenceNames<'a, 'b> {
-
        ReferenceNames {
-
            inner: self.inner.iter_mut().map(|refs| refs.names()).collect(),
-
        }
-
    }
-

-
    pub fn peeled(self) -> impl Iterator<Item = (String, git2::Oid)> + 'a {
-
        self.filter_map(|reference| {
-
            reference.ok().and_then(|head| {
-
                head.name().and_then(|name| {
-
                    head.target()
-
                        .map(|target| (name.to_owned(), target.to_owned()))
-
                })
-
            })
-
        })
-
    }
-
}
-

-
impl<'a> Iterator for References<'a> {
-
    type Item = Result<git2::Reference<'a>, git2::Error>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.inner.pop().and_then(|mut iter| match iter.next() {
-
            None => self.next(),
-
            Some(item) => {
-
                self.inner.push(iter);
-
                Some(item)
-
            },
-
        })
-
    }
-
}
-

-
/// Iterator chaining multiple [`git2::ReferenceNames`]
-
#[must_use = "iterators are lazy and do nothing unless consumed"]
-
pub struct ReferenceNames<'repo, 'references> {
-
    inner: Vec<git2::ReferenceNames<'repo, 'references>>,
-
}
-

-
impl<'a, 'b> Iterator for ReferenceNames<'a, 'b> {
-
    type Item = Result<&'b str, git2::Error>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.inner.pop().and_then(|mut iter| match iter.next() {
-
            None => self.next(),
-
            Some(item) => {
-
                self.inner.push(iter);
-
                Some(item)
-
            },
-
        })
-
    }
-
}
deleted git-ext/src/reference/name.rs
@@ -1,711 +0,0 @@
-
// 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},
-
    iter::FromIterator,
-
    ops::Deref,
-
    path::Path,
-
    str::{self, FromStr},
-
};
-

-
pub use percent_encoding::PercentEncode;
-
use thiserror::Error;
-

-
use super::check;
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum Error {
-
    #[error("invalid utf8")]
-
    Utf8,
-

-
    #[error("not a valid git ref name or pattern")]
-
    RefFormat(#[from] check::Error),
-
}
-

-
impl Error {
-
    pub const fn empty() -> Self {
-
        Self::RefFormat(check::Error::Empty)
-
    }
-
}
-

-
#[derive(Debug, Error)]
-
#[non_exhaustive]
-
pub enum StripPrefixError {
-
    #[error("prefix is equal to path")]
-
    ImproperPrefix,
-

-
    #[error("not prefixed by given path")]
-
    NotPrefix,
-
}
-

-
/// An owned path-like value which is a valid git refname.
-
///
-
/// See [`git-check-ref-format`] for what the rules for refnames are --
-
/// conversion functions behave as if `--allow-onelevel` was given.
-
/// Additionally, we impose the rule that the name must consist of valid utf8.
-
///
-
/// Note that refspec patterns (eg. "refs/heads/*") are not allowed (see
-
/// [`RefspecPattern`]), and that the maximum length of the name is 1024 bytes.
-
///
-
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
#[cfg_attr(
-
    feature = "serde",
-
    derive(serde::Serialize, serde::Deserialize),
-
    serde(into = "String", try_from = "String")
-
)]
-
pub struct RefLike(String);
-

-
impl RefLike {
-
    /// Append the path in `Other` to `self.
-
    pub fn join<Other: Into<Self>>(&self, other: Other) -> Self {
-
        Self(format!("{}/{}", self.0, other.into().0))
-
    }
-

-
    /// Append a [`RefspecPattern`], yielding a [`RefspecPattern`]
-
    pub fn with_pattern_suffix<Suf: Into<RefspecPattern>>(&self, suf: Suf) -> RefspecPattern {
-
        RefspecPattern(format!("{}/{}", self.0, suf.into().0))
-
    }
-

-
    /// Returns a [`RefLike`] that, when joined onto `base`, yields `self`.
-
    ///
-
    /// # Errors
-
    ///
-
    /// If `base` is not a prefix of `self`, or `base` equals the path in `self`
-
    /// (ie. the result would be the empty path, which is not a valid
-
    /// [`RefLike`]).
-
    pub fn strip_prefix<P: AsRef<str>>(&self, base: P) -> Result<Self, StripPrefixError> {
-
        let base = base.as_ref();
-
        let base = format!("{}/", base.strip_suffix('/').unwrap_or(base));
-
        self.0
-
            .strip_prefix(&base)
-
            .ok_or(StripPrefixError::NotPrefix)
-
            .and_then(|path| {
-
                if path.is_empty() {
-
                    Err(StripPrefixError::ImproperPrefix)
-
                } else {
-
                    Ok(Self(path.into()))
-
                }
-
            })
-
    }
-

-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-

-
    pub fn percent_encode(&self) -> PercentEncode {
-
        /// https://url.spec.whatwg.org/#fragment-percent-encode-set
-
        const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
-
            &percent_encoding::CONTROLS
-
                .add(b' ')
-
                .add(b'"')
-
                .add(b'<')
-
                .add(b'>')
-
                .add(b'`');
-

-
        /// https://url.spec.whatwg.org/#path-percent-encode-set
-
        const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
-
            .add(b'#')
-
            .add(b'?')
-
            .add(b'{')
-
            .add(b'}');
-

-
        percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
-
    }
-
}
-

-
impl Deref for RefLike {
-
    type Target = str;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<str> for RefLike {
-
    fn as_ref(&self) -> &str {
-
        &self.0
-
    }
-
}
-

-
impl TryFrom<&str> for RefLike {
-
    type Error = Error;
-

-
    fn try_from(s: &str) -> Result<Self, Self::Error> {
-
        check::ref_format(
-
            check::Options {
-
                allow_onelevel: true,
-
                allow_pattern: false,
-
            },
-
            s,
-
        )?;
-
        Ok(Self(s.to_owned()))
-
    }
-
}
-

-
impl TryFrom<&[u8]> for RefLike {
-
    type Error = Error;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-
        str::from_utf8(bytes)
-
            .or(Err(Error::Utf8))
-
            .and_then(Self::try_from)
-
    }
-
}
-

-
impl FromStr for RefLike {
-
    type Err = Error;
-

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

-
impl TryFrom<String> for RefLike {
-
    type Error = Error;
-

-
    fn try_from(s: String) -> Result<Self, Self::Error> {
-
        Self::try_from(s.as_str())
-
    }
-
}
-

-
impl TryFrom<&Path> for RefLike {
-
    type Error = Error;
-

-
    #[cfg(target_family = "windows")]
-
    fn try_from(p: &Path) -> Result<Self, Self::Error> {
-
        use std::{convert::TryInto as _, path::Component::Normal};
-

-
        p.components()
-
            .filter_map(|comp| match comp {
-
                Normal(s) => Some(s),
-
                _ => None,
-
            })
-
            .map(|os| os.to_str().ok_or(Error::Utf8))
-
            .collect::<Result<Vec<_>, Self::Error>>()?
-
            .join("/")
-
            .try_into()
-
    }
-

-
    #[cfg(target_family = "unix")]
-
    fn try_from(p: &Path) -> Result<Self, Self::Error> {
-
        Self::try_from(p.to_str().ok_or(Error::Utf8)?)
-
    }
-
}
-

-
impl From<&RefLike> for RefLike {
-
    fn from(me: &RefLike) -> Self {
-
        me.clone()
-
    }
-
}
-

-
impl From<git_ref_format::RefString> for RefLike {
-
    #[inline]
-
    fn from(r: git_ref_format::RefString) -> Self {
-
        Self(r.into())
-
    }
-
}
-

-
impl From<&git_ref_format::RefString> for RefLike {
-
    #[inline]
-
    fn from(r: &git_ref_format::RefString) -> Self {
-
        Self::from(r.as_refstr())
-
    }
-
}
-

-
impl From<&git_ref_format::RefStr> for RefLike {
-
    #[inline]
-
    fn from(r: &git_ref_format::RefStr) -> Self {
-
        Self(r.to_owned().into())
-
    }
-
}
-

-
impl From<RefLike> for String {
-
    fn from(RefLike(path): RefLike) -> Self {
-
        path
-
    }
-
}
-

-
impl FromIterator<Self> for RefLike {
-
    fn from_iter<T>(iter: T) -> Self
-
    where
-
        T: IntoIterator<Item = Self>,
-
    {
-
        Self(iter.into_iter().map(|x| x.0).collect::<Vec<_>>().join("/"))
-
    }
-
}
-

-
impl Display for RefLike {
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self.as_str())
-
    }
-
}
-

-
/// A [`RefLike`] without a "refs/" prefix.
-
///
-
/// Conversion functions strip the first **two** path components iff the path
-
/// starts with `refs/`.
-
///
-
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
-
/// [`Qualified`] form is accepted, and silently converted.
-
///
-
/// # Examples
-
///
-
/// ```rust
-
/// use std::convert::TryFrom;
-
/// use radicle_git_ext::reference::name::*;
-
///
-
/// assert_eq!(
-
///     &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
-
///     "next"
-
/// );
-
///
-
/// assert_eq!(
-
///     &*OneLevel::from(RefLike::try_from("refs/remotes/origin/it").unwrap()),
-
///     "origin/it"
-
/// );
-
///
-
/// assert_eq!(
-
///     &*OneLevel::from(RefLike::try_from("mistress").unwrap()),
-
///     "mistress"
-
/// );
-
///
-
/// assert_eq!(
-
///     OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/tags/grace").unwrap())),
-
///     (
-
///         OneLevel::from(RefLike::try_from("grace").unwrap()),
-
///         Some(RefLike::try_from("tags").unwrap())
-
///     ),
-
/// );
-
///
-
/// assert_eq!(
-
///     OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/remotes/origin/hopper").unwrap())),
-
///     (
-
///         OneLevel::from(RefLike::try_from("origin/hopper").unwrap()),
-
///         Some(RefLike::try_from("remotes").unwrap())
-
///     ),
-
/// );
-
///
-
/// assert_eq!(
-
///     OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/HEAD").unwrap())),
-
///     (OneLevel::from(RefLike::try_from("HEAD").unwrap()), None)
-
/// );
-
///
-
/// assert_eq!(
-
///     &*OneLevel::from(RefLike::try_from("origin/hopper").unwrap()).into_qualified(
-
///         RefLike::try_from("remotes").unwrap()
-
///     ),
-
///     "refs/remotes/origin/hopper",
-
/// );
-
/// ```
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
#[cfg_attr(
-
    feature = "serde",
-
    derive(serde::Serialize, serde::Deserialize),
-
    serde(into = "String", try_from = "RefLike")
-
)]
-
pub struct OneLevel(String);
-

-
impl OneLevel {
-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-

-
    pub fn from_qualified(Qualified(path): Qualified) -> (Self, Option<RefLike>) {
-
        let mut path = path.strip_prefix("refs/").unwrap_or(&path).split('/');
-
        match path.next() {
-
            Some(category) => {
-
                let category = RefLike(category.into());
-
                // check that the "category" is not the only component of the path
-
                match path.next() {
-
                    Some(head) => (
-
                        Self(
-
                            std::iter::once(head)
-
                                .chain(path)
-
                                .collect::<Vec<_>>()
-
                                .join("/"),
-
                        ),
-
                        Some(category),
-
                    ),
-
                    None => (Self::from(category), None),
-
                }
-
            },
-
            None => unreachable!(),
-
        }
-
    }
-

-
    pub fn into_qualified(self, category: RefLike) -> Qualified {
-
        Qualified(format!("refs/{}/{}", category, self))
-
    }
-
}
-

-
impl Deref for OneLevel {
-
    type Target = str;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<str> for OneLevel {
-
    fn as_ref(&self) -> &str {
-
        self
-
    }
-
}
-

-
impl From<RefLike> for OneLevel {
-
    fn from(RefLike(path): RefLike) -> Self {
-
        if path.starts_with("refs/") {
-
            Self(path.split('/').skip(2).collect::<Vec<_>>().join("/"))
-
        } else {
-
            Self(path)
-
        }
-
    }
-
}
-

-
impl From<Qualified> for OneLevel {
-
    fn from(Qualified(path): Qualified) -> Self {
-
        Self::from(RefLike(path))
-
    }
-
}
-

-
impl From<OneLevel> for RefLike {
-
    fn from(OneLevel(path): OneLevel) -> Self {
-
        Self(path)
-
    }
-
}
-

-
impl From<OneLevel> for String {
-
    fn from(OneLevel(path): OneLevel) -> Self {
-
        path
-
    }
-
}
-

-
impl Display for OneLevel {
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self.as_str())
-
    }
-
}
-

-
/// A [`RefLike`] **with** a "refs/" prefix.
-
///
-
/// Conversion functions will assume `refs/heads/` if the input was not
-
/// qualified.
-
///
-
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
-
/// [`OneLevel`] form is accepted, and silently converted.
-
///
-
/// # Examples
-
///
-
/// ```rust
-
/// use std::convert::TryFrom;
-
/// use radicle_git_ext::reference::name::*;
-
///
-
/// assert_eq!(
-
///     &*Qualified::from(RefLike::try_from("laplace").unwrap()),
-
///     "refs/heads/laplace"
-
/// );
-
///
-
/// assert_eq!(
-
///     &*Qualified::from(RefLike::try_from("refs/heads/pu").unwrap()),
-
///     "refs/heads/pu"
-
/// );
-
///
-
/// assert_eq!(
-
///     &*Qualified::from(RefLike::try_from("refs/tags/v6.6.6").unwrap()),
-
///     "refs/tags/v6.6.6"
-
/// );
-
/// ```
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
#[cfg_attr(
-
    feature = "serde",
-
    derive(serde::Serialize, serde::Deserialize),
-
    serde(into = "String", try_from = "RefLike")
-
)]
-
pub struct Qualified(String);
-

-
impl Qualified {
-
    pub fn as_str(&self) -> &str {
-
        &self.0
-
    }
-
}
-

-
impl Deref for Qualified {
-
    type Target = str;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<str> for Qualified {
-
    fn as_ref(&self) -> &str {
-
        &self.0
-
    }
-
}
-

-
impl From<RefLike> for Qualified {
-
    fn from(RefLike(path): RefLike) -> Self {
-
        if path.starts_with("refs/") {
-
            Self(path)
-
        } else {
-
            Self(format!("refs/heads/{}", path))
-
        }
-
    }
-
}
-

-
impl From<OneLevel> for Qualified {
-
    fn from(OneLevel(path): OneLevel) -> Self {
-
        Self::from(RefLike(path))
-
    }
-
}
-

-
impl From<Qualified> for RefLike {
-
    fn from(Qualified(path): Qualified) -> Self {
-
        Self(path)
-
    }
-
}
-

-
impl From<Qualified> for String {
-
    fn from(Qualified(path): Qualified) -> Self {
-
        path
-
    }
-
}
-

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

-
/// An owned, path-like value which is a valid refspec pattern.
-
///
-
/// Conversion functions behave as if `--allow-onelevel --refspec-pattern` where
-
/// given to [`git-check-ref-format`]. That is, most of the rules of [`RefLike`]
-
/// apply, but the path _may_ contain exactly one `*` character.
-
///
-
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
-
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
-
#[cfg_attr(
-
    feature = "serde",
-
    derive(serde::Serialize, serde::Deserialize),
-
    serde(into = "String", try_from = "String")
-
)]
-
pub struct RefspecPattern(String);
-

-
impl RefspecPattern {
-
    /// Append the `RefLike` to the `RefspecPattern`. This allows the creation
-
    /// of patterns where the `*` appears in the middle of the path, e.g.
-
    /// `refs/remotes/*/mfdoom`
-
    pub fn append(&self, refl: impl Into<RefLike>) -> Self {
-
        RefspecPattern(format!("{}/{}", self.0, refl.into()))
-
    }
-

-
    pub fn as_str(&self) -> &str {
-
        self.as_ref()
-
    }
-
}
-

-
impl From<&RefspecPattern> for RefspecPattern {
-
    fn from(pat: &RefspecPattern) -> Self {
-
        pat.clone()
-
    }
-
}
-

-
impl Deref for RefspecPattern {
-
    type Target = str;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl AsRef<str> for RefspecPattern {
-
    fn as_ref(&self) -> &str {
-
        self
-
    }
-
}
-

-
impl TryFrom<&str> for RefspecPattern {
-
    type Error = Error;
-

-
    fn try_from(s: &str) -> Result<Self, Self::Error> {
-
        check::ref_format(
-
            check::Options {
-
                allow_onelevel: true,
-
                allow_pattern: true,
-
            },
-
            s,
-
        )?;
-
        Ok(Self(s.to_owned()))
-
    }
-
}
-

-
impl TryFrom<&[u8]> for RefspecPattern {
-
    type Error = Error;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-
        str::from_utf8(bytes)
-
            .or(Err(Error::Utf8))
-
            .and_then(Self::try_from)
-
    }
-
}
-

-
impl FromStr for RefspecPattern {
-
    type Err = Error;
-

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

-
impl TryFrom<String> for RefspecPattern {
-
    type Error = Error;
-

-
    fn try_from(s: String) -> Result<Self, Self::Error> {
-
        Self::try_from(s.as_str())
-
    }
-
}
-

-
impl From<RefspecPattern> for String {
-
    fn from(RefspecPattern(path): RefspecPattern) -> Self {
-
        path
-
    }
-
}
-

-
impl Display for RefspecPattern {
-
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-
        f.write_str(self.as_str())
-
    }
-
}
-

-
// `RefLike`-likes can be coerced into `RefspecPattern`s
-

-
impl From<RefLike> for RefspecPattern {
-
    fn from(RefLike(path): RefLike) -> Self {
-
        Self(path)
-
    }
-
}
-

-
impl From<&RefLike> for RefspecPattern {
-
    fn from(RefLike(path): &RefLike) -> Self {
-
        Self(path.to_owned())
-
    }
-
}
-

-
impl From<OneLevel> for RefspecPattern {
-
    fn from(OneLevel(path): OneLevel) -> Self {
-
        Self(path)
-
    }
-
}
-

-
impl From<&OneLevel> for RefspecPattern {
-
    fn from(OneLevel(path): &OneLevel) -> Self {
-
        Self(path.to_owned())
-
    }
-
}
-

-
impl From<Qualified> for RefspecPattern {
-
    fn from(Qualified(path): Qualified) -> Self {
-
        Self(path)
-
    }
-
}
-

-
impl From<&Qualified> for RefspecPattern {
-
    fn from(Qualified(path): &Qualified) -> Self {
-
        Self(path.to_owned())
-
    }
-
}
-

-
impl From<git_ref_format::refspec::PatternString> for RefspecPattern {
-
    #[inline]
-
    fn from(r: git_ref_format::refspec::PatternString) -> Self {
-
        Self(r.into())
-
    }
-
}
-

-
impl From<&git_ref_format::refspec::PatternStr> for RefspecPattern {
-
    #[inline]
-
    fn from(r: &git_ref_format::refspec::PatternStr) -> Self {
-
        Self(r.to_owned().into())
-
    }
-
}
-

-
#[cfg(feature = "minicbor")]
-
mod minicbor_impls {
-
    use super::*;
-
    use minicbor::{
-
        decode,
-
        encode::{self, Write},
-
        Decode,
-
        Decoder,
-
        Encode,
-
        Encoder,
-
    };
-

-
    impl Encode for RefLike {
-
        fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
            e.str(self.as_str())?;
-
            Ok(())
-
        }
-
    }
-

-
    impl<'b> Decode<'b> for RefLike {
-
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
-
            let path = d.str()?;
-
            Self::try_from(path).or(Err(decode::Error::Message("invalid reflike")))
-
        }
-
    }
-

-
    impl minicbor::Encode for OneLevel {
-
        fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
-
            e.str(self.as_str())?;
-
            Ok(())
-
        }
-
    }
-

-
    impl<'b> Decode<'b> for OneLevel {
-
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
-
            let refl: RefLike = Decode::decode(d)?;
-
            Ok(Self::from(refl))
-
        }
-
    }
-

-
    impl Encode for Qualified {
-
        fn encode<W: encode::Write>(
-
            &self,
-
            e: &mut Encoder<W>,
-
        ) -> Result<(), encode::Error<W::Error>> {
-
            e.str(self.as_str())?;
-
            Ok(())
-
        }
-
    }
-

-
    impl<'b> Decode<'b> for Qualified {
-
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
-
            let refl: RefLike = Decode::decode(d)?;
-
            Ok(Self::from(refl))
-
        }
-
    }
-

-
    impl Encode for RefspecPattern {
-
        fn encode<W: encode::Write>(
-
            &self,
-
            e: &mut Encoder<W>,
-
        ) -> Result<(), encode::Error<W::Error>> {
-
            e.str(self.as_str())?;
-
            Ok(())
-
        }
-
    }
-

-
    impl<'b> Decode<'b> for RefspecPattern {
-
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
-
            Self::try_from(d.str()?).or(Err(decode::Error::Message("invalid refspec pattern")))
-
        }
-
    }
-
}
deleted git-ext/src/revwalk.rs
@@ -1,44 +0,0 @@
-
// 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 enum Start {
-
    Oid(git2::Oid),
-
    Ref(String),
-
}
-

-
pub struct FirstParent<'a> {
-
    inner: git2::Revwalk<'a>,
-
}
-

-
impl<'a> FirstParent<'a> {
-
    pub fn new(repo: &'a git2::Repository, start: Start) -> Result<Self, git2::Error> {
-
        let mut revwalk = repo.revwalk()?;
-
        revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
-
        revwalk.simplify_first_parent()?;
-

-
        match start {
-
            Start::Oid(oid) => revwalk.push(oid),
-
            Start::Ref(name) => revwalk.push_ref(&name),
-
        }?;
-

-
        Ok(Self { inner: revwalk })
-
    }
-

-
    pub fn reverse(mut self) -> Result<Self, git2::Error> {
-
        let mut sort = git2::Sort::TOPOLOGICAL;
-
        sort.insert(git2::Sort::REVERSE);
-
        self.inner.set_sorting(sort)?;
-
        Ok(self)
-
    }
-
}
-

-
impl<'a> IntoIterator for FirstParent<'a> {
-
    type Item = Result<git2::Oid, git2::Error>;
-
    type IntoIter = git2::Revwalk<'a>;
-

-
    fn into_iter(self) -> Self::IntoIter {
-
        self.inner
-
    }
-
}
deleted git-ext/src/transport.rs
@@ -1,7 +0,0 @@
-
// 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 const UPLOAD_PACK_HEADER: &[u8] = b"001e# service=git-upload-pack\n0000";
-
pub const RECEIVE_PACK_HEADER: &[u8] = b"001f# service=git-receive-pack\n0000";
deleted git-ext/src/tree.rs
@@ -1,147 +0,0 @@
-
// 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, collections::BTreeMap, iter::FromIterator};
-

-
/// A simplified representation of a git tree, intended mainly to be created
-
/// from literals.
-
///
-
/// # Example
-
///
-
/// ```
-
/// use radicle_git_ext::tree::{Tree, blob};
-
///
-
/// let my_tree = vec![
-
///     ("README", blob(b"awe")),
-
///     ("src", vec![("main.rs", blob(b"fn main() {}"))].into_iter().collect()),
-
/// ]
-
/// .into_iter()
-
/// .collect::<Tree>();
-
///
-
/// assert_eq!(
-
///     format!("{:?}", my_tree),
-
///     "Tree(\
-
///         {\
-
///             \"README\": Blob(\
-
///                 [\
-
///                     97, \
-
///                     119, \
-
///                     101\
-
///                 ]\
-
///             ), \
-
///             \"src\": Tree(\
-
///                 Tree(\
-
///                     {\
-
///                         \"main.rs\": Blob(\
-
///                             [\
-
///                                 102, \
-
///                                 110, \
-
///                                 32, \
-
///                                 109, \
-
///                                 97, \
-
///                                 105, \
-
///                                 110, \
-
///                                 40, \
-
///                                 41, \
-
///                                 32, \
-
///                                 123, \
-
///                                 125\
-
///                             ]\
-
///                         )\
-
///                     }\
-
///                 )\
-
///             )\
-
///         }\
-
///     )"
-
/// )
-
/// ```
-
#[derive(Clone, Debug)]
-
pub struct Tree<'a>(BTreeMap<Cow<'a, str>, Node<'a>>);
-

-
impl Tree<'_> {
-
    pub fn write(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error> {
-
        use Node::*;
-

-
        let mut builder = repo.treebuilder(None)?;
-
        for (name, node) in &self.0 {
-
            match node {
-
                Blob(data) => {
-
                    let oid = repo.blob(data)?;
-
                    builder.insert(name.as_ref(), oid, git2::FileMode::Blob.into())?;
-
                },
-
                Tree(sub) => {
-
                    let oid = sub.write(repo)?;
-
                    builder.insert(name.as_ref(), oid, git2::FileMode::Tree.into())?;
-
                },
-
            }
-
        }
-

-
        builder.write()
-
    }
-
}
-

-
impl<'a> From<BTreeMap<Cow<'a, str>, Node<'a>>> for Tree<'a> {
-
    fn from(map: BTreeMap<Cow<'a, str>, Node<'a>>) -> Self {
-
        Self(map)
-
    }
-
}
-

-
impl<'a, K, N> FromIterator<(K, N)> for Tree<'a>
-
where
-
    K: Into<Cow<'a, str>>,
-
    N: Into<Node<'a>>,
-
{
-
    fn from_iter<T>(iter: T) -> Self
-
    where
-
        T: IntoIterator<Item = (K, N)>,
-
    {
-
        Self(
-
            iter.into_iter()
-
                .map(|(k, v)| (k.into(), v.into()))
-
                .collect(),
-
        )
-
    }
-
}
-

-
#[derive(Clone, Debug)]
-
pub enum Node<'a> {
-
    Blob(Cow<'a, [u8]>),
-
    Tree(Tree<'a>),
-
}
-

-
pub fn blob(slice: &[u8]) -> Node {
-
    Node::from(slice)
-
}
-

-
impl<'a> From<&'a [u8]> for Node<'a> {
-
    fn from(slice: &'a [u8]) -> Self {
-
        Self::from(Cow::Borrowed(slice))
-
    }
-
}
-

-
impl<'a> From<Cow<'a, [u8]>> for Node<'a> {
-
    fn from(bytes: Cow<'a, [u8]>) -> Self {
-
        Self::Blob(bytes)
-
    }
-
}
-

-
impl<'a> From<Tree<'a>> for Node<'a> {
-
    fn from(tree: Tree<'a>) -> Self {
-
        Self::Tree(tree)
-
    }
-
}
-

-
impl<'a, K, N> FromIterator<(K, N)> for Node<'a>
-
where
-
    K: Into<Cow<'a, str>>,
-
    N: Into<Node<'a>>,
-
{
-
    fn from_iter<T>(iter: T) -> Self
-
    where
-
        T: IntoIterator<Item = (K, N)>,
-
    {
-
        Self::Tree(iter.into_iter().collect())
-
    }
-
}
deleted git-ext/t/Cargo.toml
@@ -1,28 +0,0 @@
-
[package]
-
name = "git-ext-test"
-
version = "0.1.0"
-
edition = "2021"
-
license = "GPL-3.0-or-later"
-

-
publish = false
-

-
[lib]
-
doctest = false
-
test = true
-
doc = false
-

-
[features]
-
test = []
-

-
[dev-dependencies]
-
assert_matches = "1.5"
-
minicbor = "0.13"
-
serde = "1"
-
serde_json = "1"
-

-
[dev-dependencies.radicle-git-ext]
-
path = ".."
-
features = ["minicbor", "serde"]
-

-
[dev-dependencies.test-helpers]
-
path = "../../test/test-helpers"
deleted git-ext/t/src/lib.rs
@@ -1,9 +0,0 @@
-
// Copyright © 2022 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
#[cfg(test)]
-
#[macro_use]
-
extern crate assert_matches;
-

-
#[cfg(test)]
-
mod tests;
deleted git-ext/t/src/tests.rs
@@ -1,255 +0,0 @@
-
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
-
// SPDX-License-Identifier: GPL-3.0-or-later
-

-
use std::convert::TryFrom;
-

-
use radicle_git_ext::reference::{check, name::*};
-
use test_helpers::roundtrip;
-

-
mod common {
-
    use super::*;
-
    use std::fmt::Debug;
-

-
    pub fn invalid<T>()
-
    where
-
        T: TryFrom<&'static str, Error = Error> + Debug,
-
    {
-
        const INVALID: [&str; 16] = [
-
            ".hidden",
-
            "/etc/shadow",
-
            "@",
-
            "@{",
-
            "C:",
-
            "\\WORKGROUP",
-
            "foo.lock",
-
            "head^",
-
            "here/../../etc/shadow",
-
            "refs//heads/main",
-
            "refs/heads/",
-
            "shawn/ white",
-
            "the/dotted./quad",
-
            "wh?t",
-
            "x[a-z]",
-
            "~ommij",
-
        ];
-

-
        for v in INVALID {
-
            assert_matches!(T::try_from(v), Err(Error::RefFormat(_)), "input: {}", v)
-
        }
-
    }
-

-
    pub fn valid<T>()
-
    where
-
        T: TryFrom<&'static str, Error = Error> + AsRef<str> + Debug,
-
    {
-
        const VALID: [&str; 5] = [
-
            "\u{1F32F}",
-
            "cl@wn",
-
            "foo/bar",
-
            "master",
-
            "refs/heads/mistress",
-
        ];
-

-
        for v in VALID {
-
            assert_matches!(T::try_from(v), Ok(x) if x.as_ref() == v, "input: {}", v)
-
        }
-
    }
-

-
    pub fn empty<T>()
-
    where
-
        T: TryFrom<&'static str, Error = Error> + Debug,
-
    {
-
        assert_matches!(T::try_from(""), Err(Error::RefFormat(check::Error::Empty)))
-
    }
-

-
    pub fn nulsafe<T>()
-
    where
-
        T: TryFrom<&'static str, Error = Error> + Debug,
-
    {
-
        assert_matches!(
-
            T::try_from("jeff\0"),
-
            Err(Error::RefFormat(check::Error::InvalidChar('\0')))
-
        )
-
    }
-
}
-

-
mod reflike {
-
    use super::*;
-

-
    #[test]
-
    fn empty() {
-
        common::empty::<RefLike>()
-
    }
-

-
    #[test]
-
    fn valid() {
-
        common::valid::<RefLike>()
-
    }
-

-
    #[test]
-
    fn invalid() {
-
        common::invalid::<RefLike>()
-
    }
-

-
    #[test]
-
    fn nulsafe() {
-
        common::nulsafe::<RefLike>()
-
    }
-

-
    #[test]
-
    fn globstar_invalid() {
-
        assert_matches!(
-
            RefLike::try_from("refs/heads/*"),
-
            Err(Error::RefFormat(check::Error::InvalidChar('*')))
-
        )
-
    }
-

-
    #[test]
-
    fn into_onelevel() {
-
        assert_eq!(
-
            &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
-
            "next"
-
        )
-
    }
-

-
    #[test]
-
    fn into_heads() {
-
        assert_eq!(
-
            &*Qualified::from(RefLike::try_from("pu").unwrap()),
-
            "refs/heads/pu"
-
        )
-
    }
-

-
    #[test]
-
    fn serde() {
-
        let refl = RefLike::try_from("pu").unwrap();
-
        roundtrip::json(refl.clone());
-
        roundtrip::json(OneLevel::from(refl.clone()));
-
        roundtrip::json(Qualified::from(refl))
-
    }
-

-
    #[test]
-
    fn serde_invalid() {
-
        let json = serde_json::to_string("HEAD^").unwrap();
-
        assert!(serde_json::from_str::<RefLike>(&json).is_err());
-
        assert!(serde_json::from_str::<OneLevel>(&json).is_err());
-
        assert!(serde_json::from_str::<Qualified>(&json).is_err())
-
    }
-

-
    #[test]
-
    fn cbor() {
-
        let refl = RefLike::try_from("pu").unwrap();
-
        roundtrip::cbor(refl.clone());
-
        roundtrip::cbor(OneLevel::from(refl.clone()));
-
        roundtrip::cbor(Qualified::from(refl))
-
    }
-

-
    #[test]
-
    fn cbor_invalid() {
-
        let cbor = minicbor::to_vec("HEAD^").unwrap();
-
        assert!(minicbor::decode::<RefLike>(&cbor).is_err());
-
        assert!(minicbor::decode::<OneLevel>(&cbor).is_err());
-
        assert!(minicbor::decode::<Qualified>(&cbor).is_err())
-
    }
-
}
-

-
mod pattern {
-
    use super::*;
-

-
    #[test]
-
    fn empty() {
-
        common::empty::<RefspecPattern>()
-
    }
-

-
    #[test]
-
    fn valid() {
-
        common::valid::<RefspecPattern>()
-
    }
-

-
    #[test]
-
    fn invalid() {
-
        common::invalid::<RefspecPattern>()
-
    }
-

-
    #[test]
-
    fn nulsafe() {
-
        common::nulsafe::<RefspecPattern>()
-
    }
-

-
    #[test]
-
    fn globstar_ok() {
-
        const GLOBBED: [&str; 7] = [
-
            "*",
-
            "fo*",
-
            "fo*/bar",
-
            "foo/*/bar",
-
            "foo/ba*",
-
            "foo/bar/*",
-
            "foo/b*r",
-
        ];
-

-
        for v in GLOBBED {
-
            assert_matches!(
-
                RefspecPattern::try_from(v),
-
                Ok(ref x) if x.as_str() == v,
-
                "input: {}", v
-
            )
-
        }
-
    }
-

-
    #[test]
-
    fn globstar_invalid() {
-
        const GLOBBED: [&str; 12] = [
-
            "**",
-
            "***",
-
            "*/*",
-
            "*/L/*",
-
            "fo*/*/bar",
-
            "fo*/ba*",
-
            "fo*/ba*/baz",
-
            "fo*/ba*/ba*",
-
            "fo*/bar/*",
-
            "foo/*/bar/*",
-
            "foo/*/bar/*/baz*",
-
            "foo/*/bar/*/baz/*",
-
        ];
-

-
        for v in GLOBBED {
-
            assert_matches!(
-
                RefspecPattern::try_from(v),
-
                Err(Error::RefFormat(check::Error::Pattern))
-
            )
-
        }
-
    }
-

-
    #[test]
-
    fn serde() {
-
        roundtrip::json(RefspecPattern::try_from("refs/heads/*").unwrap())
-
    }
-

-
    #[test]
-
    fn serde_invalid() {
-
        let json = serde_json::to_string("HEAD^").unwrap();
-
        assert!(serde_json::from_str::<RefspecPattern>(&json).is_err())
-
    }
-

-
    #[test]
-
    fn cbor() {
-
        roundtrip::cbor(RefspecPattern::try_from("refs/heads/*").unwrap())
-
    }
-

-
    #[test]
-
    fn cbor_invalid() {
-
        let cbor = minicbor::to_vec("HEAD^").unwrap();
-
        assert!(minicbor::decode::<RefspecPattern>(&cbor).is_err())
-
    }
-

-
    #[test]
-
    fn strip_prefix_works_for_different_ends() {
-
        let refl = RefLike::try_from("refs/heads/next").unwrap();
-
        assert_eq!(
-
            refl.strip_prefix("refs/heads").unwrap(),
-
            refl.strip_prefix("refs/heads/").unwrap()
-
        );
-
    }
-
}
deleted git-types/Cargo.toml
@@ -1,40 +0,0 @@
-
[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"]
deleted git-types/src/lib.rs
@@ -1,76 +0,0 @@
-
// 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
-
        }
-
    }
-
}
deleted git-types/src/namespace.rs
@@ -1,105 +0,0 @@
-
// 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
-
    }
-
}
deleted git-types/src/reference.rs
@@ -1,550 +0,0 @@
-
// 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,
-
        )
-
    }
-
}
deleted git-types/src/refspec.rs
@@ -1,201 +0,0 @@
-
// 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)
-
    }
-
}
deleted git-types/src/remote.rs
@@ -1,203 +0,0 @@
-
// 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
-
    }
-
}
deleted git-types/src/sealed.rs
@@ -1,8 +0,0 @@
-
// 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 {}
deleted git-types/src/urn.rs
@@ -1,447 +0,0 @@
-
// 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())
-
        }
-
    }
-
}
deleted macros/Cargo.toml
@@ -1,20 +0,0 @@
-
[package]
-
name = "radicle-macros"
-
version = "0.1.0"
-
authors = ["The Radicle Team <dev@radicle.xyz>"]
-
edition = "2018"
-
license = "GPL-3.0-or-later"
-
description = "Radicle procedural macros"
-

-
[lib]
-
doctest = false
-
proc-macro = true
-
test = false
-

-
[dependencies]
-
proc-macro-error = "1.0.4"
-
quote = "1"
-
syn = "1"
-

-
[dependencies.radicle-git-ext]
-
path = "../git-ext"
deleted macros/src/lib.rs
@@ -1,72 +0,0 @@
-
// 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.
-

-
#[macro_use]
-
extern crate proc_macro_error;
-

-
use std::convert::TryFrom;
-

-
use proc_macro::TokenStream;
-
use proc_macro_error::abort;
-
use quote::quote;
-
use syn::{parse_macro_input, LitStr};
-

-
use radicle_git_ext::reference::name::{RefLike, RefspecPattern};
-

-
/// Create `RefLike` from a string literal.
-
///
-
/// The string is validated at compile time, and an unsafe conversion is
-
/// emitted.
-
///
-
/// ```rust
-
/// use radicle_macros::reflike;
-
///
-
/// assert_eq!("lolek/bolek", reflike!("lolek/bolek").as_str())
-
/// ```
-
#[proc_macro_error]
-
#[proc_macro]
-
pub fn reflike(input: TokenStream) -> TokenStream {
-
    let lit = parse_macro_input!(input as LitStr);
-

-
    match RefLike::try_from(lit.value()) {
-
        Ok(safe) => {
-
            let safe: &str = &*safe;
-
            let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefLike>(#safe.to_owned()) }};
-
            TokenStream::from(expand)
-
        },
-

-
        Err(e) => {
-
            abort!(lit.span(), "invalid RefLike literal: {}", e);
-
        },
-
    }
-
}
-

-
/// Create a `RefspecPattern` from a string literal.
-
///
-
/// The string is validated at compile time, and an unsafe conversion is
-
/// emitted.
-
///
-
/// ```rust
-
/// use radicle_macros::refspec_pattern;
-
///
-
/// assert_eq!("refs/heads/*", refspec_pattern!("refs/heads/*").as_str())
-
/// ```
-
#[proc_macro_error]
-
#[proc_macro]
-
pub fn refspec_pattern(input: TokenStream) -> TokenStream {
-
    let lit = parse_macro_input!(input as LitStr);
-

-
    match RefspecPattern::try_from(lit.value()) {
-
        Ok(safe) => {
-
            let safe: &str = &*safe;
-
            let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefspecPattern>(#safe.to_owned()) }};
-
            TokenStream::from(expand)
-
        },
-

-
        Err(e) => {
-
            abort!(lit.span(), "invalid RefspecPattern literal: {}", e);
-
        },
-
    }
-
}
added radicle-git-ext/Cargo.toml
@@ -0,0 +1,41 @@
+
[package]
+
name = "radicle-git-ext"
+
version = "0.1.0"
+
authors = ["The Radicle Team <dev@radicle.xyz>"]
+
edition = "2018"
+
license = "GPL-3.0-or-later"
+
description = "Utilities and extensions to the git2 crate"
+

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

+
[dependencies]
+
multihash = "0.11"
+
percent-encoding = "2"
+
thiserror = "1"
+

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

+
[dependencies.git-ref-format]
+
path = "../git-ref-format"
+

+
[dependencies.link-git]
+
path = "../link-git"
+
optional = true
+

+
[dependencies.minicbor]
+
version = "0.13"
+
features = ["std"]
+
optional = true
+

+
[dependencies.serde]
+
version = "1"
+
features = ["derive"]
+
optional = true
+

+
[dependencies.radicle-std-ext]
+
path = "../radicle-std-ext"
added radicle-git-ext/src/blob.rs
@@ -0,0 +1,146 @@
+
// 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, path::Path};
+

+
use radicle_std_ext::result::ResultExt as _;
+
use thiserror::Error;
+

+
use crate::{error::is_not_found_err, revwalk};
+

+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum Error {
+
    #[error(transparent)]
+
    NotFound(#[from] NotFound),
+

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

+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum NotFound {
+
    #[error("blob with path {0} not found")]
+
    NoSuchBlob(String),
+

+
    #[error("branch {0} not found")]
+
    NoSuchBranch(String),
+

+
    #[error("object {0} not found")]
+
    NoSuchObject(git2::Oid),
+

+
    #[error("the supplied git2::Reference does not have a target")]
+
    NoRefTarget,
+
}
+

+
pub enum Branch<'a> {
+
    Name(Cow<'a, str>),
+
    Ref(git2::Reference<'a>),
+
}
+

+
impl<'a> From<&'a str> for Branch<'a> {
+
    fn from(s: &'a str) -> Self {
+
        Self::Name(Cow::Borrowed(s))
+
    }
+
}
+

+
impl<'a> From<String> for Branch<'a> {
+
    fn from(s: String) -> Self {
+
        Self::Name(Cow::Owned(s))
+
    }
+
}
+

+
impl<'a> From<git2::Reference<'a>> for Branch<'a> {
+
    fn from(r: git2::Reference<'a>) -> Self {
+
        Self::Ref(r)
+
    }
+
}
+

+
/// Conveniently read a [`git2::Blob`] from a starting point.
+
pub enum Blob<'a> {
+
    /// Look up the tip of the reference specified by [`Branch`], peel until a
+
    /// tree is found, and traverse the tree along the given [`Path`] until
+
    /// the blob is found.
+
    Tip { branch: Branch<'a>, path: &'a Path },
+
    /// Traverse the history from the tip of [`Branch`] along the first parent
+
    /// until a commit without parents is found. Try to get the blob in that
+
    /// commit's tree at [`Path`].
+
    Init { branch: Branch<'a>, path: &'a Path },
+
    /// Look up `object`, peel until a tree is found, and try to get at the blob
+
    /// at [`Path`].
+
    At { object: git2::Oid, path: &'a Path },
+
}
+

+
impl<'a> Blob<'a> {
+
    pub fn get(self, git: &'a git2::Repository) -> Result<git2::Blob<'a>, Error> {
+
        match self {
+
            Self::Tip { branch, path } => {
+
                let reference = match branch {
+
                    Branch::Name(name) => {
+
                        git.find_reference(&name).or_matches(is_not_found_err, || {
+
                            Err(Error::NotFound(NotFound::NoSuchBranch(
+
                                name.to_owned().to_string(),
+
                            )))
+
                        })
+
                    },
+

+
                    Branch::Ref(reference) => Ok(reference),
+
                }?;
+
                let tree = reference.peel_to_tree()?;
+
                blob(git, tree, path)
+
            },
+

+
            Self::Init { branch, path } => {
+
                let start = match branch {
+
                    Branch::Name(name) => Ok(revwalk::Start::Ref(name.to_string())),
+
                    Branch::Ref(reference) => {
+
                        match (reference.target(), reference.symbolic_target()) {
+
                            (Some(oid), _) => Ok(revwalk::Start::Oid(oid)),
+
                            (_, Some(sym)) => Ok(revwalk::Start::Ref(sym.to_string())),
+
                            (_, _) => Err(Error::NotFound(NotFound::NoRefTarget)),
+
                        }
+
                    },
+
                }?;
+

+
                let revwalk = revwalk::FirstParent::new(git, start)?.reverse()?;
+
                match revwalk.into_iter().next() {
+
                    None => Err(Error::NotFound(NotFound::NoSuchBlob(
+
                        path.display().to_string(),
+
                    ))),
+
                    Some(oid) => {
+
                        let oid = oid?;
+
                        let tree = git.find_commit(oid)?.tree()?;
+
                        blob(git, tree, path)
+
                    },
+
                }
+
            },
+

+
            Self::At { object, path } => {
+
                let tree = git
+
                    .find_object(object, None)
+
                    .or_matches(is_not_found_err, || {
+
                        Err(Error::NotFound(NotFound::NoSuchObject(object)))
+
                    })
+
                    .and_then(|obj| Ok(obj.peel_to_tree()?))?;
+
                blob(git, tree, path)
+
            },
+
        }
+
    }
+
}
+

+
fn blob<'a>(
+
    repo: &'a git2::Repository,
+
    tree: git2::Tree<'a>,
+
    path: &'a Path,
+
) -> Result<git2::Blob<'a>, Error> {
+
    let entry = tree.get_path(path).or_matches(is_not_found_err, || {
+
        Err(Error::NotFound(NotFound::NoSuchBlob(
+
            path.display().to_string(),
+
        )))
+
    })?;
+

+
    entry.to_object(repo)?.peel_to_blob().map_err(Error::from)
+
}
added radicle-git-ext/src/error.rs
@@ -0,0 +1,22 @@
+
// 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::Display, io};
+

+
pub fn is_not_found_err(e: &git2::Error) -> bool {
+
    e.code() == git2::ErrorCode::NotFound
+
}
+

+
pub fn is_exists_err(e: &git2::Error) -> bool {
+
    e.code() == git2::ErrorCode::Exists
+
}
+

+
pub fn into_git_err<E: Display>(e: E) -> git2::Error {
+
    git2::Error::from_str(&e.to_string())
+
}
+

+
pub fn into_io_err(e: git2::Error) -> io::Error {
+
    io::Error::new(io::ErrorKind::Other, e)
+
}
added radicle-git-ext/src/lib.rs
@@ -0,0 +1,22 @@
+
// 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.
+

+
//! Extensions and wrappers for `git2` types
+

+
pub mod blob;
+
pub mod error;
+
pub mod oid;
+
pub mod reference;
+
pub mod revwalk;
+
pub mod transport;
+
pub mod tree;
+

+
pub use blob::*;
+
pub use error::*;
+
pub use oid::*;
+
pub use reference::*;
+
pub use revwalk::*;
+
pub use transport::*;
+
pub use tree::Tree;
added radicle-git-ext/src/oid.rs
@@ -0,0 +1,233 @@
+
// 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},
+
    ops::Deref,
+
    str::FromStr,
+
};
+

+
use multihash::{Multihash, MultihashRef};
+
use thiserror::Error;
+

+
#[cfg(feature = "link-git")]
+
use link_git::hash as git_hash;
+

+
/// Serializable [`git2::Oid`]
+
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+
pub struct Oid(git2::Oid);
+

+
impl Oid {
+
    pub fn into_multihash(self) -> Multihash {
+
        self.into()
+
    }
+
}
+

+
#[cfg(feature = "serde")]
+
mod serde_impls {
+
    use super::*;
+
    use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
+

+
    impl Serialize for Oid {
+
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
        where
+
            S: Serializer,
+
        {
+
            self.0.to_string().serialize(serializer)
+
        }
+
    }
+

+
    impl<'de> Deserialize<'de> for Oid {
+
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
        where
+
            D: Deserializer<'de>,
+
        {
+
            struct OidVisitor;
+

+
            impl<'de> Visitor<'de> for OidVisitor {
+
                type Value = Oid;
+

+
                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+
                    write!(f, "a hexidecimal git2::Oid")
+
                }
+

+
                fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+
                where
+
                    E: serde::de::Error,
+
                {
+
                    s.parse().map_err(serde::de::Error::custom)
+
                }
+
            }
+

+
            deserializer.deserialize_str(OidVisitor)
+
        }
+
    }
+
}
+

+
#[cfg(feature = "minicbor")]
+
mod minicbor_impls {
+
    use super::*;
+
    use minicbor::{
+
        decode,
+
        encode::{self, Write},
+
        Decode,
+
        Decoder,
+
        Encode,
+
        Encoder,
+
    };
+

+
    impl Encode for Oid {
+
        fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
            e.bytes(Multihash::from(self).as_bytes())?;
+
            Ok(())
+
        }
+
    }
+

+
    impl<'b> Decode<'b> for Oid {
+
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
+
            let bytes = d.bytes()?;
+
            let mhash = MultihashRef::from_slice(bytes)
+
                .or(Err(decode::Error::Message("not a multihash")))?;
+
            Self::try_from(mhash).or(Err(decode::Error::Message("not a git oid")))
+
        }
+
    }
+
}
+

+
impl Deref for Oid {
+
    type Target = git2::Oid;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<git2::Oid> for Oid {
+
    fn as_ref(&self) -> &git2::Oid {
+
        self
+
    }
+
}
+

+
impl AsRef<[u8]> for Oid {
+
    fn as_ref(&self) -> &[u8] {
+
        self.as_bytes()
+
    }
+
}
+

+
#[cfg(feature = "link-git")]
+
impl AsRef<git_hash::oid> for Oid {
+
    fn as_ref(&self) -> &git_hash::oid {
+
        // SAFETY: checks the length of the slice, which we know is correct
+
        git_hash::oid::try_from(self.as_bytes()).unwrap()
+
    }
+
}
+

+
impl From<git2::Oid> for Oid {
+
    fn from(oid: git2::Oid) -> Self {
+
        Self(oid)
+
    }
+
}
+

+
impl From<Oid> for git2::Oid {
+
    fn from(oid: Oid) -> Self {
+
        oid.0
+
    }
+
}
+

+
#[cfg(feature = "link-git")]
+
impl From<git_hash::ObjectId> for Oid {
+
    fn from(git_hash::ObjectId::Sha1(bs): git_hash::ObjectId) -> Self {
+
        // SAFETY: checks the length of the slice, which we statically know
+
        Self(git2::Oid::from_bytes(&bs).unwrap())
+
    }
+
}
+

+
#[cfg(feature = "link-git")]
+
impl From<Oid> for git_hash::ObjectId {
+
    fn from(oid: Oid) -> Self {
+
        Self::from_20_bytes(oid.as_ref())
+
    }
+
}
+

+
#[cfg(feature = "link-git")]
+
impl<'a> From<&'a Oid> for &'a git_hash::oid {
+
    fn from(oid: &'a Oid) -> Self {
+
        oid.as_ref()
+
    }
+
}
+

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

+
impl TryFrom<&str> for Oid {
+
    type Error = git2::Error;
+

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

+
impl FromStr for Oid {
+
    type Err = git2::Error;
+

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

+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum FromMultihashError {
+
    #[error("invalid hash algorithm: expected Sha1, got {actual:?}")]
+
    AlgorithmMismatch { actual: multihash::Code },
+

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

+
impl TryFrom<Multihash> for Oid {
+
    type Error = FromMultihashError;
+

+
    fn try_from(mhash: Multihash) -> Result<Self, Self::Error> {
+
        Self::try_from(mhash.as_ref())
+
    }
+
}
+

+
impl TryFrom<MultihashRef<'_>> for Oid {
+
    type Error = FromMultihashError;
+

+
    fn try_from(mhash: MultihashRef) -> Result<Self, Self::Error> {
+
        if mhash.algorithm() != multihash::Code::Sha1 {
+
            return Err(Self::Error::AlgorithmMismatch {
+
                actual: mhash.algorithm(),
+
            });
+
        }
+

+
        Self::try_from(mhash.digest()).map_err(Self::Error::from)
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Oid {
+
    type Error = git2::Error;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+
        git2::Oid::from_bytes(bytes).map(Self)
+
    }
+
}
+

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

+
impl From<&Oid> for Multihash {
+
    fn from(oid: &Oid) -> Self {
+
        multihash::wrap(multihash::Code::Sha1, oid.as_ref())
+
    }
+
}
added radicle-git-ext/src/reference.rs
@@ -0,0 +1,26 @@
+
// 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 as _;
+

+
mod iter;
+
pub use iter::{ReferenceNames, References};
+

+
pub mod name;
+
pub use name::{OneLevel, Qualified, RefLike, RefspecPattern};
+

+
pub mod check {
+
    pub use git_ref_format::{check_ref_format as ref_format, Error, Options};
+
}
+

+
pub fn peeled(head: git2::Reference) -> Option<(String, git2::Oid)> {
+
    head.name()
+
        .and_then(|name| head.target().map(|target| (name.to_owned(), target)))
+
}
+

+
pub fn refined((name, oid): (&str, git2::Oid)) -> Result<(OneLevel, crate::Oid), name::Error> {
+
    let name = RefLike::try_from(name)?;
+
    Ok((OneLevel::from(name), oid.into()))
+
}
added radicle-git-ext/src/reference/iter.rs
@@ -0,0 +1,87 @@
+
// 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.
+

+
/// Iterator chaining multiple [`git2::References`]
+
#[must_use = "iterators are lazy and do nothing unless consumed"]
+
pub struct References<'a> {
+
    inner: Vec<git2::References<'a>>,
+
}
+

+
impl<'a> References<'a> {
+
    pub fn new(refs: impl IntoIterator<Item = git2::References<'a>>) -> Self {
+
        Self {
+
            inner: refs.into_iter().collect(),
+
        }
+
    }
+

+
    pub fn from_globs(
+
        repo: &'a git2::Repository,
+
        globs: impl IntoIterator<Item = impl AsRef<str>>,
+
    ) -> Result<Self, git2::Error> {
+
        let globs = globs.into_iter();
+
        let mut iters = globs
+
            .size_hint()
+
            .1
+
            .map(Vec::with_capacity)
+
            .unwrap_or_else(Vec::new);
+
        for glob in globs {
+
            let iter = repo.references_glob(glob.as_ref())?;
+
            iters.push(iter);
+
        }
+

+
        Ok(Self::new(iters))
+
    }
+

+
    pub fn names<'b>(&'b mut self) -> ReferenceNames<'a, 'b> {
+
        ReferenceNames {
+
            inner: self.inner.iter_mut().map(|refs| refs.names()).collect(),
+
        }
+
    }
+

+
    pub fn peeled(self) -> impl Iterator<Item = (String, git2::Oid)> + 'a {
+
        self.filter_map(|reference| {
+
            reference.ok().and_then(|head| {
+
                head.name().and_then(|name| {
+
                    head.target()
+
                        .map(|target| (name.to_owned(), target.to_owned()))
+
                })
+
            })
+
        })
+
    }
+
}
+

+
impl<'a> Iterator for References<'a> {
+
    type Item = Result<git2::Reference<'a>, git2::Error>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner.pop().and_then(|mut iter| match iter.next() {
+
            None => self.next(),
+
            Some(item) => {
+
                self.inner.push(iter);
+
                Some(item)
+
            },
+
        })
+
    }
+
}
+

+
/// Iterator chaining multiple [`git2::ReferenceNames`]
+
#[must_use = "iterators are lazy and do nothing unless consumed"]
+
pub struct ReferenceNames<'repo, 'references> {
+
    inner: Vec<git2::ReferenceNames<'repo, 'references>>,
+
}
+

+
impl<'a, 'b> Iterator for ReferenceNames<'a, 'b> {
+
    type Item = Result<&'b str, git2::Error>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner.pop().and_then(|mut iter| match iter.next() {
+
            None => self.next(),
+
            Some(item) => {
+
                self.inner.push(iter);
+
                Some(item)
+
            },
+
        })
+
    }
+
}
added radicle-git-ext/src/reference/name.rs
@@ -0,0 +1,711 @@
+
// 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},
+
    iter::FromIterator,
+
    ops::Deref,
+
    path::Path,
+
    str::{self, FromStr},
+
};
+

+
pub use percent_encoding::PercentEncode;
+
use thiserror::Error;
+

+
use super::check;
+

+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum Error {
+
    #[error("invalid utf8")]
+
    Utf8,
+

+
    #[error("not a valid git ref name or pattern")]
+
    RefFormat(#[from] check::Error),
+
}
+

+
impl Error {
+
    pub const fn empty() -> Self {
+
        Self::RefFormat(check::Error::Empty)
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
#[non_exhaustive]
+
pub enum StripPrefixError {
+
    #[error("prefix is equal to path")]
+
    ImproperPrefix,
+

+
    #[error("not prefixed by given path")]
+
    NotPrefix,
+
}
+

+
/// An owned path-like value which is a valid git refname.
+
///
+
/// See [`git-check-ref-format`] for what the rules for refnames are --
+
/// conversion functions behave as if `--allow-onelevel` was given.
+
/// Additionally, we impose the rule that the name must consist of valid utf8.
+
///
+
/// Note that refspec patterns (eg. "refs/heads/*") are not allowed (see
+
/// [`RefspecPattern`]), and that the maximum length of the name is 1024 bytes.
+
///
+
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
#[cfg_attr(
+
    feature = "serde",
+
    derive(serde::Serialize, serde::Deserialize),
+
    serde(into = "String", try_from = "String")
+
)]
+
pub struct RefLike(String);
+

+
impl RefLike {
+
    /// Append the path in `Other` to `self.
+
    pub fn join<Other: Into<Self>>(&self, other: Other) -> Self {
+
        Self(format!("{}/{}", self.0, other.into().0))
+
    }
+

+
    /// Append a [`RefspecPattern`], yielding a [`RefspecPattern`]
+
    pub fn with_pattern_suffix<Suf: Into<RefspecPattern>>(&self, suf: Suf) -> RefspecPattern {
+
        RefspecPattern(format!("{}/{}", self.0, suf.into().0))
+
    }
+

+
    /// Returns a [`RefLike`] that, when joined onto `base`, yields `self`.
+
    ///
+
    /// # Errors
+
    ///
+
    /// If `base` is not a prefix of `self`, or `base` equals the path in `self`
+
    /// (ie. the result would be the empty path, which is not a valid
+
    /// [`RefLike`]).
+
    pub fn strip_prefix<P: AsRef<str>>(&self, base: P) -> Result<Self, StripPrefixError> {
+
        let base = base.as_ref();
+
        let base = format!("{}/", base.strip_suffix('/').unwrap_or(base));
+
        self.0
+
            .strip_prefix(&base)
+
            .ok_or(StripPrefixError::NotPrefix)
+
            .and_then(|path| {
+
                if path.is_empty() {
+
                    Err(StripPrefixError::ImproperPrefix)
+
                } else {
+
                    Ok(Self(path.into()))
+
                }
+
            })
+
    }
+

+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+

+
    pub fn percent_encode(&self) -> PercentEncode {
+
        /// https://url.spec.whatwg.org/#fragment-percent-encode-set
+
        const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
+
            &percent_encoding::CONTROLS
+
                .add(b' ')
+
                .add(b'"')
+
                .add(b'<')
+
                .add(b'>')
+
                .add(b'`');
+

+
        /// https://url.spec.whatwg.org/#path-percent-encode-set
+
        const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
+
            .add(b'#')
+
            .add(b'?')
+
            .add(b'{')
+
            .add(b'}');
+

+
        percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
+
    }
+
}
+

+
impl Deref for RefLike {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for RefLike {
+
    fn as_ref(&self) -> &str {
+
        &self.0
+
    }
+
}
+

+
impl TryFrom<&str> for RefLike {
+
    type Error = Error;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        check::ref_format(
+
            check::Options {
+
                allow_onelevel: true,
+
                allow_pattern: false,
+
            },
+
            s,
+
        )?;
+
        Ok(Self(s.to_owned()))
+
    }
+
}
+

+
impl TryFrom<&[u8]> for RefLike {
+
    type Error = Error;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+
        str::from_utf8(bytes)
+
            .or(Err(Error::Utf8))
+
            .and_then(Self::try_from)
+
    }
+
}
+

+
impl FromStr for RefLike {
+
    type Err = Error;
+

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

+
impl TryFrom<String> for RefLike {
+
    type Error = Error;
+

+
    fn try_from(s: String) -> Result<Self, Self::Error> {
+
        Self::try_from(s.as_str())
+
    }
+
}
+

+
impl TryFrom<&Path> for RefLike {
+
    type Error = Error;
+

+
    #[cfg(target_family = "windows")]
+
    fn try_from(p: &Path) -> Result<Self, Self::Error> {
+
        use std::{convert::TryInto as _, path::Component::Normal};
+

+
        p.components()
+
            .filter_map(|comp| match comp {
+
                Normal(s) => Some(s),
+
                _ => None,
+
            })
+
            .map(|os| os.to_str().ok_or(Error::Utf8))
+
            .collect::<Result<Vec<_>, Self::Error>>()?
+
            .join("/")
+
            .try_into()
+
    }
+

+
    #[cfg(target_family = "unix")]
+
    fn try_from(p: &Path) -> Result<Self, Self::Error> {
+
        Self::try_from(p.to_str().ok_or(Error::Utf8)?)
+
    }
+
}
+

+
impl From<&RefLike> for RefLike {
+
    fn from(me: &RefLike) -> Self {
+
        me.clone()
+
    }
+
}
+

+
impl From<git_ref_format::RefString> for RefLike {
+
    #[inline]
+
    fn from(r: git_ref_format::RefString) -> Self {
+
        Self(r.into())
+
    }
+
}
+

+
impl From<&git_ref_format::RefString> for RefLike {
+
    #[inline]
+
    fn from(r: &git_ref_format::RefString) -> Self {
+
        Self::from(r.as_refstr())
+
    }
+
}
+

+
impl From<&git_ref_format::RefStr> for RefLike {
+
    #[inline]
+
    fn from(r: &git_ref_format::RefStr) -> Self {
+
        Self(r.to_owned().into())
+
    }
+
}
+

+
impl From<RefLike> for String {
+
    fn from(RefLike(path): RefLike) -> Self {
+
        path
+
    }
+
}
+

+
impl FromIterator<Self> for RefLike {
+
    fn from_iter<T>(iter: T) -> Self
+
    where
+
        T: IntoIterator<Item = Self>,
+
    {
+
        Self(iter.into_iter().map(|x| x.0).collect::<Vec<_>>().join("/"))
+
    }
+
}
+

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

+
/// A [`RefLike`] without a "refs/" prefix.
+
///
+
/// Conversion functions strip the first **two** path components iff the path
+
/// starts with `refs/`.
+
///
+
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
+
/// [`Qualified`] form is accepted, and silently converted.
+
///
+
/// # Examples
+
///
+
/// ```rust
+
/// use std::convert::TryFrom;
+
/// use radicle_git_ext::reference::name::*;
+
///
+
/// assert_eq!(
+
///     &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
+
///     "next"
+
/// );
+
///
+
/// assert_eq!(
+
///     &*OneLevel::from(RefLike::try_from("refs/remotes/origin/it").unwrap()),
+
///     "origin/it"
+
/// );
+
///
+
/// assert_eq!(
+
///     &*OneLevel::from(RefLike::try_from("mistress").unwrap()),
+
///     "mistress"
+
/// );
+
///
+
/// assert_eq!(
+
///     OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/tags/grace").unwrap())),
+
///     (
+
///         OneLevel::from(RefLike::try_from("grace").unwrap()),
+
///         Some(RefLike::try_from("tags").unwrap())
+
///     ),
+
/// );
+
///
+
/// assert_eq!(
+
///     OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/remotes/origin/hopper").unwrap())),
+
///     (
+
///         OneLevel::from(RefLike::try_from("origin/hopper").unwrap()),
+
///         Some(RefLike::try_from("remotes").unwrap())
+
///     ),
+
/// );
+
///
+
/// assert_eq!(
+
///     OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/HEAD").unwrap())),
+
///     (OneLevel::from(RefLike::try_from("HEAD").unwrap()), None)
+
/// );
+
///
+
/// assert_eq!(
+
///     &*OneLevel::from(RefLike::try_from("origin/hopper").unwrap()).into_qualified(
+
///         RefLike::try_from("remotes").unwrap()
+
///     ),
+
///     "refs/remotes/origin/hopper",
+
/// );
+
/// ```
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
#[cfg_attr(
+
    feature = "serde",
+
    derive(serde::Serialize, serde::Deserialize),
+
    serde(into = "String", try_from = "RefLike")
+
)]
+
pub struct OneLevel(String);
+

+
impl OneLevel {
+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+

+
    pub fn from_qualified(Qualified(path): Qualified) -> (Self, Option<RefLike>) {
+
        let mut path = path.strip_prefix("refs/").unwrap_or(&path).split('/');
+
        match path.next() {
+
            Some(category) => {
+
                let category = RefLike(category.into());
+
                // check that the "category" is not the only component of the path
+
                match path.next() {
+
                    Some(head) => (
+
                        Self(
+
                            std::iter::once(head)
+
                                .chain(path)
+
                                .collect::<Vec<_>>()
+
                                .join("/"),
+
                        ),
+
                        Some(category),
+
                    ),
+
                    None => (Self::from(category), None),
+
                }
+
            },
+
            None => unreachable!(),
+
        }
+
    }
+

+
    pub fn into_qualified(self, category: RefLike) -> Qualified {
+
        Qualified(format!("refs/{}/{}", category, self))
+
    }
+
}
+

+
impl Deref for OneLevel {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for OneLevel {
+
    fn as_ref(&self) -> &str {
+
        self
+
    }
+
}
+

+
impl From<RefLike> for OneLevel {
+
    fn from(RefLike(path): RefLike) -> Self {
+
        if path.starts_with("refs/") {
+
            Self(path.split('/').skip(2).collect::<Vec<_>>().join("/"))
+
        } else {
+
            Self(path)
+
        }
+
    }
+
}
+

+
impl From<Qualified> for OneLevel {
+
    fn from(Qualified(path): Qualified) -> Self {
+
        Self::from(RefLike(path))
+
    }
+
}
+

+
impl From<OneLevel> for RefLike {
+
    fn from(OneLevel(path): OneLevel) -> Self {
+
        Self(path)
+
    }
+
}
+

+
impl From<OneLevel> for String {
+
    fn from(OneLevel(path): OneLevel) -> Self {
+
        path
+
    }
+
}
+

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

+
/// A [`RefLike`] **with** a "refs/" prefix.
+
///
+
/// Conversion functions will assume `refs/heads/` if the input was not
+
/// qualified.
+
///
+
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
+
/// [`OneLevel`] form is accepted, and silently converted.
+
///
+
/// # Examples
+
///
+
/// ```rust
+
/// use std::convert::TryFrom;
+
/// use radicle_git_ext::reference::name::*;
+
///
+
/// assert_eq!(
+
///     &*Qualified::from(RefLike::try_from("laplace").unwrap()),
+
///     "refs/heads/laplace"
+
/// );
+
///
+
/// assert_eq!(
+
///     &*Qualified::from(RefLike::try_from("refs/heads/pu").unwrap()),
+
///     "refs/heads/pu"
+
/// );
+
///
+
/// assert_eq!(
+
///     &*Qualified::from(RefLike::try_from("refs/tags/v6.6.6").unwrap()),
+
///     "refs/tags/v6.6.6"
+
/// );
+
/// ```
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
#[cfg_attr(
+
    feature = "serde",
+
    derive(serde::Serialize, serde::Deserialize),
+
    serde(into = "String", try_from = "RefLike")
+
)]
+
pub struct Qualified(String);
+

+
impl Qualified {
+
    pub fn as_str(&self) -> &str {
+
        &self.0
+
    }
+
}
+

+
impl Deref for Qualified {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for Qualified {
+
    fn as_ref(&self) -> &str {
+
        &self.0
+
    }
+
}
+

+
impl From<RefLike> for Qualified {
+
    fn from(RefLike(path): RefLike) -> Self {
+
        if path.starts_with("refs/") {
+
            Self(path)
+
        } else {
+
            Self(format!("refs/heads/{}", path))
+
        }
+
    }
+
}
+

+
impl From<OneLevel> for Qualified {
+
    fn from(OneLevel(path): OneLevel) -> Self {
+
        Self::from(RefLike(path))
+
    }
+
}
+

+
impl From<Qualified> for RefLike {
+
    fn from(Qualified(path): Qualified) -> Self {
+
        Self(path)
+
    }
+
}
+

+
impl From<Qualified> for String {
+
    fn from(Qualified(path): Qualified) -> Self {
+
        path
+
    }
+
}
+

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

+
/// An owned, path-like value which is a valid refspec pattern.
+
///
+
/// Conversion functions behave as if `--allow-onelevel --refspec-pattern` where
+
/// given to [`git-check-ref-format`]. That is, most of the rules of [`RefLike`]
+
/// apply, but the path _may_ contain exactly one `*` character.
+
///
+
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
+
#[cfg_attr(
+
    feature = "serde",
+
    derive(serde::Serialize, serde::Deserialize),
+
    serde(into = "String", try_from = "String")
+
)]
+
pub struct RefspecPattern(String);
+

+
impl RefspecPattern {
+
    /// Append the `RefLike` to the `RefspecPattern`. This allows the creation
+
    /// of patterns where the `*` appears in the middle of the path, e.g.
+
    /// `refs/remotes/*/mfdoom`
+
    pub fn append(&self, refl: impl Into<RefLike>) -> Self {
+
        RefspecPattern(format!("{}/{}", self.0, refl.into()))
+
    }
+

+
    pub fn as_str(&self) -> &str {
+
        self.as_ref()
+
    }
+
}
+

+
impl From<&RefspecPattern> for RefspecPattern {
+
    fn from(pat: &RefspecPattern) -> Self {
+
        pat.clone()
+
    }
+
}
+

+
impl Deref for RefspecPattern {
+
    type Target = str;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl AsRef<str> for RefspecPattern {
+
    fn as_ref(&self) -> &str {
+
        self
+
    }
+
}
+

+
impl TryFrom<&str> for RefspecPattern {
+
    type Error = Error;
+

+
    fn try_from(s: &str) -> Result<Self, Self::Error> {
+
        check::ref_format(
+
            check::Options {
+
                allow_onelevel: true,
+
                allow_pattern: true,
+
            },
+
            s,
+
        )?;
+
        Ok(Self(s.to_owned()))
+
    }
+
}
+

+
impl TryFrom<&[u8]> for RefspecPattern {
+
    type Error = Error;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+
        str::from_utf8(bytes)
+
            .or(Err(Error::Utf8))
+
            .and_then(Self::try_from)
+
    }
+
}
+

+
impl FromStr for RefspecPattern {
+
    type Err = Error;
+

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

+
impl TryFrom<String> for RefspecPattern {
+
    type Error = Error;
+

+
    fn try_from(s: String) -> Result<Self, Self::Error> {
+
        Self::try_from(s.as_str())
+
    }
+
}
+

+
impl From<RefspecPattern> for String {
+
    fn from(RefspecPattern(path): RefspecPattern) -> Self {
+
        path
+
    }
+
}
+

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

+
// `RefLike`-likes can be coerced into `RefspecPattern`s
+

+
impl From<RefLike> for RefspecPattern {
+
    fn from(RefLike(path): RefLike) -> Self {
+
        Self(path)
+
    }
+
}
+

+
impl From<&RefLike> for RefspecPattern {
+
    fn from(RefLike(path): &RefLike) -> Self {
+
        Self(path.to_owned())
+
    }
+
}
+

+
impl From<OneLevel> for RefspecPattern {
+
    fn from(OneLevel(path): OneLevel) -> Self {
+
        Self(path)
+
    }
+
}
+

+
impl From<&OneLevel> for RefspecPattern {
+
    fn from(OneLevel(path): &OneLevel) -> Self {
+
        Self(path.to_owned())
+
    }
+
}
+

+
impl From<Qualified> for RefspecPattern {
+
    fn from(Qualified(path): Qualified) -> Self {
+
        Self(path)
+
    }
+
}
+

+
impl From<&Qualified> for RefspecPattern {
+
    fn from(Qualified(path): &Qualified) -> Self {
+
        Self(path.to_owned())
+
    }
+
}
+

+
impl From<git_ref_format::refspec::PatternString> for RefspecPattern {
+
    #[inline]
+
    fn from(r: git_ref_format::refspec::PatternString) -> Self {
+
        Self(r.into())
+
    }
+
}
+

+
impl From<&git_ref_format::refspec::PatternStr> for RefspecPattern {
+
    #[inline]
+
    fn from(r: &git_ref_format::refspec::PatternStr) -> Self {
+
        Self(r.to_owned().into())
+
    }
+
}
+

+
#[cfg(feature = "minicbor")]
+
mod minicbor_impls {
+
    use super::*;
+
    use minicbor::{
+
        decode,
+
        encode::{self, Write},
+
        Decode,
+
        Decoder,
+
        Encode,
+
        Encoder,
+
    };
+

+
    impl Encode for RefLike {
+
        fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
            e.str(self.as_str())?;
+
            Ok(())
+
        }
+
    }
+

+
    impl<'b> Decode<'b> for RefLike {
+
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
+
            let path = d.str()?;
+
            Self::try_from(path).or(Err(decode::Error::Message("invalid reflike")))
+
        }
+
    }
+

+
    impl minicbor::Encode for OneLevel {
+
        fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
+
            e.str(self.as_str())?;
+
            Ok(())
+
        }
+
    }
+

+
    impl<'b> Decode<'b> for OneLevel {
+
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
+
            let refl: RefLike = Decode::decode(d)?;
+
            Ok(Self::from(refl))
+
        }
+
    }
+

+
    impl Encode for Qualified {
+
        fn encode<W: encode::Write>(
+
            &self,
+
            e: &mut Encoder<W>,
+
        ) -> Result<(), encode::Error<W::Error>> {
+
            e.str(self.as_str())?;
+
            Ok(())
+
        }
+
    }
+

+
    impl<'b> Decode<'b> for Qualified {
+
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
+
            let refl: RefLike = Decode::decode(d)?;
+
            Ok(Self::from(refl))
+
        }
+
    }
+

+
    impl Encode for RefspecPattern {
+
        fn encode<W: encode::Write>(
+
            &self,
+
            e: &mut Encoder<W>,
+
        ) -> Result<(), encode::Error<W::Error>> {
+
            e.str(self.as_str())?;
+
            Ok(())
+
        }
+
    }
+

+
    impl<'b> Decode<'b> for RefspecPattern {
+
        fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
+
            Self::try_from(d.str()?).or(Err(decode::Error::Message("invalid refspec pattern")))
+
        }
+
    }
+
}
added radicle-git-ext/src/revwalk.rs
@@ -0,0 +1,44 @@
+
// 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 enum Start {
+
    Oid(git2::Oid),
+
    Ref(String),
+
}
+

+
pub struct FirstParent<'a> {
+
    inner: git2::Revwalk<'a>,
+
}
+

+
impl<'a> FirstParent<'a> {
+
    pub fn new(repo: &'a git2::Repository, start: Start) -> Result<Self, git2::Error> {
+
        let mut revwalk = repo.revwalk()?;
+
        revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
+
        revwalk.simplify_first_parent()?;
+

+
        match start {
+
            Start::Oid(oid) => revwalk.push(oid),
+
            Start::Ref(name) => revwalk.push_ref(&name),
+
        }?;
+

+
        Ok(Self { inner: revwalk })
+
    }
+

+
    pub fn reverse(mut self) -> Result<Self, git2::Error> {
+
        let mut sort = git2::Sort::TOPOLOGICAL;
+
        sort.insert(git2::Sort::REVERSE);
+
        self.inner.set_sorting(sort)?;
+
        Ok(self)
+
    }
+
}
+

+
impl<'a> IntoIterator for FirstParent<'a> {
+
    type Item = Result<git2::Oid, git2::Error>;
+
    type IntoIter = git2::Revwalk<'a>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.inner
+
    }
+
}
added radicle-git-ext/src/transport.rs
@@ -0,0 +1,7 @@
+
// 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 const UPLOAD_PACK_HEADER: &[u8] = b"001e# service=git-upload-pack\n0000";
+
pub const RECEIVE_PACK_HEADER: &[u8] = b"001f# service=git-receive-pack\n0000";
added radicle-git-ext/src/tree.rs
@@ -0,0 +1,147 @@
+
// 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, collections::BTreeMap, iter::FromIterator};
+

+
/// A simplified representation of a git tree, intended mainly to be created
+
/// from literals.
+
///
+
/// # Example
+
///
+
/// ```
+
/// use radicle_git_ext::tree::{Tree, blob};
+
///
+
/// let my_tree = vec![
+
///     ("README", blob(b"awe")),
+
///     ("src", vec![("main.rs", blob(b"fn main() {}"))].into_iter().collect()),
+
/// ]
+
/// .into_iter()
+
/// .collect::<Tree>();
+
///
+
/// assert_eq!(
+
///     format!("{:?}", my_tree),
+
///     "Tree(\
+
///         {\
+
///             \"README\": Blob(\
+
///                 [\
+
///                     97, \
+
///                     119, \
+
///                     101\
+
///                 ]\
+
///             ), \
+
///             \"src\": Tree(\
+
///                 Tree(\
+
///                     {\
+
///                         \"main.rs\": Blob(\
+
///                             [\
+
///                                 102, \
+
///                                 110, \
+
///                                 32, \
+
///                                 109, \
+
///                                 97, \
+
///                                 105, \
+
///                                 110, \
+
///                                 40, \
+
///                                 41, \
+
///                                 32, \
+
///                                 123, \
+
///                                 125\
+
///                             ]\
+
///                         )\
+
///                     }\
+
///                 )\
+
///             )\
+
///         }\
+
///     )"
+
/// )
+
/// ```
+
#[derive(Clone, Debug)]
+
pub struct Tree<'a>(BTreeMap<Cow<'a, str>, Node<'a>>);
+

+
impl Tree<'_> {
+
    pub fn write(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error> {
+
        use Node::*;
+

+
        let mut builder = repo.treebuilder(None)?;
+
        for (name, node) in &self.0 {
+
            match node {
+
                Blob(data) => {
+
                    let oid = repo.blob(data)?;
+
                    builder.insert(name.as_ref(), oid, git2::FileMode::Blob.into())?;
+
                },
+
                Tree(sub) => {
+
                    let oid = sub.write(repo)?;
+
                    builder.insert(name.as_ref(), oid, git2::FileMode::Tree.into())?;
+
                },
+
            }
+
        }
+

+
        builder.write()
+
    }
+
}
+

+
impl<'a> From<BTreeMap<Cow<'a, str>, Node<'a>>> for Tree<'a> {
+
    fn from(map: BTreeMap<Cow<'a, str>, Node<'a>>) -> Self {
+
        Self(map)
+
    }
+
}
+

+
impl<'a, K, N> FromIterator<(K, N)> for Tree<'a>
+
where
+
    K: Into<Cow<'a, str>>,
+
    N: Into<Node<'a>>,
+
{
+
    fn from_iter<T>(iter: T) -> Self
+
    where
+
        T: IntoIterator<Item = (K, N)>,
+
    {
+
        Self(
+
            iter.into_iter()
+
                .map(|(k, v)| (k.into(), v.into()))
+
                .collect(),
+
        )
+
    }
+
}
+

+
#[derive(Clone, Debug)]
+
pub enum Node<'a> {
+
    Blob(Cow<'a, [u8]>),
+
    Tree(Tree<'a>),
+
}
+

+
pub fn blob(slice: &[u8]) -> Node {
+
    Node::from(slice)
+
}
+

+
impl<'a> From<&'a [u8]> for Node<'a> {
+
    fn from(slice: &'a [u8]) -> Self {
+
        Self::from(Cow::Borrowed(slice))
+
    }
+
}
+

+
impl<'a> From<Cow<'a, [u8]>> for Node<'a> {
+
    fn from(bytes: Cow<'a, [u8]>) -> Self {
+
        Self::Blob(bytes)
+
    }
+
}
+

+
impl<'a> From<Tree<'a>> for Node<'a> {
+
    fn from(tree: Tree<'a>) -> Self {
+
        Self::Tree(tree)
+
    }
+
}
+

+
impl<'a, K, N> FromIterator<(K, N)> for Node<'a>
+
where
+
    K: Into<Cow<'a, str>>,
+
    N: Into<Node<'a>>,
+
{
+
    fn from_iter<T>(iter: T) -> Self
+
    where
+
        T: IntoIterator<Item = (K, N)>,
+
    {
+
        Self::Tree(iter.into_iter().collect())
+
    }
+
}
added radicle-git-ext/t/Cargo.toml
@@ -0,0 +1,28 @@
+
[package]
+
name = "git-ext-test"
+
version = "0.1.0"
+
edition = "2021"
+
license = "GPL-3.0-or-later"
+

+
publish = false
+

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

+
[features]
+
test = []
+

+
[dev-dependencies]
+
assert_matches = "1.5"
+
minicbor = "0.13"
+
serde = "1"
+
serde_json = "1"
+

+
[dev-dependencies.radicle-git-ext]
+
path = ".."
+
features = ["minicbor", "serde"]
+

+
[dev-dependencies.test-helpers]
+
path = "../../test/test-helpers"
added radicle-git-ext/t/src/lib.rs
@@ -0,0 +1,9 @@
+
// Copyright © 2022 The Radicle Link Contributors
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
#[cfg(test)]
+
#[macro_use]
+
extern crate assert_matches;
+

+
#[cfg(test)]
+
mod tests;
added radicle-git-ext/t/src/tests.rs
@@ -0,0 +1,255 @@
+
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
+
// SPDX-License-Identifier: GPL-3.0-or-later
+

+
use std::convert::TryFrom;
+

+
use radicle_git_ext::reference::{check, name::*};
+
use test_helpers::roundtrip;
+

+
mod common {
+
    use super::*;
+
    use std::fmt::Debug;
+

+
    pub fn invalid<T>()
+
    where
+
        T: TryFrom<&'static str, Error = Error> + Debug,
+
    {
+
        const INVALID: [&str; 16] = [
+
            ".hidden",
+
            "/etc/shadow",
+
            "@",
+
            "@{",
+
            "C:",
+
            "\\WORKGROUP",
+
            "foo.lock",
+
            "head^",
+
            "here/../../etc/shadow",
+
            "refs//heads/main",
+
            "refs/heads/",
+
            "shawn/ white",
+
            "the/dotted./quad",
+
            "wh?t",
+
            "x[a-z]",
+
            "~ommij",
+
        ];
+

+
        for v in INVALID {
+
            assert_matches!(T::try_from(v), Err(Error::RefFormat(_)), "input: {}", v)
+
        }
+
    }
+

+
    pub fn valid<T>()
+
    where
+
        T: TryFrom<&'static str, Error = Error> + AsRef<str> + Debug,
+
    {
+
        const VALID: [&str; 5] = [
+
            "\u{1F32F}",
+
            "cl@wn",
+
            "foo/bar",
+
            "master",
+
            "refs/heads/mistress",
+
        ];
+

+
        for v in VALID {
+
            assert_matches!(T::try_from(v), Ok(x) if x.as_ref() == v, "input: {}", v)
+
        }
+
    }
+

+
    pub fn empty<T>()
+
    where
+
        T: TryFrom<&'static str, Error = Error> + Debug,
+
    {
+
        assert_matches!(T::try_from(""), Err(Error::RefFormat(check::Error::Empty)))
+
    }
+

+
    pub fn nulsafe<T>()
+
    where
+
        T: TryFrom<&'static str, Error = Error> + Debug,
+
    {
+
        assert_matches!(
+
            T::try_from("jeff\0"),
+
            Err(Error::RefFormat(check::Error::InvalidChar('\0')))
+
        )
+
    }
+
}
+

+
mod reflike {
+
    use super::*;
+

+
    #[test]
+
    fn empty() {
+
        common::empty::<RefLike>()
+
    }
+

+
    #[test]
+
    fn valid() {
+
        common::valid::<RefLike>()
+
    }
+

+
    #[test]
+
    fn invalid() {
+
        common::invalid::<RefLike>()
+
    }
+

+
    #[test]
+
    fn nulsafe() {
+
        common::nulsafe::<RefLike>()
+
    }
+

+
    #[test]
+
    fn globstar_invalid() {
+
        assert_matches!(
+
            RefLike::try_from("refs/heads/*"),
+
            Err(Error::RefFormat(check::Error::InvalidChar('*')))
+
        )
+
    }
+

+
    #[test]
+
    fn into_onelevel() {
+
        assert_eq!(
+
            &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
+
            "next"
+
        )
+
    }
+

+
    #[test]
+
    fn into_heads() {
+
        assert_eq!(
+
            &*Qualified::from(RefLike::try_from("pu").unwrap()),
+
            "refs/heads/pu"
+
        )
+
    }
+

+
    #[test]
+
    fn serde() {
+
        let refl = RefLike::try_from("pu").unwrap();
+
        roundtrip::json(refl.clone());
+
        roundtrip::json(OneLevel::from(refl.clone()));
+
        roundtrip::json(Qualified::from(refl))
+
    }
+

+
    #[test]
+
    fn serde_invalid() {
+
        let json = serde_json::to_string("HEAD^").unwrap();
+
        assert!(serde_json::from_str::<RefLike>(&json).is_err());
+
        assert!(serde_json::from_str::<OneLevel>(&json).is_err());
+
        assert!(serde_json::from_str::<Qualified>(&json).is_err())
+
    }
+

+
    #[test]
+
    fn cbor() {
+
        let refl = RefLike::try_from("pu").unwrap();
+
        roundtrip::cbor(refl.clone());
+
        roundtrip::cbor(OneLevel::from(refl.clone()));
+
        roundtrip::cbor(Qualified::from(refl))
+
    }
+

+
    #[test]
+
    fn cbor_invalid() {
+
        let cbor = minicbor::to_vec("HEAD^").unwrap();
+
        assert!(minicbor::decode::<RefLike>(&cbor).is_err());
+
        assert!(minicbor::decode::<OneLevel>(&cbor).is_err());
+
        assert!(minicbor::decode::<Qualified>(&cbor).is_err())
+
    }
+
}
+

+
mod pattern {
+
    use super::*;
+

+
    #[test]
+
    fn empty() {
+
        common::empty::<RefspecPattern>()
+
    }
+

+
    #[test]
+
    fn valid() {
+
        common::valid::<RefspecPattern>()
+
    }
+

+
    #[test]
+
    fn invalid() {
+
        common::invalid::<RefspecPattern>()
+
    }
+

+
    #[test]
+
    fn nulsafe() {
+
        common::nulsafe::<RefspecPattern>()
+
    }
+

+
    #[test]
+
    fn globstar_ok() {
+
        const GLOBBED: [&str; 7] = [
+
            "*",
+
            "fo*",
+
            "fo*/bar",
+
            "foo/*/bar",
+
            "foo/ba*",
+
            "foo/bar/*",
+
            "foo/b*r",
+
        ];
+

+
        for v in GLOBBED {
+
            assert_matches!(
+
                RefspecPattern::try_from(v),
+
                Ok(ref x) if x.as_str() == v,
+
                "input: {}", v
+
            )
+
        }
+
    }
+

+
    #[test]
+
    fn globstar_invalid() {
+
        const GLOBBED: [&str; 12] = [
+
            "**",
+
            "***",
+
            "*/*",
+
            "*/L/*",
+
            "fo*/*/bar",
+
            "fo*/ba*",
+
            "fo*/ba*/baz",
+
            "fo*/ba*/ba*",
+
            "fo*/bar/*",
+
            "foo/*/bar/*",
+
            "foo/*/bar/*/baz*",
+
            "foo/*/bar/*/baz/*",
+
        ];
+

+
        for v in GLOBBED {
+
            assert_matches!(
+
                RefspecPattern::try_from(v),
+
                Err(Error::RefFormat(check::Error::Pattern))
+
            )
+
        }
+
    }
+

+
    #[test]
+
    fn serde() {
+
        roundtrip::json(RefspecPattern::try_from("refs/heads/*").unwrap())
+
    }
+

+
    #[test]
+
    fn serde_invalid() {
+
        let json = serde_json::to_string("HEAD^").unwrap();
+
        assert!(serde_json::from_str::<RefspecPattern>(&json).is_err())
+
    }
+

+
    #[test]
+
    fn cbor() {
+
        roundtrip::cbor(RefspecPattern::try_from("refs/heads/*").unwrap())
+
    }
+

+
    #[test]
+
    fn cbor_invalid() {
+
        let cbor = minicbor::to_vec("HEAD^").unwrap();
+
        assert!(minicbor::decode::<RefspecPattern>(&cbor).is_err())
+
    }
+

+
    #[test]
+
    fn strip_prefix_works_for_different_ends() {
+
        let refl = RefLike::try_from("refs/heads/next").unwrap();
+
        assert_eq!(
+
            refl.strip_prefix("refs/heads").unwrap(),
+
            refl.strip_prefix("refs/heads/").unwrap()
+
        );
+
    }
+
}
added radicle-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 = "../radicle-git-ext"
+

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

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

+
[dependencies.serde]
+
version = "1.0"
+
features = ["derive"]
added radicle-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 radicle-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 radicle-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 radicle-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 radicle-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 radicle-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 radicle-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())
+
        }
+
    }
+
}
added radicle-macros/Cargo.toml
@@ -0,0 +1,20 @@
+
[package]
+
name = "radicle-macros"
+
version = "0.1.0"
+
authors = ["The Radicle Team <dev@radicle.xyz>"]
+
edition = "2018"
+
license = "GPL-3.0-or-later"
+
description = "Radicle procedural macros"
+

+
[lib]
+
doctest = false
+
proc-macro = true
+
test = false
+

+
[dependencies]
+
proc-macro-error = "1.0.4"
+
quote = "1"
+
syn = "1"
+

+
[dependencies.radicle-git-ext]
+
path = "../radicle-git-ext"
added radicle-macros/src/lib.rs
@@ -0,0 +1,72 @@
+
// 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.
+

+
#[macro_use]
+
extern crate proc_macro_error;
+

+
use std::convert::TryFrom;
+

+
use proc_macro::TokenStream;
+
use proc_macro_error::abort;
+
use quote::quote;
+
use syn::{parse_macro_input, LitStr};
+

+
use radicle_git_ext::reference::name::{RefLike, RefspecPattern};
+

+
/// Create `RefLike` from a string literal.
+
///
+
/// The string is validated at compile time, and an unsafe conversion is
+
/// emitted.
+
///
+
/// ```rust
+
/// use radicle_macros::reflike;
+
///
+
/// assert_eq!("lolek/bolek", reflike!("lolek/bolek").as_str())
+
/// ```
+
#[proc_macro_error]
+
#[proc_macro]
+
pub fn reflike(input: TokenStream) -> TokenStream {
+
    let lit = parse_macro_input!(input as LitStr);
+

+
    match RefLike::try_from(lit.value()) {
+
        Ok(safe) => {
+
            let safe: &str = &*safe;
+
            let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefLike>(#safe.to_owned()) }};
+
            TokenStream::from(expand)
+
        },
+

+
        Err(e) => {
+
            abort!(lit.span(), "invalid RefLike literal: {}", e);
+
        },
+
    }
+
}
+

+
/// Create a `RefspecPattern` from a string literal.
+
///
+
/// The string is validated at compile time, and an unsafe conversion is
+
/// emitted.
+
///
+
/// ```rust
+
/// use radicle_macros::refspec_pattern;
+
///
+
/// assert_eq!("refs/heads/*", refspec_pattern!("refs/heads/*").as_str())
+
/// ```
+
#[proc_macro_error]
+
#[proc_macro]
+
pub fn refspec_pattern(input: TokenStream) -> TokenStream {
+
    let lit = parse_macro_input!(input as LitStr);
+

+
    match RefspecPattern::try_from(lit.value()) {
+
        Ok(safe) => {
+
            let safe: &str = &*safe;
+
            let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefspecPattern>(#safe.to_owned()) }};
+
            TokenStream::from(expand)
+
        },
+

+
        Err(e) => {
+
            abort!(lit.span(), "invalid RefspecPattern literal: {}", e);
+
        },
+
    }
+
}
added radicle-std-ext/Cargo.toml
@@ -0,0 +1,15 @@
+
[package]
+
name = "radicle-std-ext"
+
version = "0.1.0"
+
authors = ["The Radicle Team <dev@radicle.xyz>"]
+
edition = "2018"
+
license = "GPL-3.0-or-later"
+
description = "Monkey patches of std types"
+

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

+
[features]
+
default = []
+
nightly = []
added radicle-std-ext/src/lib.rs
@@ -0,0 +1,19 @@
+
// 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.
+

+
#![cfg_attr(feature = "nightly", feature(try_trait_v2))]
+

+
pub mod ops;
+
pub mod result;
+

+
pub type Void = std::convert::Infallible;
+

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

+
    pub use super::Void;
+
    pub use ops::{FromResidual, Try};
+
    pub use result::ResultExt;
+
}
added radicle-std-ext/src/ops.rs
@@ -0,0 +1,173 @@
+
// Copyright © 2021 The Radicle Link Contributors
+
//
+
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
+
// Linking Exception. For full terms see the included LICENSE file.
+

+
//! Provides Try-trait for stable rust
+
//!
+
//! Probably doesn't work with `?`-desugaring. If the `nightly` feature is
+
//! enabled for this crate, the `std` version is enabled.
+

+
#[cfg(not(feature = "nightly"))]
+
pub use stable::{FromResidual, Try};
+
#[cfg(feature = "nightly")]
+
pub use std::ops::{FromResidual, Try};
+

+
mod stable {
+
    use std::{convert, ops::ControlFlow, task::Poll};
+

+
    pub trait Try: FromResidual {
+
        type Output;
+
        type Residual;
+

+
        fn from_output(output: Self::Output) -> Self;
+
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
+
    }
+

+
    pub trait FromResidual<R = <Self as Try>::Residual> {
+
        fn from_residual(residual: R) -> Self;
+
    }
+

+
    impl<B, C> Try for ControlFlow<B, C> {
+
        type Output = C;
+
        type Residual = ControlFlow<B, convert::Infallible>;
+

+
        #[inline]
+
        fn from_output(output: Self::Output) -> Self {
+
            ControlFlow::Continue(output)
+
        }
+

+
        #[inline]
+
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
+
            match self {
+
                ControlFlow::Continue(c) => ControlFlow::Continue(c),
+
                ControlFlow::Break(b) => ControlFlow::Break(ControlFlow::Break(b)),
+
            }
+
        }
+
    }
+

+
    impl<B, C> FromResidual for ControlFlow<B, C> {
+
        #[inline]
+
        fn from_residual(residual: ControlFlow<B, convert::Infallible>) -> Self {
+
            match residual {
+
                ControlFlow::Break(b) => ControlFlow::Break(b),
+
                _ => unreachable!(),
+
            }
+
        }
+
    }
+

+
    impl<T> Try for Option<T> {
+
        type Output = T;
+
        type Residual = Option<convert::Infallible>;
+

+
        #[inline]
+
        fn from_output(output: Self::Output) -> Self {
+
            Some(output)
+
        }
+

+
        #[inline]
+
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
+
            match self {
+
                Some(v) => ControlFlow::Continue(v),
+
                None => ControlFlow::Break(None),
+
            }
+
        }
+
    }
+

+
    impl<T> FromResidual for Option<T> {
+
        #[inline]
+
        fn from_residual(residual: Option<convert::Infallible>) -> Self {
+
            match residual {
+
                None => None,
+
                _ => unreachable!(),
+
            }
+
        }
+
    }
+

+
    impl<T, E> Try for Result<T, E> {
+
        type Output = T;
+
        type Residual = Result<convert::Infallible, E>;
+

+
        #[inline]
+
        fn from_output(output: Self::Output) -> Self {
+
            Ok(output)
+
        }
+

+
        #[inline]
+
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
+
            match self {
+
                Ok(v) => ControlFlow::Continue(v),
+
                Err(e) => ControlFlow::Break(Err(e)),
+
            }
+
        }
+
    }
+

+
    impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Result<T, F> {
+
        #[inline]
+
        fn from_residual(residual: Result<convert::Infallible, E>) -> Self {
+
            match residual {
+
                Err(e) => Err(From::from(e)),
+
                _ => unreachable!(),
+
            }
+
        }
+
    }
+

+
    impl<T, E> Try for Poll<Option<Result<T, E>>> {
+
        type Output = Poll<Option<T>>;
+
        type Residual = Result<convert::Infallible, E>;
+

+
        #[inline]
+
        fn from_output(c: Self::Output) -> Self {
+
            c.map(|x| x.map(Ok))
+
        }
+

+
        #[inline]
+
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
+
            match self {
+
                Poll::Ready(Some(Ok(x))) => ControlFlow::Continue(Poll::Ready(Some(x))),
+
                Poll::Ready(Some(Err(e))) => ControlFlow::Break(Err(e)),
+
                Poll::Ready(None) => ControlFlow::Continue(Poll::Ready(None)),
+
                Poll::Pending => ControlFlow::Continue(Poll::Pending),
+
            }
+
        }
+
    }
+

+
    impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Poll<Option<Result<T, F>>> {
+
        #[inline]
+
        fn from_residual(x: Result<convert::Infallible, E>) -> Self {
+
            match x {
+
                Err(e) => Poll::Ready(Some(Err(From::from(e)))),
+
                _ => unreachable!(),
+
            }
+
        }
+
    }
+

+
    impl<T, E> Try for Poll<Result<T, E>> {
+
        type Output = Poll<T>;
+
        type Residual = Result<convert::Infallible, E>;
+

+
        #[inline]
+
        fn from_output(c: Self::Output) -> Self {
+
            c.map(Ok)
+
        }
+

+
        #[inline]
+
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
+
            match self {
+
                Poll::Ready(Ok(x)) => ControlFlow::Continue(Poll::Ready(x)),
+
                Poll::Ready(Err(e)) => ControlFlow::Break(Err(e)),
+
                Poll::Pending => ControlFlow::Continue(Poll::Pending),
+
            }
+
        }
+
    }
+

+
    impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Poll<Result<T, F>> {
+
        #[inline]
+
        fn from_residual(x: Result<convert::Infallible, E>) -> Self {
+
            match x {
+
                Err(e) => Poll::Ready(Err(From::from(e))),
+
                _ => unreachable!(),
+
            }
+
        }
+
    }
+
}
added radicle-std-ext/src/result.rs
@@ -0,0 +1,40 @@
+
// 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 ResultExt<T, E> {
+
    /// Calls `f` if the result is [`Err`], **and** the predicate `pred` on the
+
    /// error value returns true. Otherwise returns the [`Ok`] value of
+
    /// `self`. Note that `f` may change the error type, so as long as the
+
    /// target type can be converted from the original one.
+
    ///
+
    /// # Examples
+
    ///
+
    /// ```
+
    /// use std::io;
+
    /// use radicle_std_ext::result::ResultExt as _;
+
    ///
+
    /// let res = Err(io::Error::new(io::ErrorKind::Other, "crashbug"))
+
    ///     .or_matches::<io::Error, _, _>(|e| matches!(e.kind(), io::ErrorKind::Other), || Ok(()))
+
    ///     .unwrap();
+
    ///
+
    /// assert_eq!((), res)
+
    /// ```
+
    fn or_matches<E2, P, F>(self, pred: P, f: F) -> Result<T, E2>
+
    where
+
        E2: From<E>,
+
        P: FnOnce(&E) -> bool,
+
        F: FnOnce() -> Result<T, E2>;
+
}
+

+
impl<T, E> ResultExt<T, E> for Result<T, E> {
+
    fn or_matches<E2, P, F>(self, pred: P, f: F) -> Result<T, E2>
+
    where
+
        E2: From<E>,
+
        P: FnOnce(&E) -> bool,
+
        F: FnOnce() -> Result<T, E2>,
+
    {
+
        self.or_else(|e| if pred(&e) { f() } else { Err(e.into()) })
+
    }
+
}
deleted std-ext/Cargo.toml
@@ -1,15 +0,0 @@
-
[package]
-
name = "radicle-std-ext"
-
version = "0.1.0"
-
authors = ["The Radicle Team <dev@radicle.xyz>"]
-
edition = "2018"
-
license = "GPL-3.0-or-later"
-
description = "Monkey patches of std types"
-

-
[lib]
-
doctest = false
-
test = false
-

-
[features]
-
default = []
-
nightly = []
deleted std-ext/src/lib.rs
@@ -1,19 +0,0 @@
-
// 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.
-

-
#![cfg_attr(feature = "nightly", feature(try_trait_v2))]
-

-
pub mod ops;
-
pub mod result;
-

-
pub type Void = std::convert::Infallible;
-

-
pub mod prelude {
-
    use super::*;
-

-
    pub use super::Void;
-
    pub use ops::{FromResidual, Try};
-
    pub use result::ResultExt;
-
}
deleted std-ext/src/ops.rs
@@ -1,173 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
-
// Linking Exception. For full terms see the included LICENSE file.
-

-
//! Provides Try-trait for stable rust
-
//!
-
//! Probably doesn't work with `?`-desugaring. If the `nightly` feature is
-
//! enabled for this crate, the `std` version is enabled.
-

-
#[cfg(not(feature = "nightly"))]
-
pub use stable::{FromResidual, Try};
-
#[cfg(feature = "nightly")]
-
pub use std::ops::{FromResidual, Try};
-

-
mod stable {
-
    use std::{convert, ops::ControlFlow, task::Poll};
-

-
    pub trait Try: FromResidual {
-
        type Output;
-
        type Residual;
-

-
        fn from_output(output: Self::Output) -> Self;
-
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
-
    }
-

-
    pub trait FromResidual<R = <Self as Try>::Residual> {
-
        fn from_residual(residual: R) -> Self;
-
    }
-

-
    impl<B, C> Try for ControlFlow<B, C> {
-
        type Output = C;
-
        type Residual = ControlFlow<B, convert::Infallible>;
-

-
        #[inline]
-
        fn from_output(output: Self::Output) -> Self {
-
            ControlFlow::Continue(output)
-
        }
-

-
        #[inline]
-
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
-
            match self {
-
                ControlFlow::Continue(c) => ControlFlow::Continue(c),
-
                ControlFlow::Break(b) => ControlFlow::Break(ControlFlow::Break(b)),
-
            }
-
        }
-
    }
-

-
    impl<B, C> FromResidual for ControlFlow<B, C> {
-
        #[inline]
-
        fn from_residual(residual: ControlFlow<B, convert::Infallible>) -> Self {
-
            match residual {
-
                ControlFlow::Break(b) => ControlFlow::Break(b),
-
                _ => unreachable!(),
-
            }
-
        }
-
    }
-

-
    impl<T> Try for Option<T> {
-
        type Output = T;
-
        type Residual = Option<convert::Infallible>;
-

-
        #[inline]
-
        fn from_output(output: Self::Output) -> Self {
-
            Some(output)
-
        }
-

-
        #[inline]
-
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
-
            match self {
-
                Some(v) => ControlFlow::Continue(v),
-
                None => ControlFlow::Break(None),
-
            }
-
        }
-
    }
-

-
    impl<T> FromResidual for Option<T> {
-
        #[inline]
-
        fn from_residual(residual: Option<convert::Infallible>) -> Self {
-
            match residual {
-
                None => None,
-
                _ => unreachable!(),
-
            }
-
        }
-
    }
-

-
    impl<T, E> Try for Result<T, E> {
-
        type Output = T;
-
        type Residual = Result<convert::Infallible, E>;
-

-
        #[inline]
-
        fn from_output(output: Self::Output) -> Self {
-
            Ok(output)
-
        }
-

-
        #[inline]
-
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
-
            match self {
-
                Ok(v) => ControlFlow::Continue(v),
-
                Err(e) => ControlFlow::Break(Err(e)),
-
            }
-
        }
-
    }
-

-
    impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Result<T, F> {
-
        #[inline]
-
        fn from_residual(residual: Result<convert::Infallible, E>) -> Self {
-
            match residual {
-
                Err(e) => Err(From::from(e)),
-
                _ => unreachable!(),
-
            }
-
        }
-
    }
-

-
    impl<T, E> Try for Poll<Option<Result<T, E>>> {
-
        type Output = Poll<Option<T>>;
-
        type Residual = Result<convert::Infallible, E>;
-

-
        #[inline]
-
        fn from_output(c: Self::Output) -> Self {
-
            c.map(|x| x.map(Ok))
-
        }
-

-
        #[inline]
-
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
-
            match self {
-
                Poll::Ready(Some(Ok(x))) => ControlFlow::Continue(Poll::Ready(Some(x))),
-
                Poll::Ready(Some(Err(e))) => ControlFlow::Break(Err(e)),
-
                Poll::Ready(None) => ControlFlow::Continue(Poll::Ready(None)),
-
                Poll::Pending => ControlFlow::Continue(Poll::Pending),
-
            }
-
        }
-
    }
-

-
    impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Poll<Option<Result<T, F>>> {
-
        #[inline]
-
        fn from_residual(x: Result<convert::Infallible, E>) -> Self {
-
            match x {
-
                Err(e) => Poll::Ready(Some(Err(From::from(e)))),
-
                _ => unreachable!(),
-
            }
-
        }
-
    }
-

-
    impl<T, E> Try for Poll<Result<T, E>> {
-
        type Output = Poll<T>;
-
        type Residual = Result<convert::Infallible, E>;
-

-
        #[inline]
-
        fn from_output(c: Self::Output) -> Self {
-
            c.map(Ok)
-
        }
-

-
        #[inline]
-
        fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
-
            match self {
-
                Poll::Ready(Ok(x)) => ControlFlow::Continue(Poll::Ready(x)),
-
                Poll::Ready(Err(e)) => ControlFlow::Break(Err(e)),
-
                Poll::Pending => ControlFlow::Continue(Poll::Pending),
-
            }
-
        }
-
    }
-

-
    impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Poll<Result<T, F>> {
-
        #[inline]
-
        fn from_residual(x: Result<convert::Infallible, E>) -> Self {
-
            match x {
-
                Err(e) => Poll::Ready(Err(From::from(e))),
-
                _ => unreachable!(),
-
            }
-
        }
-
    }
-
}
deleted std-ext/src/result.rs
@@ -1,40 +0,0 @@
-
// 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 ResultExt<T, E> {
-
    /// Calls `f` if the result is [`Err`], **and** the predicate `pred` on the
-
    /// error value returns true. Otherwise returns the [`Ok`] value of
-
    /// `self`. Note that `f` may change the error type, so as long as the
-
    /// target type can be converted from the original one.
-
    ///
-
    /// # Examples
-
    ///
-
    /// ```
-
    /// use std::io;
-
    /// use radicle_std_ext::result::ResultExt as _;
-
    ///
-
    /// let res = Err(io::Error::new(io::ErrorKind::Other, "crashbug"))
-
    ///     .or_matches::<io::Error, _, _>(|e| matches!(e.kind(), io::ErrorKind::Other), || Ok(()))
-
    ///     .unwrap();
-
    ///
-
    /// assert_eq!((), res)
-
    /// ```
-
    fn or_matches<E2, P, F>(self, pred: P, f: F) -> Result<T, E2>
-
    where
-
        E2: From<E>,
-
        P: FnOnce(&E) -> bool,
-
        F: FnOnce() -> Result<T, E2>;
-
}
-

-
impl<T, E> ResultExt<T, E> for Result<T, E> {
-
    fn or_matches<E2, P, F>(self, pred: P, f: F) -> Result<T, E2>
-
    where
-
        E2: From<E>,
-
        P: FnOnce(&E) -> bool,
-
        F: FnOnce() -> Result<T, E2>,
-
    {
-
        self.or_else(|e| if pred(&e) { f() } else { Err(e.into()) })
-
    }
-
}
modified test/Cargo.toml
@@ -12,7 +12,7 @@ test = true
doc = false

[dev-dependencies.git-ext-test]
-
path = "../git-ext/t"
+
path = "../radicle-git-ext/t"
features = ["test"]

[dev-dependencies.git-ref-format-test]
deleted test/it-helpers/Cargo.toml
@@ -1,48 +0,0 @@
-
[package]
-
name = "it-helpers"
-
version = "0.1.0"
-
edition = "2021"
-
license = "GPL-3.0-or-later"
-
publish = false
-

-
description = "Integration test helpers"
-

-
[lib]
-
doctest = false
-
test = false
-

-
[dependencies]
-
anyhow = "1"
-
futures = "0.3"
-
once_cell = "1.10"
-
tempfile = "3.3"
-
tokio = "1.13"
-
tracing = "0.1"
-
either = "1.6"
-

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

-
#
-
# workspace dependencies
-
#
-

-
[dependencies.git-ref-format]
-
path = "../../git-ref-format"
-

-
[dependencies.librad]
-
path = "../../librad"
-

-
[dependencies.link-async]
-
path = "../../link-async"
-

-
[dependencies.lnk-clib]
-
path = "../../cli/lnk-clib"
-

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

-
[dependencies.test-helpers]
-
path = "../test-helpers"
deleted test/it-helpers/src/git.rs
@@ -1,27 +0,0 @@
-
// 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 git_ref_format::Qualified;
-

-
#[tracing::instrument(skip(repo))]
-
pub fn create_commit(
-
    repo: &git2::Repository,
-
    on_branch: Qualified,
-
) -> Result<git2::Oid, git2::Error> {
-
    let empty_tree = {
-
        let mut index = repo.index()?;
-
        let oid = index.write_tree()?;
-
        repo.find_tree(oid).unwrap()
-
    };
-
    let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
-
    repo.commit(
-
        Some(on_branch.as_str()),
-
        &author,
-
        &author,
-
        "Initial commit",
-
        &empty_tree,
-
        &[],
-
    )
-
}
deleted test/it-helpers/src/lib.rs
@@ -1,10 +0,0 @@
-
#[macro_use]
-
extern crate tracing;
-

-
pub mod fixed;
-
pub mod git;
-
pub mod layout;
-
pub mod ssh;
-
pub mod testnet;
-
pub mod tmp;
-
pub mod working_copy;
deleted test/it-helpers/src/ssh.rs
@@ -1,68 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
//
-
// 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::process::{Command, Stdio};
-

-
use lnk_clib::keys::ssh::SshAuthSock;
-
use test_helpers::tempdir::WithTmpDir;
-

-
pub type TmpSshSock = WithTmpDir<SshAuthSock>;
-

-
pub fn ssh_auth_sock() -> TmpSshSock {
-
    WithTmpDir::new(|path| -> anyhow::Result<SshAuthSock> {
-
        let sock = path.join("tmp.sock");
-
        Ok(SshAuthSock::Uds(sock))
-
    })
-
    .unwrap()
-
}
-

-
/// Run a computation with a forked `ssh-agent` on a temporary file handle.
-
///
-
/// Once the computation is finished, the `ssh-agent` is killed by getting its
-
/// PID and running the equivalent of `SSH_AGENT_PID=<pid> ssh-agent -k`.
-
/// This is a best effort of resource cleanup, but has no guarantees if the
-
/// parsing of the PID or the killin of the agent fail.
-
pub fn with_ssh_agent<F, T>(callback: F) -> anyhow::Result<T>
-
where
-
    F: FnOnce(SshAuthSock) -> anyhow::Result<T>,
-
{
-
    let sock = ssh_auth_sock();
-
    let path = match &*sock {
-
        SshAuthSock::Uds(path) => path,
-
        _ => unreachable!(),
-
    };
-
    let agent = Command::new("ssh-agent").arg("-a").arg(path).output()?;
-
    anyhow::ensure!(agent.status.success(), agent.status);
-
    let pid = agent_pid(&agent.stdout)?;
-
    let res = callback((*sock).clone());
-
    kill_agent_pid(pid)?;
-
    res
-
}
-

-
/// Kill the ssh-agent running on the given PID.
-
fn kill_agent_pid(pid: &str) -> anyhow::Result<()> {
-
    debug!(pid = %pid, "killing ssh-agent");
-
    let status = Command::new("ssh-agent")
-
        .env("SSH_AGENT_PID", pid)
-
        .args(["-k"])
-
        .stdout(Stdio::null())
-
        .status()?;
-
    debug!(status = %status, "status of killing agent");
-
    Ok(())
-
}
-

-
/// Get the PID of the launched ssh-agent.
-
///
-
/// It gets the PID by stripping the output from the command using the text
-
/// `"echo Agent pid "`.
-
fn agent_pid(out: &[u8]) -> anyhow::Result<&str> {
-
    const PREFIX: &str = "SSH_AGENT_PID=";
-
    const SEP: u8 = b';';
-
    let pid = out
-
        .split(|b| b == &SEP)
-
        .find_map(|bs| std::str::from_utf8(bs).ok()?.trim().strip_prefix(PREFIX))
-
        .ok_or_else(|| anyhow::anyhow!("could not find SSH_AGENT_PID"))?;
-
    Ok(pid)
-
}
deleted test/it-helpers/src/tmp.rs
@@ -1,47 +0,0 @@
-
// Copyright © 2021 The Radicle Link Contributors
-
// SPDX-License-Identifier: GPLv3-or-later
-

-
use std::io;
-

-
use librad::{git::storage::Storage, paths::Paths, SecretKey};
-
use test_helpers::tempdir::WithTmpDir;
-

-
pub type TmpPaths = WithTmpDir<Paths>;
-

-
pub fn paths() -> TmpPaths {
-
    WithTmpDir::new(|path| -> Result<_, io::Error> {
-
        let paths = Paths::from_root(path)?;
-
        Ok::<_, io::Error>(paths)
-
    })
-
    .unwrap()
-
}
-

-
type TmpRepo = WithTmpDir<git2::Repository>;
-

-
pub fn repo() -> anyhow::Result<TmpRepo> {
-
    Ok(WithTmpDir::new(|path| {
-
        let setup = || {
-
            let repo = git2::Repository::init(path)?;
-

-
            // We need to set user info to _something_, but that doesn't have to
-
            // be valid, as we're using a shared repo with many keys
-
            let mut config = repo.config()?;
-
            config.set_str("user.name", "shared")?;
-
            config.set_str("user.email", "not.relevant@for.testing")?;
-
            Ok(repo)
-
        };
-
        setup().map_err(|e: git2::Error| io::Error::new(io::ErrorKind::Other, e))
-
    })?)
-
}
-

-
pub type TmpStorage = WithTmpDir<Storage>;
-

-
pub fn storage(signer: SecretKey) -> TmpStorage {
-
    WithTmpDir::new(|path| -> Result<_, io::Error> {
-
        let paths = Paths::from_root(path)?;
-
        let storage =
-
            Storage::open(&paths, signer).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
-
        Ok::<_, io::Error>(storage)
-
    })
-
    .unwrap()
-
}
deleted test/it-helpers/src/working_copy.rs
@@ -1,301 +0,0 @@
-
use git_ref_format::{lit, name, refspec, Qualified, RefStr, RefString};
-

-
use librad::{
-
    git::{
-
        local::url::LocalUrl,
-
        types::{
-
            remote::{LocalFetchspec, LocalPushspec},
-
            Fetchspec,
-
            Force,
-
            Refspec,
-
            Remote,
-
        },
-
    },
-
    git_ext as ext,
-
    net::{peer::Peer, protocol::RequestPullGuard},
-
    refspec_pattern,
-
    PeerId,
-
    Signer,
-
};
-

-
use crate::fixed::TestProject;
-

-
/// A remote in the working copy
-
pub enum WorkingRemote {
-
    /// A remote representing a remote peer, named `PeerId::encode_id`
-
    Peer(PeerId),
-
    /// A remote representing the local peer, named "rad"
-
    Rad,
-
}
-

-
impl From<PeerId> for WorkingRemote {
-
    fn from(p: PeerId) -> Self {
-
        WorkingRemote::Peer(p)
-
    }
-
}
-

-
impl WorkingRemote {
-
    fn fetchspec(&self) -> Fetchspec {
-
        match self {
-
            Self::Peer(peer_id) => {
-
                let name = RefString::try_from(format!("{}", peer_id)).expect("peer is refstring");
-
                let dst = RefString::from(Qualified::from(lit::refs_remotes(name.clone())))
-
                    .with_pattern(refspec::STAR);
-
                let src = RefString::from(Qualified::from(lit::refs_remotes(name)))
-
                    .and(name::HEADS)
-
                    .with_pattern(refspec::STAR);
-
                let refspec = Refspec {
-
                    src,
-
                    dst,
-
                    force: Force::True,
-
                };
-
                refspec.into_fetchspec()
-
            },
-
            Self::Rad => {
-
                let name = RefString::try_from("rad").unwrap();
-
                let src =
-
                    RefString::from_iter([name::REFS, name::HEADS]).with_pattern(refspec::STAR);
-
                Refspec {
-
                    src,
-
                    dst: RefString::from(Qualified::from(lit::refs_remotes(name)))
-
                        .with_pattern(refspec::STAR),
-
                    force: Force::True,
-
                }
-
                .into_fetchspec()
-
            },
-
        }
-
    }
-

-
    fn remote_ref(&self, branch: &RefStr) -> RefString {
-
        let name = match self {
-
            Self::Rad => name::RAD.to_owned(),
-
            Self::Peer(peer_id) => {
-
                RefString::try_from(peer_id.to_string()).expect("peer id is refstring")
-
            },
-
        };
-
        RefString::from(Qualified::from(lit::refs_remotes(name))).join(branch)
-
    }
-
}
-

-
/// A `WorkingCopy` for test driving interactions with the monorepo where one
-
/// needs to update the tree of a project.
-
///
-
/// Remotes are named after the peer ID, except in the case of the remote
-
/// representing the local Peer ID - which is called "rad".
-
pub struct WorkingCopy<'a, S, G> {
-
    repo: git2::Repository,
-
    _repo_path: tempfile::TempDir,
-
    peer: &'a Peer<S, G>,
-
    project: &'a TestProject,
-
}
-

-
impl<'a, S, G> WorkingCopy<'a, S, G>
-
where
-
    S: Signer + Clone,
-
    G: RequestPullGuard,
-
{
-
    /// Create a new working copy. This initializes a git repository and then
-
    /// fetches the state of the local peer into `refs/remotes/rad/*`.
-
    pub fn new(
-
        project: &'a TestProject,
-
        peer: &'a Peer<S, G>,
-
    ) -> Result<WorkingCopy<'a, S, G>, anyhow::Error> {
-
        let repo_path = tempfile::tempdir()?;
-
        let repo = git2::Repository::init(repo_path.as_ref())?;
-

-
        let mut copy = WorkingCopy {
-
            peer,
-
            project,
-
            repo,
-
            _repo_path: repo_path,
-
        };
-
        copy.fetch(WorkingRemote::Rad)?;
-
        Ok(copy)
-
    }
-

-
    /// Fetch changes from the monorepo into the working copy. The fetchspec
-
    /// used depends on the peer ID.
-
    ///
-
    /// * If `from` is `WorkingRemote::Peer` then `refs/remotes/<peer
-
    ///   ID>/refs/*:refs/remotes/<peer ID>/heads/*`
-
    /// * If `from` is `WorkingRemote::Rad` then
-
    ///   `refs/heads/*:refs/remotes/rad/*`
-
    ///
-
    /// I.e. changes from remote peers end up in a remote called
-
    /// `PeerId::encode_id` whilst changes from the local peer end up in a
-
    /// remote called "rad".
-
    pub fn fetch(&mut self, from: WorkingRemote) -> Result<(), anyhow::Error> {
-
        let fetchspec = from.fetchspec();
-
        let url = LocalUrl::from(self.project.project.urn());
-
        let mut remote = Remote::rad_remote(url, fetchspec);
-
        let _ = remote.fetch(self.peer.clone(), &self.repo, LocalFetchspec::Configured)?;
-
        Ok(())
-
    }
-

-
    /// Push changes from `refs/heads/*` to the local peer
-
    pub fn push(&mut self) -> Result<(), anyhow::Error> {
-
        let url = LocalUrl::from(self.project.project.urn());
-
        let name = RefString::try_from("rad").unwrap();
-
        let fetchspec = Refspec {
-
            src: RefString::from_iter([name::REFS, name::HEADS]).with_pattern(refspec::STAR),
-
            dst: RefString::from(Qualified::from(lit::refs_remotes(name)))
-
                .with_pattern(refspec::STAR),
-
            force: Force::True,
-
        }
-
        .into_fetchspec();
-
        let mut remote = Remote::rad_remote(url, fetchspec);
-
        let _ = remote.push(
-
            self.peer.clone(),
-
            &self.repo,
-
            LocalPushspec::Matching {
-
                pattern: refspec_pattern!("refs/heads/*"),
-
                force: Force::True,
-
            },
-
        )?;
-
        Ok(())
-
    }
-

-
    /// Create a new commit on top of whichever commit is the head of
-
    /// `on_branch`. If the branch does not exist this will create it.
-
    pub fn commit(
-
        &mut self,
-
        message: &str,
-
        on_branch: Qualified,
-
    ) -> Result<git2::Oid, anyhow::Error> {
-
        let branch_name = on_branch.non_empty_components().2;
-
        let parent = match self.repo.find_branch(&branch_name, git2::BranchType::Local) {
-
            Ok(b) => b.get().target().and_then(|o| self.repo.find_commit(o).ok()),
-
            Err(e) if ext::error::is_not_found_err(&e) => None,
-
            Err(e) => return Err(anyhow::Error::from(e)),
-
        };
-
        let empty_tree = {
-
            let mut index = self.repo.index()?;
-
            let oid = index.write_tree()?;
-
            self.repo.find_tree(oid).unwrap()
-
        };
-
        let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
-
        let parents = match &parent {
-
            Some(p) => vec![p],
-
            None => Vec::new(),
-
        };
-
        self.repo
-
            .commit(
-
                Some(&on_branch),
-
                &author,
-
                &author,
-
                message,
-
                &empty_tree,
-
                &parents,
-
            )
-
            .map_err(anyhow::Error::from)
-
    }
-

-
    pub fn commit_and_push(
-
        &mut self,
-
        message: &str,
-
        on_branch: Qualified,
-
    ) -> Result<git2::Oid, anyhow::Error> {
-
        let id = self.commit(message, on_branch)?;
-
        self.push()?;
-
        Ok(id)
-
    }
-

-
    /// Create a branch at `refs/heads/<branch>` which tracks the given remote.
-
    /// The remote branch name depends on `from`.
-
    ///
-
    /// * If `from` is `WorkingCopy::Rad` then `refs/remotes/rad/<branch>`
-
    /// * If `from` is `WorkingCopy::Peer(peer_id)` then `refs/remotes/<peer
-
    ///   id>/<branch>`
-
    pub fn create_remote_tracking_branch(
-
        &self,
-
        from: WorkingRemote,
-
        branch: &RefStr,
-
    ) -> Result<(), anyhow::Error> {
-
        let target = self
-
            .repo
-
            .find_reference(from.remote_ref(branch).as_str())?
-
            .target()
-
            .ok_or_else(|| anyhow::anyhow!("remote ref is not a direct reference"))?;
-
        let commit = self.repo.find_commit(target)?;
-
        self.repo.branch(branch.as_str(), &commit, false)?;
-
        Ok(())
-
    }
-

-
    /// Fast forward the local branch `refs/heads/<branch>` to whatever is
-
    /// pointed to by `refs/remotes/<remote>/<branch>`
-
    ///
-
    /// * If `from` is `WorkingRemote::Peer(peer_id)` then `remote` is
-
    ///   `peer_id.encode_id()`
-
    /// * If `from` is `WorkingRemote::Rad` then `remote` is `"rad"`
-
    ///
-
    /// # Errors
-
    ///
-
    /// * If the local branch does not exist
-
    /// * If the remote branch does not exist
-
    /// * If either of the branches does not point at a commit
-
    /// * If the remote branch is not a descendant of the local branch
-
    pub fn fast_forward_to(&self, from: WorkingRemote, branch: &RefStr) -> anyhow::Result<()> {
-
        let remote_ref = from.remote_ref(branch);
-
        let remote_target = self
-
            .repo
-
            .find_reference(&remote_ref)?
-
            .target()
-
            .ok_or_else(|| anyhow::anyhow!("remote ref had no target"))?;
-
        let local_ref = RefString::from(Qualified::from(lit::refs_heads(branch)));
-
        let local_target = self
-
            .repo
-
            .find_reference(&local_ref)?
-
            .target()
-
            .ok_or_else(|| anyhow::anyhow!("local ref had no target"))?;
-
        if !self.repo.graph_descendant_of(remote_target, local_target)? {
-
            anyhow::bail!("remote ref was not a descendant of local ref");
-
        } else {
-
            self.repo
-
                .reference(&local_ref, remote_target, true, "fast forward")?;
-
        }
-
        Ok(())
-
    }
-

-
    /// Create a new commit which merges `refs/heads/<branch>` and
-
    /// `refs/remotes/<remote>/<branch>`
-
    ///
-
    /// this will create a new commit with two parents, one for the remote
-
    /// branch and one for the local branch
-
    ///
-
    /// # Errors
-
    ///
-
    /// * If the remote branch does not exist
-
    /// * If the local branch does not exist
-
    /// * If either of the references does not point to a commit
-
    pub fn merge_remote(&self, remote: PeerId, branch: &RefStr) -> anyhow::Result<git2::Oid> {
-
        let peer_branch = WorkingRemote::Peer(remote).remote_ref(branch);
-
        let peer_commit = self
-
            .repo
-
            .find_reference(peer_branch.as_ref())?
-
            .peel_to_commit()?;
-
        let local_branch = Qualified::from(lit::refs_heads(branch));
-
        let local_commit = self
-
            .repo
-
            .find_reference(local_branch.as_ref())?
-
            .peel_to_commit()?;
-

-
        let message = format!("merge {} into {}", peer_branch, local_branch);
-
        let empty_tree = {
-
            let mut index = self.repo.index()?;
-
            let oid = index.write_tree()?;
-
            self.repo.find_tree(oid).unwrap()
-
        };
-
        let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
-
        let parents = vec![&peer_commit, &local_commit];
-
        self.repo
-
            .commit(
-
                Some(&local_branch),
-
                &author,
-
                &author,
-
                &message,
-
                &empty_tree,
-
                &parents,
-
            )
-
            .map_err(anyhow::Error::from)
-
    }
-
}