git-types: port librad::git::types and link_identities::urn

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Fintan Halpenny
2022-09-08 12:07:52 +01:00
parent 70b571b67b
commit b379909610
9 changed files with 1631 additions and 0 deletions
+76
View File
@@ -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
}
}
}
+105
View File
@@ -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
}
}
+550
View File
@@ -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,
)
}
}
+201
View File
@@ -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)
}
}
+203
View File
@@ -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
}
}
+8
View File
@@ -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 {}
+447
View File
@@ -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())
}
}
}