mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-29 16:01:01 +08:00
git-types: port librad::git::types and link_identities::urn
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user