mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-30 08:20:48 +08:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user