diff --git a/Cargo.toml b/Cargo.toml index ea69334..ba679d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "git-ext", "git-ref-format", "git-trailers", + "git-types", "link-git", "macros", # TODO: port gitd-lib over diff --git a/git-types/Cargo.toml b/git-types/Cargo.toml new file mode 100644 index 0000000..9598316 --- /dev/null +++ b/git-types/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "radicle-git-types" +version = "0.1.0" +authors = ["Kim Altintop ", "Fintan Halpenny "] +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"] diff --git a/git-types/src/lib.rs b/git-types/src/lib.rs new file mode 100644 index 0000000..f8fd255 --- /dev/null +++ b/git-types/src/lib.rs @@ -0,0 +1,76 @@ +// Copyright © 2019-2020 The 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 for Option> { + fn from(_flat: Flat) -> Self { + None + } +} + +/// Type specialised reference for the most common use within this crate. +pub type Reference = GenericRef, 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 for Force { + fn from(b: bool) -> Self { + if b { + Self::True + } else { + Self::False + } + } +} diff --git a/git-types/src/namespace.rs b/git-types/src/namespace.rs new file mode 100644 index 0000000..068e0e4 --- /dev/null +++ b/git-types/src/namespace.rs @@ -0,0 +1,105 @@ +// Copyright © 2019-2020 The 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 { + fn into_namespace(self) -> ext::RefLike { + self.into() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Namespace(Urn); + +impl AsNamespace for Namespace +where + R: urn::HasProtocol, + for<'a> &'a R: Into, +{ +} + +impl<'a, R> AsNamespace for &'a Namespace +where + R: urn::HasProtocol, + &'a R: Into, +{ +} + +impl From> for Namespace { + fn from(urn: Urn) -> Self { + Self(Urn { path: None, ..urn }) + } +} + +impl From<&Urn> for Namespace { + fn from(urn: &Urn) -> Self { + Self(Urn { + path: None, + id: urn.id.clone(), + }) + } +} + +impl Display for Namespace +where + R: urn::HasProtocol, + for<'a> &'a R: Into, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(ext::RefLike::from(self).as_str()) + } +} + +impl From> for ext::RefLike +where + R: urn::HasProtocol, + for<'a> &'a R: Into, +{ + fn from(ns: Namespace) -> Self { + Self::from(ns.0) + } +} + +impl<'a, R> From<&'a Namespace> for ext::RefLike +where + R: urn::HasProtocol, + &'a R: Into, +{ + fn from(ns: &'a Namespace) -> Self { + Self::from(&ns.0) + } +} + +impl From> for ext::RefspecPattern +where + R: urn::HasProtocol, + for<'a> &'a R: Into, +{ + fn from(ns: Namespace) -> Self { + ext::RefLike::from(ns).into() + } +} + +impl<'a, R> From<&'a Namespace> for ext::RefspecPattern +where + R: urn::HasProtocol, + &'a R: Into, +{ + fn from(ns: &'a Namespace) -> Self { + ext::RefLike::from(ns).into() + } +} + +impl From> for Urn { + fn from(ns: Namespace) -> Self { + ns.0 + } +} diff --git a/git-types/src/reference.rs b/git-types/src/reference.rs new file mode 100644 index 0000000..ea4f960 --- /dev/null +++ b/git-types/src/reference.rs @@ -0,0 +1,550 @@ +// Copyright © 2019-2020 The 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 { + 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 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 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` into `ext::RefLike` for any `R: +/// Into`. 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 {} + +impl AsRemote for ext::RefLike {} +impl AsRemote for &ext::RefLike {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Reference { + /// The remote portion of this reference. + pub remote: Option, + /// 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, +} + +// Polymorphic definitions +impl Reference +where + N: Clone, + R: Clone, + C: Clone, +{ + pub fn with_remote(self, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + ..self + } + } + + pub fn set_remote(&mut self, remote: impl Into>) { + self.remote = remote.into(); + } + + pub fn remote(&mut self, remote: impl Into>) -> &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(self, namespace: NN) -> Reference + where + NN: Into>, + 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>(self, name: S) -> Self { + Self { + name: name.into(), + ..self + } + } + + /// Set the named portion of this path. + pub fn set_name>(&mut self, name: S) { + self.name = name.into(); + } + + pub fn name>(&mut self, name: S) -> &mut Self { + self.set_name(name); + self + } +} + +// References with a `One` cardinality +impl Reference { + /// Find this particular reference. + pub fn find<'a>(&self, repo: &'a git2::Repository) -> Result, 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 + 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::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( + self, + source: Reference, + force: Force, + ) -> SymbolicRef, Self> + where + R: Clone, + N: Clone, + { + SymbolicRef { + source, + target: self, + force, + } + } + + /// Build a reference that points to: + /// * `refs/namespaces//refs/rad/id` + pub fn rad_id(namespace: impl Into>) -> Self { + Self { + remote: None, + category: RefsCategory::Rad, + name: reflike!("id"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs/namespaces//refs/rad/signed_refs` + /// * `refs/namespaces//refs/remote//rad/ + /// signed_refs` + pub fn rad_signed_refs(namespace: impl Into>, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Rad, + name: reflike!("signed_refs"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs/namespaces//refs/rad/self` + /// * `refs/namespaces//refs/remote//rad/self` + pub fn rad_self(namespace: impl Into>, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Rad, + name: reflike!("self"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs/namespaces//refs/heads/` + /// * `refs/namespaces//refs/remote//heads/ + pub fn head(namespace: impl Into>, remote: impl Into>, name: One) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Heads, + name, + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs/namespaces//refs/tags/` + /// * `refs/namespaces//refs/remote//tags/ + pub fn tag(namespace: impl Into>, remote: impl Into>, name: One) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Tags, + name, + namespace: namespace.into(), + } + } +} + +impl Display for Reference +where + for<'a> &'a N: AsNamespace, + for<'a> &'a R: AsRemote, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(Into::::into(self).as_str()) + } +} + +impl From> for ext::RefLike +where + for<'a> &'a N: AsNamespace, + for<'a> &'a R: AsRemote, +{ + fn from(r: Reference) -> Self { + Self::from(&r) + } +} + +impl<'a, N, R> From<&'a Reference> for ext::RefLike +where + &'a N: AsNamespace, + &'a R: AsRemote, +{ + fn from(r: &'a Reference) -> 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 From> for ext::RefspecPattern +where + for<'a> &'a N: AsNamespace, + for<'a> &'a R: AsRemote, +{ + fn from(r: Reference) -> Self { + Self::from(&r) + } +} + +impl<'a, N, R> From<&'a Reference> for ext::RefspecPattern +where + &'a N: AsNamespace, + &'a R: AsRemote, +{ + fn from(r: &'a Reference) -> Self { + Into::::into(r).into() + } +} + +// TODO(kim): what is this for? +#[allow(clippy::from_over_into)] +impl<'a, N, R> Into> for &'a Reference +where + Self: ToString, +{ + fn into(self) -> ext::blob::Branch<'a> { + ext::blob::Branch::from(self.to_string()) + } +} + +// References with a `Many` cardinality +impl Reference { + /// Get the iterator for these references. + pub fn references<'a>( + &self, + repo: &'a git2::Repository, + ) -> Result, git2::Error> + where + Self: ToString, + { + ext::References::from_globs(repo, &[self.to_string()]) + } + + /// Build a reference that points to: + /// * `refs[/namespaces//refs]/rad/ids/*` + pub fn rad_ids_glob(namespace: impl Into>) -> Self { + Self { + remote: None, + category: RefsCategory::Rad, + name: refspec_pattern!("ids/*"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs[/namespaces//refs][/remotes/]/heads/*` + pub fn heads(namespace: impl Into>, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Heads, + name: refspec_pattern!("*"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs[/namespaces/]/refs[/remotes/]/rad/*` + pub fn rads(namespace: impl Into>, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Rad, + name: refspec_pattern!("*"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs[/namespaces/]/refs[/remotes/]/tags/*` + pub fn tags(namespace: impl Into>, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Tags, + name: refspec_pattern!("*"), + namespace: namespace.into(), + } + } + + /// Build a reference that points to: + /// * `refs[/namespaces/]/refs[/remotes/]/notes/*` + pub fn notes(namespace: impl Into>, remote: impl Into>) -> 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/]/cobs/*` + pub fn cob(namespace: impl Into>, remote: impl Into>) -> Self { + Self { + remote: remote.into(), + category: RefsCategory::Cobs, + name: refspec_pattern!("*"), + namespace: namespace.into(), + } + } +} + +impl Display for Reference +where + for<'a> &'a N: AsNamespace, + for<'a> &'a R: AsRemote, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(Into::::into(self).as_str()) + } +} + +impl From> for ext::RefspecPattern +where + for<'a> &'a N: AsNamespace, + for<'a> &'a R: AsRemote, +{ + fn from(r: Reference) -> Self { + Self::from(&r) + } +} + +impl<'a, N, R> From<&'a Reference> for ext::RefspecPattern +where + &'a N: AsNamespace, + &'a R: AsRemote, +{ + fn from(r: &'a Reference) -> 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 { + /// 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 SymbolicRef { + /// 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::Error> + where + for<'b> &'b S: Into, + for<'b> &'b T: Into, + { + let source = Into::::into(&self.source); + let target = Into::::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, + ) + } +} diff --git a/git-types/src/refspec.rs b/git-types/src/refspec.rs new file mode 100644 index 0000000..69248db --- /dev/null +++ b/git-types/src/refspec.rs @@ -0,0 +1,201 @@ +// Copyright © 2019-2020 The 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 { + /// 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 Refspec { + pub fn into_fetchspec(self) -> Fetchspec + where + S: Into, + D: Into, + { + self.into() + } + + pub fn into_pushspec(self) -> Pushspec + where + S: Into, + D: Into, + { + self.into() + } +} + +impl Display for Refspec +where + for<'a> &'a S: Into, + for<'a> &'a D: Into, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.force.as_bool() { + f.write_str("+")?; + } + + let src = Into::::into(&self.src); + let dst = Into::::into(&self.dst); + + write!(f, "{}:{}", src, dst) + } +} + +impl TryFrom<&str> for Refspec { + type Error = ext::reference::name::Error; + + fn try_from(s: &str) -> Result { + 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 { + type Err = ext::reference::name::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl TryFrom<&str> for Refspec { + type Error = ext::reference::name::Error; + + fn try_from(s: &str) -> Result { + 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 { + type Err = ext::reference::name::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +#[derive(Debug)] +pub struct Fetchspec(Refspec); + +impl From> for Fetchspec +where + S: Into, + D: Into, +{ + fn from(spec: Refspec) -> 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 { + Refspec::try_from(s).map(Self) + } +} + +impl FromStr for Fetchspec { + type Err = ext::reference::name::Error; + + fn from_str(s: &str) -> Result { + 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); + +impl From> for Pushspec +where + S: Into, + D: Into, +{ + fn from(spec: Refspec) -> 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 { + Refspec::try_from(s).map(Self) + } +} + +impl FromStr for Pushspec { + type Err = ext::reference::name::Error; + + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl Display for Pushspec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/git-types/src/remote.rs b/git-types/src/remote.rs new file mode 100644 index 0000000..600125e --- /dev/null +++ b/git-types/src/remote.rs @@ -0,0 +1,203 @@ +// Copyright © 2019-2020 The 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), + + #[error("failed to parse refspec")] + Refspec(#[from] reference::name::Error), + + #[error(transparent)] + Git(#[from] git2::Error), +} + +#[derive(Debug)] +pub struct Remote { + /// 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//*`), but ... empty fetch specs. + pub fetchspecs: Vec, + /// The set of push specs to add upon creation. + pub pushspecs: Vec, +} + +impl Remote { + /// Create a `"rad"` remote with a single fetch spec. + pub fn rad_remote(url: Url, fetch_spec: Ref) -> Self + where + Ref: Into>, + Spec: Into, + { + 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(url: Url, name: R) -> Self + where + R: Into, + { + Self { + url, + name: name.into(), + fetchspecs: vec![], + pushspecs: vec![], + } + } + + /// Override the fetch specs. + pub fn with_fetchspecs(self, specs: I) -> Self + where + I: IntoIterator, + ::Item: Into, + { + Self { + fetchspecs: specs.into_iter().map(Into::into).collect(), + ..self + } + } + + /// Add a fetch spec. + pub fn add_fetchspec(&mut self, spec: impl Into) { + self.fetchspecs.push(spec.into()) + } + + /// Override the push specs. + pub fn with_pushspecs(self, specs: I) -> Self + where + I: IntoIterator, + ::Item: Into, + { + Self { + pushspecs: specs.into_iter().map(Into::into).collect(), + ..self + } + } + + /// Add a push spec. + pub fn add_pushspec(&mut self, spec: impl Into) { + 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::(is_exists_err, || Ok(()))?; + + { + let mut config = repo.config()?; + config + .remove_multivar(&format!("remote.{}.url", self.name), ".*") + .or_matches::(is_not_found_err, || Ok(()))?; + config + .remove_multivar(&format!("remote.{}.fetch", self.name), ".*") + .or_matches::(is_not_found_err, || Ok(()))?; + config + .remove_multivar(&format!("remote.{}.push", self.name), ".*") + .or_matches::(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, FindError> + where + Url: FromStr, + ::Err: std::error::Error + Send + Sync + 'static, + { + let git_remote = repo + .find_remote(name.as_str()) + .map(Some) + .or_matches::(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::>()?; + let pushspecs = remote + .push_refspecs()? + .into_iter() + .flatten() + .map(Pushspec::try_from) + .collect::>()?; + + Ok(Some(Self { + url, + name, + fetchspecs, + pushspecs, + })) + }, + } + } +} + +impl AsRef for Remote { + fn as_ref(&self) -> &Url { + &self.url + } +} diff --git a/git-types/src/sealed.rs b/git-types/src/sealed.rs new file mode 100644 index 0000000..05b1c56 --- /dev/null +++ b/git-types/src/sealed.rs @@ -0,0 +1,8 @@ +// Copyright © 2019-2020 The 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 {} diff --git a/git-types/src/urn.rs b/git-types/src/urn.rs new file mode 100644 index 0000000..44825c4 --- /dev/null +++ b/git-types/src/urn.rs @@ -0,0 +1,447 @@ +// Copyright © 2019-2020 The 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 { + #[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 { + #[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), + + #[error(transparent)] + Path(#[from] ext::reference::name::Error), + } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum FromStr { + #[error("missing {0}")] + Missing(&'static str), + + #[error("invalid namespace identifier: {0}, expected `rad::`")] + InvalidNID(String), + + #[error("invalid protocol: {0}, expected `rad::`")] + InvalidProto(String), + + #[error("invalid id: {id}, expected `rad::`")] + InvalidId { + id: String, + #[source] + source: DecodeId, + }, + + #[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( + &self, + e: &mut minicbor::Encoder, + ) -> Result<(), minicbor::encode::Error> { + match self { + Self::Git => e.u8(0), + }?; + + Ok(()) + } +} + +impl<'de> minicbor::Decode<'de> for SomeProtocol { + fn decode(d: &mut minicbor::Decoder) -> Result { + 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 { + match s { + "git" => Ok(SomeProtocol::Git), + _ => Err("unknown protocol"), + } + } +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Urn { + pub id: R, + pub path: Option, +} + +impl Urn { + 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, + { + multibase::encode(multibase::Base::Base32Z, (&self.id).into()) + } + + pub fn try_from_id(s: impl AsRef) -> Result> + where + R: TryFrom, + 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(self, f: F) -> Urn + where + F: FnOnce(R) -> S, + { + Urn { + id: f(self.id), + path: self.path, + } + } + + pub fn map_path(self, f: F) -> Self + where + F: FnOnce(Option) -> Option, + { + Self { + id: self.id, + path: f(self.path), + } + } + + pub fn with_path

