radicle-git init

Port over all git relevant parts of the radicle-link repository[0].
The packages that are kept are:
* git-ext
* git-ref-format
* git-trailers
* link-git
* macros
* std-ext
* test -- only the portions that test the above

The development and testing scripts are also added for local and CI
testing.

[0]: https://github.com/radicle-dev/radicle-link/
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Fintan Halpenny
2022-09-08 12:07:51 +01:00
parent c43420956c
commit 70b571b67b
112 changed files with 10341 additions and 7 deletions
+146
View File
@@ -0,0 +1,146 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{borrow::Cow, path::Path};
use radicle_std_ext::result::ResultExt as _;
use thiserror::Error;
use crate::{error::is_not_found_err, revwalk};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
NotFound(#[from] NotFound),
#[error(transparent)]
Git(#[from] git2::Error),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum NotFound {
#[error("blob with path {0} not found")]
NoSuchBlob(String),
#[error("branch {0} not found")]
NoSuchBranch(String),
#[error("object {0} not found")]
NoSuchObject(git2::Oid),
#[error("the supplied git2::Reference does not have a target")]
NoRefTarget,
}
pub enum Branch<'a> {
Name(Cow<'a, str>),
Ref(git2::Reference<'a>),
}
impl<'a> From<&'a str> for Branch<'a> {
fn from(s: &'a str) -> Self {
Self::Name(Cow::Borrowed(s))
}
}
impl<'a> From<String> for Branch<'a> {
fn from(s: String) -> Self {
Self::Name(Cow::Owned(s))
}
}
impl<'a> From<git2::Reference<'a>> for Branch<'a> {
fn from(r: git2::Reference<'a>) -> Self {
Self::Ref(r)
}
}
/// Conveniently read a [`git2::Blob`] from a starting point.
pub enum Blob<'a> {
/// Look up the tip of the reference specified by [`Branch`], peel until a
/// tree is found, and traverse the tree along the given [`Path`] until
/// the blob is found.
Tip { branch: Branch<'a>, path: &'a Path },
/// Traverse the history from the tip of [`Branch`] along the first parent
/// until a commit without parents is found. Try to get the blob in that
/// commit's tree at [`Path`].
Init { branch: Branch<'a>, path: &'a Path },
/// Look up `object`, peel until a tree is found, and try to get at the blob
/// at [`Path`].
At { object: git2::Oid, path: &'a Path },
}
impl<'a> Blob<'a> {
pub fn get(self, git: &'a git2::Repository) -> Result<git2::Blob<'a>, Error> {
match self {
Self::Tip { branch, path } => {
let reference = match branch {
Branch::Name(name) => {
git.find_reference(&name).or_matches(is_not_found_err, || {
Err(Error::NotFound(NotFound::NoSuchBranch(
name.to_owned().to_string(),
)))
})
},
Branch::Ref(reference) => Ok(reference),
}?;
let tree = reference.peel_to_tree()?;
blob(git, tree, path)
},
Self::Init { branch, path } => {
let start = match branch {
Branch::Name(name) => Ok(revwalk::Start::Ref(name.to_string())),
Branch::Ref(reference) => {
match (reference.target(), reference.symbolic_target()) {
(Some(oid), _) => Ok(revwalk::Start::Oid(oid)),
(_, Some(sym)) => Ok(revwalk::Start::Ref(sym.to_string())),
(_, _) => Err(Error::NotFound(NotFound::NoRefTarget)),
}
},
}?;
let revwalk = revwalk::FirstParent::new(git, start)?.reverse()?;
match revwalk.into_iter().next() {
None => Err(Error::NotFound(NotFound::NoSuchBlob(
path.display().to_string(),
))),
Some(oid) => {
let oid = oid?;
let tree = git.find_commit(oid)?.tree()?;
blob(git, tree, path)
},
}
},
Self::At { object, path } => {
let tree = git
.find_object(object, None)
.or_matches(is_not_found_err, || {
Err(Error::NotFound(NotFound::NoSuchObject(object)))
})
.and_then(|obj| Ok(obj.peel_to_tree()?))?;
blob(git, tree, path)
},
}
}
}
fn blob<'a>(
repo: &'a git2::Repository,
tree: git2::Tree<'a>,
path: &'a Path,
) -> Result<git2::Blob<'a>, Error> {
let entry = tree.get_path(path).or_matches(is_not_found_err, || {
Err(Error::NotFound(NotFound::NoSuchBlob(
path.display().to_string(),
)))
})?;
entry.to_object(repo)?.peel_to_blob().map_err(Error::from)
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{fmt::Display, io};
pub fn is_not_found_err(e: &git2::Error) -> bool {
e.code() == git2::ErrorCode::NotFound
}
pub fn is_exists_err(e: &git2::Error) -> bool {
e.code() == git2::ErrorCode::Exists
}
pub fn into_git_err<E: Display>(e: E) -> git2::Error {
git2::Error::from_str(&e.to_string())
}
pub fn into_io_err(e: git2::Error) -> io::Error {
io::Error::new(io::ErrorKind::Other, e)
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
//! Extensions and wrappers for `git2` types
pub mod blob;
pub mod error;
pub mod oid;
pub mod reference;
pub mod revwalk;
pub mod transport;
pub mod tree;
pub use blob::*;
pub use error::*;
pub use oid::*;
pub use reference::*;
pub use revwalk::*;
pub use transport::*;
pub use tree::Tree;
+233
View File
@@ -0,0 +1,233 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{
convert::TryFrom,
fmt::{self, Display},
ops::Deref,
str::FromStr,
};
use multihash::{Multihash, MultihashRef};
use thiserror::Error;
#[cfg(feature = "link-git")]
use link_git::hash as git_hash;
/// Serializable [`git2::Oid`]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Oid(git2::Oid);
impl Oid {
pub fn into_multihash(self) -> Multihash {
self.into()
}
}
#[cfg(feature = "serde")]
mod serde_impls {
use super::*;
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for Oid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.to_string().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Oid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct OidVisitor;
impl<'de> Visitor<'de> for OidVisitor {
type Value = Oid;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a hexidecimal git2::Oid")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
s.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(OidVisitor)
}
}
}
#[cfg(feature = "minicbor")]
mod minicbor_impls {
use super::*;
use minicbor::{
decode,
encode::{self, Write},
Decode,
Decoder,
Encode,
Encoder,
};
impl Encode for Oid {
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
e.bytes(Multihash::from(self).as_bytes())?;
Ok(())
}
}
impl<'b> Decode<'b> for Oid {
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
let bytes = d.bytes()?;
let mhash = MultihashRef::from_slice(bytes)
.or(Err(decode::Error::Message("not a multihash")))?;
Self::try_from(mhash).or(Err(decode::Error::Message("not a git oid")))
}
}
}
impl Deref for Oid {
type Target = git2::Oid;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<git2::Oid> for Oid {
fn as_ref(&self) -> &git2::Oid {
self
}
}
impl AsRef<[u8]> for Oid {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
#[cfg(feature = "link-git")]
impl AsRef<git_hash::oid> for Oid {
fn as_ref(&self) -> &git_hash::oid {
// SAFETY: checks the length of the slice, which we know is correct
git_hash::oid::try_from(self.as_bytes()).unwrap()
}
}
impl From<git2::Oid> for Oid {
fn from(oid: git2::Oid) -> Self {
Self(oid)
}
}
impl From<Oid> for git2::Oid {
fn from(oid: Oid) -> Self {
oid.0
}
}
#[cfg(feature = "link-git")]
impl From<git_hash::ObjectId> for Oid {
fn from(git_hash::ObjectId::Sha1(bs): git_hash::ObjectId) -> Self {
// SAFETY: checks the length of the slice, which we statically know
Self(git2::Oid::from_bytes(&bs).unwrap())
}
}
#[cfg(feature = "link-git")]
impl From<Oid> for git_hash::ObjectId {
fn from(oid: Oid) -> Self {
Self::from_20_bytes(oid.as_ref())
}
}
#[cfg(feature = "link-git")]
impl<'a> From<&'a Oid> for &'a git_hash::oid {
fn from(oid: &'a Oid) -> Self {
oid.as_ref()
}
}
impl Display for Oid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl TryFrom<&str> for Oid {
type Error = git2::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse().map(Self)
}
}
impl FromStr for Oid {
type Err = git2::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FromMultihashError {
#[error("invalid hash algorithm: expected Sha1, got {actual:?}")]
AlgorithmMismatch { actual: multihash::Code },
#[error(transparent)]
Git(#[from] git2::Error),
}
impl TryFrom<Multihash> for Oid {
type Error = FromMultihashError;
fn try_from(mhash: Multihash) -> Result<Self, Self::Error> {
Self::try_from(mhash.as_ref())
}
}
impl TryFrom<MultihashRef<'_>> for Oid {
type Error = FromMultihashError;
fn try_from(mhash: MultihashRef) -> Result<Self, Self::Error> {
if mhash.algorithm() != multihash::Code::Sha1 {
return Err(Self::Error::AlgorithmMismatch {
actual: mhash.algorithm(),
});
}
Self::try_from(mhash.digest()).map_err(Self::Error::from)
}
}
impl TryFrom<&[u8]> for Oid {
type Error = git2::Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
git2::Oid::from_bytes(bytes).map(Self)
}
}
impl From<Oid> for Multihash {
fn from(oid: Oid) -> Self {
Self::from(&oid)
}
}
impl From<&Oid> for Multihash {
fn from(oid: &Oid) -> Self {
multihash::wrap(multihash::Code::Sha1, oid.as_ref())
}
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::convert::TryFrom as _;
mod iter;
pub use iter::{ReferenceNames, References};
pub mod name;
pub use name::{OneLevel, Qualified, RefLike, RefspecPattern};
pub mod check {
pub use git_ref_format::{check_ref_format as ref_format, Error, Options};
}
pub fn peeled(head: git2::Reference) -> Option<(String, git2::Oid)> {
head.name()
.and_then(|name| head.target().map(|target| (name.to_owned(), target)))
}
pub fn refined((name, oid): (&str, git2::Oid)) -> Result<(OneLevel, crate::Oid), name::Error> {
let name = RefLike::try_from(name)?;
Ok((OneLevel::from(name), oid.into()))
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
/// Iterator chaining multiple [`git2::References`]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct References<'a> {
inner: Vec<git2::References<'a>>,
}
impl<'a> References<'a> {
pub fn new(refs: impl IntoIterator<Item = git2::References<'a>>) -> Self {
Self {
inner: refs.into_iter().collect(),
}
}
pub fn from_globs(
repo: &'a git2::Repository,
globs: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<Self, git2::Error> {
let globs = globs.into_iter();
let mut iters = globs
.size_hint()
.1
.map(Vec::with_capacity)
.unwrap_or_else(Vec::new);
for glob in globs {
let iter = repo.references_glob(glob.as_ref())?;
iters.push(iter);
}
Ok(Self::new(iters))
}
pub fn names<'b>(&'b mut self) -> ReferenceNames<'a, 'b> {
ReferenceNames {
inner: self.inner.iter_mut().map(|refs| refs.names()).collect(),
}
}
pub fn peeled(self) -> impl Iterator<Item = (String, git2::Oid)> + 'a {
self.filter_map(|reference| {
reference.ok().and_then(|head| {
head.name().and_then(|name| {
head.target()
.map(|target| (name.to_owned(), target.to_owned()))
})
})
})
}
}
impl<'a> Iterator for References<'a> {
type Item = Result<git2::Reference<'a>, git2::Error>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.pop().and_then(|mut iter| match iter.next() {
None => self.next(),
Some(item) => {
self.inner.push(iter);
Some(item)
},
})
}
}
/// Iterator chaining multiple [`git2::ReferenceNames`]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct ReferenceNames<'repo, 'references> {
inner: Vec<git2::ReferenceNames<'repo, 'references>>,
}
impl<'a, 'b> Iterator for ReferenceNames<'a, 'b> {
type Item = Result<&'b str, git2::Error>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.pop().and_then(|mut iter| match iter.next() {
None => self.next(),
Some(item) => {
self.inner.push(iter);
Some(item)
},
})
}
}
+711
View File
@@ -0,0 +1,711 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{
convert::TryFrom,
fmt::{self, Display},
iter::FromIterator,
ops::Deref,
path::Path,
str::{self, FromStr},
};
pub use percent_encoding::PercentEncode;
use thiserror::Error;
use super::check;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid utf8")]
Utf8,
#[error("not a valid git ref name or pattern")]
RefFormat(#[from] check::Error),
}
impl Error {
pub const fn empty() -> Self {
Self::RefFormat(check::Error::Empty)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StripPrefixError {
#[error("prefix is equal to path")]
ImproperPrefix,
#[error("not prefixed by given path")]
NotPrefix,
}
/// An owned path-like value which is a valid git refname.
///
/// See [`git-check-ref-format`] for what the rules for refnames are --
/// conversion functions behave as if `--allow-onelevel` was given.
/// Additionally, we impose the rule that the name must consist of valid utf8.
///
/// Note that refspec patterns (eg. "refs/heads/*") are not allowed (see
/// [`RefspecPattern`]), and that the maximum length of the name is 1024 bytes.
///
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(into = "String", try_from = "String")
)]
pub struct RefLike(String);
impl RefLike {
/// Append the path in `Other` to `self.
pub fn join<Other: Into<Self>>(&self, other: Other) -> Self {
Self(format!("{}/{}", self.0, other.into().0))
}
/// Append a [`RefspecPattern`], yielding a [`RefspecPattern`]
pub fn with_pattern_suffix<Suf: Into<RefspecPattern>>(&self, suf: Suf) -> RefspecPattern {
RefspecPattern(format!("{}/{}", self.0, suf.into().0))
}
/// Returns a [`RefLike`] that, when joined onto `base`, yields `self`.
///
/// # Errors
///
/// If `base` is not a prefix of `self`, or `base` equals the path in `self`
/// (ie. the result would be the empty path, which is not a valid
/// [`RefLike`]).
pub fn strip_prefix<P: AsRef<str>>(&self, base: P) -> Result<Self, StripPrefixError> {
let base = base.as_ref();
let base = format!("{}/", base.strip_suffix('/').unwrap_or(base));
self.0
.strip_prefix(&base)
.ok_or(StripPrefixError::NotPrefix)
.and_then(|path| {
if path.is_empty() {
Err(StripPrefixError::ImproperPrefix)
} else {
Ok(Self(path.into()))
}
})
}
pub fn as_str(&self) -> &str {
self.as_ref()
}
pub fn percent_encode(&self) -> PercentEncode {
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
&percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
/// https://url.spec.whatwg.org/#path-percent-encode-set
const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
.add(b'#')
.add(b'?')
.add(b'{')
.add(b'}');
percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
}
}
impl Deref for RefLike {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for RefLike {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for RefLike {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
check::ref_format(
check::Options {
allow_onelevel: true,
allow_pattern: false,
},
s,
)?;
Ok(Self(s.to_owned()))
}
}
impl TryFrom<&[u8]> for RefLike {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
str::from_utf8(bytes)
.or(Err(Error::Utf8))
.and_then(Self::try_from)
}
}
impl FromStr for RefLike {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<String> for RefLike {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::try_from(s.as_str())
}
}
impl TryFrom<&Path> for RefLike {
type Error = Error;
#[cfg(target_family = "windows")]
fn try_from(p: &Path) -> Result<Self, Self::Error> {
use std::{convert::TryInto as _, path::Component::Normal};
p.components()
.filter_map(|comp| match comp {
Normal(s) => Some(s),
_ => None,
})
.map(|os| os.to_str().ok_or(Error::Utf8))
.collect::<Result<Vec<_>, Self::Error>>()?
.join("/")
.try_into()
}
#[cfg(target_family = "unix")]
fn try_from(p: &Path) -> Result<Self, Self::Error> {
Self::try_from(p.to_str().ok_or(Error::Utf8)?)
}
}
impl From<&RefLike> for RefLike {
fn from(me: &RefLike) -> Self {
me.clone()
}
}
impl From<git_ref_format::RefString> for RefLike {
#[inline]
fn from(r: git_ref_format::RefString) -> Self {
Self(r.into())
}
}
impl From<&git_ref_format::RefString> for RefLike {
#[inline]
fn from(r: &git_ref_format::RefString) -> Self {
Self::from(r.as_refstr())
}
}
impl From<&git_ref_format::RefStr> for RefLike {
#[inline]
fn from(r: &git_ref_format::RefStr) -> Self {
Self(r.to_owned().into())
}
}
impl From<RefLike> for String {
fn from(RefLike(path): RefLike) -> Self {
path
}
}
impl FromIterator<Self> for RefLike {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = Self>,
{
Self(iter.into_iter().map(|x| x.0).collect::<Vec<_>>().join("/"))
}
}
impl Display for RefLike {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// A [`RefLike`] without a "refs/" prefix.
///
/// Conversion functions strip the first **two** path components iff the path
/// starts with `refs/`.
///
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
/// [`Qualified`] form is accepted, and silently converted.
///
/// # Examples
///
/// ```rust
/// use std::convert::TryFrom;
/// use radicle_git_ext::reference::name::*;
///
/// assert_eq!(
/// &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
/// "next"
/// );
///
/// assert_eq!(
/// &*OneLevel::from(RefLike::try_from("refs/remotes/origin/it").unwrap()),
/// "origin/it"
/// );
///
/// assert_eq!(
/// &*OneLevel::from(RefLike::try_from("mistress").unwrap()),
/// "mistress"
/// );
///
/// assert_eq!(
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/tags/grace").unwrap())),
/// (
/// OneLevel::from(RefLike::try_from("grace").unwrap()),
/// Some(RefLike::try_from("tags").unwrap())
/// ),
/// );
///
/// assert_eq!(
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/remotes/origin/hopper").unwrap())),
/// (
/// OneLevel::from(RefLike::try_from("origin/hopper").unwrap()),
/// Some(RefLike::try_from("remotes").unwrap())
/// ),
/// );
///
/// assert_eq!(
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/HEAD").unwrap())),
/// (OneLevel::from(RefLike::try_from("HEAD").unwrap()), None)
/// );
///
/// assert_eq!(
/// &*OneLevel::from(RefLike::try_from("origin/hopper").unwrap()).into_qualified(
/// RefLike::try_from("remotes").unwrap()
/// ),
/// "refs/remotes/origin/hopper",
/// );
/// ```
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(into = "String", try_from = "RefLike")
)]
pub struct OneLevel(String);
impl OneLevel {
pub fn as_str(&self) -> &str {
self.as_ref()
}
pub fn from_qualified(Qualified(path): Qualified) -> (Self, Option<RefLike>) {
let mut path = path.strip_prefix("refs/").unwrap_or(&path).split('/');
match path.next() {
Some(category) => {
let category = RefLike(category.into());
// check that the "category" is not the only component of the path
match path.next() {
Some(head) => (
Self(
std::iter::once(head)
.chain(path)
.collect::<Vec<_>>()
.join("/"),
),
Some(category),
),
None => (Self::from(category), None),
}
},
None => unreachable!(),
}
}
pub fn into_qualified(self, category: RefLike) -> Qualified {
Qualified(format!("refs/{}/{}", category, self))
}
}
impl Deref for OneLevel {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for OneLevel {
fn as_ref(&self) -> &str {
self
}
}
impl From<RefLike> for OneLevel {
fn from(RefLike(path): RefLike) -> Self {
if path.starts_with("refs/") {
Self(path.split('/').skip(2).collect::<Vec<_>>().join("/"))
} else {
Self(path)
}
}
}
impl From<Qualified> for OneLevel {
fn from(Qualified(path): Qualified) -> Self {
Self::from(RefLike(path))
}
}
impl From<OneLevel> for RefLike {
fn from(OneLevel(path): OneLevel) -> Self {
Self(path)
}
}
impl From<OneLevel> for String {
fn from(OneLevel(path): OneLevel) -> Self {
path
}
}
impl Display for OneLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// A [`RefLike`] **with** a "refs/" prefix.
///
/// Conversion functions will assume `refs/heads/` if the input was not
/// qualified.
///
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
/// [`OneLevel`] form is accepted, and silently converted.
///
/// # Examples
///
/// ```rust
/// use std::convert::TryFrom;
/// use radicle_git_ext::reference::name::*;
///
/// assert_eq!(
/// &*Qualified::from(RefLike::try_from("laplace").unwrap()),
/// "refs/heads/laplace"
/// );
///
/// assert_eq!(
/// &*Qualified::from(RefLike::try_from("refs/heads/pu").unwrap()),
/// "refs/heads/pu"
/// );
///
/// assert_eq!(
/// &*Qualified::from(RefLike::try_from("refs/tags/v6.6.6").unwrap()),
/// "refs/tags/v6.6.6"
/// );
/// ```
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(into = "String", try_from = "RefLike")
)]
pub struct Qualified(String);
impl Qualified {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for Qualified {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for Qualified {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<RefLike> for Qualified {
fn from(RefLike(path): RefLike) -> Self {
if path.starts_with("refs/") {
Self(path)
} else {
Self(format!("refs/heads/{}", path))
}
}
}
impl From<OneLevel> for Qualified {
fn from(OneLevel(path): OneLevel) -> Self {
Self::from(RefLike(path))
}
}
impl From<Qualified> for RefLike {
fn from(Qualified(path): Qualified) -> Self {
Self(path)
}
}
impl From<Qualified> for String {
fn from(Qualified(path): Qualified) -> Self {
path
}
}
impl Display for Qualified {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self)
}
}
/// An owned, path-like value which is a valid refspec pattern.
///
/// Conversion functions behave as if `--allow-onelevel --refspec-pattern` where
/// given to [`git-check-ref-format`]. That is, most of the rules of [`RefLike`]
/// apply, but the path _may_ contain exactly one `*` character.
///
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(into = "String", try_from = "String")
)]
pub struct RefspecPattern(String);
impl RefspecPattern {
/// Append the `RefLike` to the `RefspecPattern`. This allows the creation
/// of patterns where the `*` appears in the middle of the path, e.g.
/// `refs/remotes/*/mfdoom`
pub fn append(&self, refl: impl Into<RefLike>) -> Self {
RefspecPattern(format!("{}/{}", self.0, refl.into()))
}
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
impl From<&RefspecPattern> for RefspecPattern {
fn from(pat: &RefspecPattern) -> Self {
pat.clone()
}
}
impl Deref for RefspecPattern {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for RefspecPattern {
fn as_ref(&self) -> &str {
self
}
}
impl TryFrom<&str> for RefspecPattern {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
check::ref_format(
check::Options {
allow_onelevel: true,
allow_pattern: true,
},
s,
)?;
Ok(Self(s.to_owned()))
}
}
impl TryFrom<&[u8]> for RefspecPattern {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
str::from_utf8(bytes)
.or(Err(Error::Utf8))
.and_then(Self::try_from)
}
}
impl FromStr for RefspecPattern {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<String> for RefspecPattern {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::try_from(s.as_str())
}
}
impl From<RefspecPattern> for String {
fn from(RefspecPattern(path): RefspecPattern) -> Self {
path
}
}
impl Display for RefspecPattern {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
// `RefLike`-likes can be coerced into `RefspecPattern`s
impl From<RefLike> for RefspecPattern {
fn from(RefLike(path): RefLike) -> Self {
Self(path)
}
}
impl From<&RefLike> for RefspecPattern {
fn from(RefLike(path): &RefLike) -> Self {
Self(path.to_owned())
}
}
impl From<OneLevel> for RefspecPattern {
fn from(OneLevel(path): OneLevel) -> Self {
Self(path)
}
}
impl From<&OneLevel> for RefspecPattern {
fn from(OneLevel(path): &OneLevel) -> Self {
Self(path.to_owned())
}
}
impl From<Qualified> for RefspecPattern {
fn from(Qualified(path): Qualified) -> Self {
Self(path)
}
}
impl From<&Qualified> for RefspecPattern {
fn from(Qualified(path): &Qualified) -> Self {
Self(path.to_owned())
}
}
impl From<git_ref_format::refspec::PatternString> for RefspecPattern {
#[inline]
fn from(r: git_ref_format::refspec::PatternString) -> Self {
Self(r.into())
}
}
impl From<&git_ref_format::refspec::PatternStr> for RefspecPattern {
#[inline]
fn from(r: &git_ref_format::refspec::PatternStr) -> Self {
Self(r.to_owned().into())
}
}
#[cfg(feature = "minicbor")]
mod minicbor_impls {
use super::*;
use minicbor::{
decode,
encode::{self, Write},
Decode,
Decoder,
Encode,
Encoder,
};
impl Encode for RefLike {
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
e.str(self.as_str())?;
Ok(())
}
}
impl<'b> Decode<'b> for RefLike {
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
let path = d.str()?;
Self::try_from(path).or(Err(decode::Error::Message("invalid reflike")))
}
}
impl minicbor::Encode for OneLevel {
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
e.str(self.as_str())?;
Ok(())
}
}
impl<'b> Decode<'b> for OneLevel {
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
let refl: RefLike = Decode::decode(d)?;
Ok(Self::from(refl))
}
}
impl Encode for Qualified {
fn encode<W: encode::Write>(
&self,
e: &mut Encoder<W>,
) -> Result<(), encode::Error<W::Error>> {
e.str(self.as_str())?;
Ok(())
}
}
impl<'b> Decode<'b> for Qualified {
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
let refl: RefLike = Decode::decode(d)?;
Ok(Self::from(refl))
}
}
impl Encode for RefspecPattern {
fn encode<W: encode::Write>(
&self,
e: &mut Encoder<W>,
) -> Result<(), encode::Error<W::Error>> {
e.str(self.as_str())?;
Ok(())
}
}
impl<'b> Decode<'b> for RefspecPattern {
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
Self::try_from(d.str()?).or(Err(decode::Error::Message("invalid refspec pattern")))
}
}
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
pub enum Start {
Oid(git2::Oid),
Ref(String),
}
pub struct FirstParent<'a> {
inner: git2::Revwalk<'a>,
}
impl<'a> FirstParent<'a> {
pub fn new(repo: &'a git2::Repository, start: Start) -> Result<Self, git2::Error> {
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
revwalk.simplify_first_parent()?;
match start {
Start::Oid(oid) => revwalk.push(oid),
Start::Ref(name) => revwalk.push_ref(&name),
}?;
Ok(Self { inner: revwalk })
}
pub fn reverse(mut self) -> Result<Self, git2::Error> {
let mut sort = git2::Sort::TOPOLOGICAL;
sort.insert(git2::Sort::REVERSE);
self.inner.set_sorting(sort)?;
Ok(self)
}
}
impl<'a> IntoIterator for FirstParent<'a> {
type Item = Result<git2::Oid, git2::Error>;
type IntoIter = git2::Revwalk<'a>;
fn into_iter(self) -> Self::IntoIter {
self.inner
}
}
+7
View File
@@ -0,0 +1,7 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
pub const UPLOAD_PACK_HEADER: &[u8] = b"001e# service=git-upload-pack\n0000";
pub const RECEIVE_PACK_HEADER: &[u8] = b"001f# service=git-receive-pack\n0000";
+147
View File
@@ -0,0 +1,147 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{borrow::Cow, collections::BTreeMap, iter::FromIterator};
/// A simplified representation of a git tree, intended mainly to be created
/// from literals.
///
/// # Example
///
/// ```
/// use radicle_git_ext::tree::{Tree, blob};
///
/// let my_tree = vec![
/// ("README", blob(b"awe")),
/// ("src", vec![("main.rs", blob(b"fn main() {}"))].into_iter().collect()),
/// ]
/// .into_iter()
/// .collect::<Tree>();
///
/// assert_eq!(
/// format!("{:?}", my_tree),
/// "Tree(\
/// {\
/// \"README\": Blob(\
/// [\
/// 97, \
/// 119, \
/// 101\
/// ]\
/// ), \
/// \"src\": Tree(\
/// Tree(\
/// {\
/// \"main.rs\": Blob(\
/// [\
/// 102, \
/// 110, \
/// 32, \
/// 109, \
/// 97, \
/// 105, \
/// 110, \
/// 40, \
/// 41, \
/// 32, \
/// 123, \
/// 125\
/// ]\
/// )\
/// }\
/// )\
/// )\
/// }\
/// )"
/// )
/// ```
#[derive(Clone, Debug)]
pub struct Tree<'a>(BTreeMap<Cow<'a, str>, Node<'a>>);
impl Tree<'_> {
pub fn write(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error> {
use Node::*;
let mut builder = repo.treebuilder(None)?;
for (name, node) in &self.0 {
match node {
Blob(data) => {
let oid = repo.blob(data)?;
builder.insert(name.as_ref(), oid, git2::FileMode::Blob.into())?;
},
Tree(sub) => {
let oid = sub.write(repo)?;
builder.insert(name.as_ref(), oid, git2::FileMode::Tree.into())?;
},
}
}
builder.write()
}
}
impl<'a> From<BTreeMap<Cow<'a, str>, Node<'a>>> for Tree<'a> {
fn from(map: BTreeMap<Cow<'a, str>, Node<'a>>) -> Self {
Self(map)
}
}
impl<'a, K, N> FromIterator<(K, N)> for Tree<'a>
where
K: Into<Cow<'a, str>>,
N: Into<Node<'a>>,
{
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (K, N)>,
{
Self(
iter.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
#[derive(Clone, Debug)]
pub enum Node<'a> {
Blob(Cow<'a, [u8]>),
Tree(Tree<'a>),
}
pub fn blob(slice: &[u8]) -> Node {
Node::from(slice)
}
impl<'a> From<&'a [u8]> for Node<'a> {
fn from(slice: &'a [u8]) -> Self {
Self::from(Cow::Borrowed(slice))
}
}
impl<'a> From<Cow<'a, [u8]>> for Node<'a> {
fn from(bytes: Cow<'a, [u8]>) -> Self {
Self::Blob(bytes)
}
}
impl<'a> From<Tree<'a>> for Node<'a> {
fn from(tree: Tree<'a>) -> Self {
Self::Tree(tree)
}
}
impl<'a, K, N> FromIterator<(K, N)> for Node<'a>
where
K: Into<Cow<'a, str>>,
N: Into<Node<'a>>,
{
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (K, N)>,
{
Self::Tree(iter.into_iter().collect())
}
}