(self, path: P) -> Self + where + P: Into>, + { + self.map_path(|_| path.into()) + } +} + +impl From for Urn { + fn from(r: R) -> Self { + Self::new(r) + } +} + +impl<'a, R: Clone> From> for Cow<'a, Urn> { + fn from(urn: Urn) -> Self { + Cow::Owned(urn) + } +} + +impl<'a, R: Clone> From<&'a Urn> for Cow<'a, Urn> { + fn from(urn: &'a Urn) -> Self { + Cow::Borrowed(urn) + } +} + +// FIXME: For some inexplicable reason, rustc rejects an impl for Urn, +// claiming that the blanket impl `impl TryFrom for T where U: Into` +// overlaps. We absolutely do not have `Into> for ext::RefLike`. +impl TryFrom for Urn { + type Error = error::FromRefLike; + + fn try_from(refl: ext::RefLike) -> Result { + 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::>().join("/"); + if path.is_empty() { + Ok(None) + } else { + ext::RefLike::try_from(path).map(Some) + } + }?; + + Ok(urn.with_path(path)) + } +} + +impl From> for ext::RefLike +where + R: HasProtocol, + for<'a> &'a R: Into, +{ + fn from(urn: Urn) -> 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> for ext::RefLike +where + R: HasProtocol, + &'a R: Into, +{ + fn from(urn: &'a Urn) -> 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> for git_ref_format::Component<'_> +where + R: HasProtocol, + &'a R: Into, +{ + #[inline] + fn from(urn: &'a Urn) -> 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 Display for Urn +where + R: HasProtocol, + for<'a> &'a R: Into, +{ + 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 FromStr for Urn +where + R: HasProtocol + TryFrom, + E: std::error::Error + Send + Sync + 'static, +{ + type Err = error::FromStr; + + fn from_str(s: &str) -> Result { + 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 serde::Serialize for Urn +where + R: HasProtocol, + for<'a> &'a R: Into, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de, R, E> serde::Deserialize<'de> for Urn +where + R: HasProtocol + TryFrom, + E: std::error::Error + Send + Sync + 'static, +{ + fn deserialize(deserializer: D) -> Result + 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 minicbor::Encode for Urn +where + R: HasProtocol, + for<'a> &'a R: Into, +{ + fn encode( + &self, + e: &mut minicbor::Encoder, + ) -> Result<(), minicbor::encode::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 +where + for<'a> R: HasProtocol + TryFrom>, +{ + fn decode(d: &mut minicbor::Decoder) -> Result { + 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`. + /// + /// 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 for FakeId { + fn from(sz: usize) -> FakeId { + Self(sz) + } + } + + impl From 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()) + } + } +}