mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-25 22:30:35 +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,25 @@
|
||||
[package]
|
||||
name = "git-ref-format"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
bstr = ["git-ref-format-core/bstr"]
|
||||
link-literals = ["git-ref-format-core/link-literals"]
|
||||
macro = ["git-ref-format-macro"]
|
||||
minicbor = ["git-ref-format-core/minicbor"]
|
||||
percent-encoding = ["git-ref-format-core/percent-encoding"]
|
||||
serde = ["git-ref-format-core/serde"]
|
||||
|
||||
[dependencies.git-ref-format-core]
|
||||
path = "./core"
|
||||
|
||||
[dependencies.git-ref-format-macro]
|
||||
path = "./macro"
|
||||
optional = true
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "git-ref-format-core"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
link-literals = []
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
|
||||
[dependencies.bstr]
|
||||
version = "0.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.minicbor]
|
||||
version = "0.13"
|
||||
features = ["std"]
|
||||
optional = true
|
||||
|
||||
[dependencies.percent-encoding]
|
||||
version = "2.1.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// 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;
|
||||
|
||||
use minicbor::{
|
||||
decode,
|
||||
encode::{self, Write},
|
||||
Decode,
|
||||
Decoder,
|
||||
Encode,
|
||||
Encoder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
refspec::{PatternStr, PatternString},
|
||||
Namespaced,
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for &'a RefStr {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
d.str()
|
||||
.and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for &'a RefStr {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for RefString {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).map(|s: &RefStr| s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RefString {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_refstr().encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for &'a PatternStr {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
d.str()
|
||||
.and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for &'a PatternStr {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for PatternString {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).map(|s: &PatternStr| s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for PatternString {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_pattern_str().encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for Qualified<'a> {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).and_then(|s: &RefStr| {
|
||||
s.qualified()
|
||||
.ok_or(decode::Error::Message("not a qualified ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for Qualified<'a> {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_str().encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for Namespaced<'a> {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).and_then(|s: &RefStr| {
|
||||
s.namespaced()
|
||||
.ok_or(decode::Error::Message("not a namespaced ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for Namespaced<'a> {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_str().encode(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct Options {
|
||||
/// If `false`, the refname must contain at least one `/`.
|
||||
pub allow_onelevel: bool,
|
||||
/// If `true`, the refname may contain exactly one `*` character.
|
||||
pub allow_pattern: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("empty input")]
|
||||
Empty,
|
||||
#[error("lone '@' character")]
|
||||
LoneAt,
|
||||
#[error("consecutive or trailing slash")]
|
||||
Slash,
|
||||
#[error("ends with '.lock'")]
|
||||
DotLock,
|
||||
#[error("consecutive dots ('..')")]
|
||||
DotDot,
|
||||
#[error("at-open-brace ('@{{')")]
|
||||
AtOpenBrace,
|
||||
#[error("invalid character {0:?}")]
|
||||
InvalidChar(char),
|
||||
#[error("component starts with '.'")]
|
||||
StartsDot,
|
||||
#[error("component ends with '.'")]
|
||||
EndsDot,
|
||||
#[error("control character")]
|
||||
Control,
|
||||
#[error("whitespace")]
|
||||
Space,
|
||||
#[error("must contain at most one '*'")]
|
||||
Pattern,
|
||||
#[error("must contain at least one '/'")]
|
||||
OneLevel,
|
||||
}
|
||||
|
||||
/// Validate that a string slice is a valid refname.
|
||||
pub fn ref_format(opts: Options, s: &str) -> Result<(), Error> {
|
||||
match s {
|
||||
"" => Err(Error::Empty),
|
||||
"@" => Err(Error::LoneAt),
|
||||
"." => Err(Error::StartsDot),
|
||||
_ => {
|
||||
let mut globs = 0usize;
|
||||
let mut parts = 0usize;
|
||||
|
||||
for x in s.split('/') {
|
||||
if x.is_empty() {
|
||||
return Err(Error::Slash);
|
||||
}
|
||||
|
||||
parts += 1;
|
||||
|
||||
if x.ends_with(".lock") {
|
||||
return Err(Error::DotLock);
|
||||
}
|
||||
|
||||
let last_char = x.chars().count() - 1;
|
||||
for (i, y) in x.chars().zip(x.chars().cycle().skip(1)).enumerate() {
|
||||
match y {
|
||||
('.', '.') => return Err(Error::DotDot),
|
||||
('@', '{') => return Err(Error::AtOpenBrace),
|
||||
|
||||
('\0', _) => return Err(Error::InvalidChar('\0')),
|
||||
('\\', _) => return Err(Error::InvalidChar('\\')),
|
||||
('~', _) => return Err(Error::InvalidChar('~')),
|
||||
('^', _) => return Err(Error::InvalidChar('^')),
|
||||
(':', _) => return Err(Error::InvalidChar(':')),
|
||||
('?', _) => return Err(Error::InvalidChar('?')),
|
||||
('[', _) => return Err(Error::InvalidChar('[')),
|
||||
|
||||
('*', _) => globs += 1,
|
||||
|
||||
('.', _) if i == 0 => return Err(Error::StartsDot),
|
||||
('.', _) if i == last_char => return Err(Error::EndsDot),
|
||||
|
||||
(' ', _) => return Err(Error::Space),
|
||||
|
||||
(z, _) if z.is_ascii_control() => return Err(Error::Control),
|
||||
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parts < 2 && !opts.allow_onelevel {
|
||||
Err(Error::OneLevel)
|
||||
} else if globs > 1 && opts.allow_pattern {
|
||||
Err(Error::Pattern)
|
||||
} else if globs > 0 && !opts.allow_pattern {
|
||||
Err(Error::InvalidChar('*'))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// 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,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{lit, name, Component, RefStr, RefString};
|
||||
|
||||
/// A fully-qualified refname.
|
||||
///
|
||||
/// A refname is qualified _iff_ it starts with "refs/" and has at least three
|
||||
/// components. This implies that a [`Qualified`] ref has a category, such as
|
||||
/// "refs/heads/main".
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Qualified<'a>(pub(crate) Cow<'a, RefStr>);
|
||||
|
||||
impl<'a> Qualified<'a> {
|
||||
/// Infallibly create a [`Qualified`] from components.
|
||||
///
|
||||
/// Note that the "refs/" prefix is implicitly added, so `a` is the second
|
||||
/// [`Component`]. Mirroring [`Self::non_empty_components`], providing
|
||||
/// two [`Component`]s guarantees well-formedness of the [`Qualified`].
|
||||
/// `tail` may be empty.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use git_ref_format::{component, Qualified};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "refs/heads/main",
|
||||
/// Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
|
||||
/// )
|
||||
/// ```
|
||||
pub fn from_components<'b, 'c, 'd, A, B, C>(a: A, b: B, tail: C) -> Self
|
||||
where
|
||||
A: Into<Component<'b>>,
|
||||
B: Into<Component<'c>>,
|
||||
C: IntoIterator<Item = Component<'d>>,
|
||||
{
|
||||
let mut inner = name::REFS.join(a.into()).and(b.into());
|
||||
inner.extend(tail);
|
||||
|
||||
Self(inner.into())
|
||||
}
|
||||
|
||||
pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
|
||||
Self::_from_refstr(r.into())
|
||||
}
|
||||
|
||||
fn _from_refstr(r: Cow<'a, RefStr>) -> Option<Self> {
|
||||
let mut iter = r.iter();
|
||||
match (iter.next()?, iter.next()?, iter.next()?) {
|
||||
("refs", _, _) => Some(Qualified(r)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn join<'b, R>(&self, other: R) -> Qualified<'b>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
Qualified(self.0.join(other).into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn namespaced(&self) -> Option<Namespaced> {
|
||||
self.0.as_ref().into()
|
||||
}
|
||||
|
||||
/// Add a namespace.
|
||||
///
|
||||
/// Creates a new [`Namespaced`] by prefxing `self` with
|
||||
/// "refs/namespaces/<ns>".
|
||||
pub fn add_namespace<'b>(&self, ns: Component<'b>) -> Namespaced<'a> {
|
||||
Namespaced(Cow::Owned(
|
||||
IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
|
||||
.chain(self.0.components())
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Like [`Self::non_empty_components`], but with string slices.
|
||||
pub fn non_empty_iter(&self) -> (&str, &str, &str, name::Iter) {
|
||||
let mut iter = self.iter();
|
||||
(
|
||||
iter.next().unwrap(),
|
||||
iter.next().unwrap(),
|
||||
iter.next().unwrap(),
|
||||
iter,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the first three [`Component`]s, and a possibly empty iterator
|
||||
/// over the remaining ones.
|
||||
///
|
||||
/// A qualified ref is guaranteed to have at least three components, which
|
||||
/// this method provides a witness of. This is useful eg. for pattern
|
||||
/// matching on the prefix.
|
||||
pub fn non_empty_components(&self) -> (Component, Component, Component, name::Components) {
|
||||
let mut cs = self.components();
|
||||
(
|
||||
cs.next().unwrap(),
|
||||
cs.next().unwrap(),
|
||||
cs.next().unwrap(),
|
||||
cs,
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_owned<'b>(&self) -> Qualified<'b> {
|
||||
Qualified(Cow::Owned(self.0.clone().into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_owned<'b>(self) -> Qualified<'b> {
|
||||
Qualified(Cow::Owned(self.0.into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_refstring(self) -> RefString {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Qualified<'_> {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Qualified<'a>> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(q: Qualified<'a>) -> Self {
|
||||
q.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified<'_>> for RefString {
|
||||
#[inline]
|
||||
fn from(q: Qualified) -> Self {
|
||||
q.0.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> From<(lit::Refs, T, U)> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
U: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, cat, name): (lit::Refs, T, U)) -> Self {
|
||||
let refs: &RefStr = refs.into();
|
||||
Self(Cow::Owned(refs.join(cat).and(name)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsHeads<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, heads, name): lit::RefsHeads<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), heads.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsTags<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, tags, name): lit::RefsTags<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), tags.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsNotes<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, notes, name): lit::RefsNotes<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), notes.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsRemotes<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, remotes, name): lit::RefsRemotes<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), remotes.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Qualified<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::*;
|
||||
|
||||
impl From<lit::RefsRadId> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, id): lit::RefsRadId) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), id.into()]).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lit::RefsRadSelf> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, selv): lit::RefsRadSelf) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), selv.into()]).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lit::RefsRadSignedRefs> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, sig): lit::RefsRadSignedRefs) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), sig.into()]).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Component<'a>>> From<lit::RefsRadIds<T>> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, ids, id): lit::RefsRadIds<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), ids.into(), id.into()])
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Component<'a>>, I: Into<Component<'a>>> From<lit::RefsCobs<T, I>>
|
||||
for Qualified<'_>
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, cobs, ty, id): lit::RefsCobs<T, I>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), cobs.into(), ty.into(), id.into()])
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Qualified`] ref under a git namespace.
|
||||
///
|
||||
/// A ref is namespaced if it starts with "refs/namespaces/", another path
|
||||
/// component, and "refs/". Eg.
|
||||
///
|
||||
/// refs/namespaces/xyz/refs/heads/main
|
||||
///
|
||||
/// Note that namespaces can be nested, so the result of
|
||||
/// [`Namespaced::strip_namespace`] may be convertible to a [`Namespaced`]
|
||||
/// again. For example:
|
||||
///
|
||||
/// ```no_run
|
||||
/// let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
|
||||
/// let namespaced = full.namespaced().unwrap();
|
||||
/// let strip_first = namespaced.strip_namespace();
|
||||
/// let nested = strip_first.namespaced().unwrap();
|
||||
/// let strip_second = nested.strip_namespace();
|
||||
///
|
||||
/// assert_eq!("a", namespaced.namespace().as_str());
|
||||
/// assert_eq!("b", nested.namespace().as_str());
|
||||
/// assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
|
||||
/// assert_eq!("refs/heads/main", strip_second.as_str());
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Namespaced<'a>(Cow<'a, RefStr>);
|
||||
|
||||
impl<'a> Namespaced<'a> {
|
||||
pub fn namespace(&self) -> Component {
|
||||
self.components().nth(2).unwrap()
|
||||
}
|
||||
|
||||
pub fn strip_namespace<'b>(&self) -> Qualified<'b> {
|
||||
const REFS_NAMESPACES: &RefStr = RefStr::from_str("refs/namespaces");
|
||||
|
||||
Qualified(Cow::Owned(
|
||||
self.strip_prefix(REFS_NAMESPACES)
|
||||
.unwrap()
|
||||
.components()
|
||||
.skip(1)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn strip_namespace_recursive<'b>(&self) -> Qualified<'b> {
|
||||
let mut strip = self.strip_namespace();
|
||||
while let Some(ns) = strip.namespaced() {
|
||||
strip = ns.strip_namespace();
|
||||
}
|
||||
strip
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_owned<'b>(&self) -> Namespaced<'b> {
|
||||
Namespaced(Cow::Owned(self.0.clone().into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_owned<'b>(self) -> Namespaced<'b> {
|
||||
Namespaced(Cow::Owned(self.0.into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_qualified(self) -> Qualified<'a> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Namespaced<'_> {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Namespaced<'a>> for Qualified<'a> {
|
||||
#[inline]
|
||||
fn from(ns: Namespaced<'a>) -> Self {
|
||||
Self(ns.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Option<Namespaced<'a>> {
|
||||
fn from(rs: &'a RefStr) -> Self {
|
||||
let mut cs = rs.iter();
|
||||
match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
|
||||
("refs", "namespaces", _, "refs") => Some(Namespaced(Cow::from(rs))),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<lit::RefsNamespaces<'_, T>> for Namespaced<'static>
|
||||
where
|
||||
T: Into<Component<'a>>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, namespaces, namespace, name): lit::RefsNamespaces<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([refs.into(), namespaces.into(), namespace.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
mod check;
|
||||
pub use check::{ref_format as check_ref_format, Error, Options};
|
||||
|
||||
mod deriv;
|
||||
pub use deriv::{Namespaced, Qualified};
|
||||
|
||||
pub mod lit;
|
||||
|
||||
pub mod name;
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub use name::PercentEncode;
|
||||
pub use name::{Component, RefStr, RefString};
|
||||
|
||||
pub mod refspec;
|
||||
pub use refspec::DuplicateGlob;
|
||||
|
||||
#[cfg(feature = "minicbor")]
|
||||
mod cbor;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use crate::{name, Qualified, RefStr};
|
||||
|
||||
/// A literal [`RefStr`].
|
||||
///
|
||||
/// Types implementing [`Lit`] must be [`name::Component`]s, and provide a
|
||||
/// conversion from a component _iff_ the component's [`RefStr`] representation
|
||||
/// is equal to [`Lit::NAME`]. Because these morphisms can only be guaranteed
|
||||
/// axiomatically, the trait can not currently be implemented by types outside
|
||||
/// of this crate.
|
||||
///
|
||||
/// [`Lit`] types are useful for efficiently creating known-valid [`Qualified`]
|
||||
/// refs, and sometimes for pattern matching.
|
||||
pub trait Lit: Sized + sealed::Sealed {
|
||||
const SELF: Self;
|
||||
const NAME: &'static RefStr;
|
||||
|
||||
#[inline]
|
||||
fn from_component(c: &name::Component) -> Option<Self> {
|
||||
(c.as_ref() == Self::NAME).then(|| Self::SELF)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Lit> From<T> for &'static RefStr {
|
||||
#[inline]
|
||||
fn from(_: T) -> Self {
|
||||
T::NAME
|
||||
}
|
||||
}
|
||||
|
||||
mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
/// All known literal [`RefStr`]s.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub enum KnownLit {
|
||||
Refs,
|
||||
Heads,
|
||||
Namespaces,
|
||||
Remotes,
|
||||
Tags,
|
||||
Notes,
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
Rad,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Id,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Ids,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Selv,
|
||||
#[cfg(feature = "link-literals")]
|
||||
SignedRefs,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Cobs,
|
||||
}
|
||||
|
||||
impl KnownLit {
|
||||
#[inline]
|
||||
pub fn from_component(c: &name::Component) -> Option<Self> {
|
||||
let c: &RefStr = c.as_ref();
|
||||
if c == Refs::NAME {
|
||||
Some(Self::Refs)
|
||||
} else if c == Heads::NAME {
|
||||
Some(Self::Heads)
|
||||
} else if c == Namespaces::NAME {
|
||||
Some(Self::Namespaces)
|
||||
} else if c == Remotes::NAME {
|
||||
Some(Self::Remotes)
|
||||
} else if c == Tags::NAME {
|
||||
Some(Self::Tags)
|
||||
} else if c == Notes::NAME {
|
||||
Some(Self::Notes)
|
||||
} else {
|
||||
#[cfg(feature = "link-literals")]
|
||||
{
|
||||
if c == Rad::NAME {
|
||||
Some(Self::Rad)
|
||||
} else if c == Id::NAME {
|
||||
Some(Self::Id)
|
||||
} else if c == Ids::NAME {
|
||||
Some(Self::Ids)
|
||||
} else if c == Selv::NAME {
|
||||
Some(Self::Selv)
|
||||
} else if c == SignedRefs::NAME {
|
||||
Some(Self::SignedRefs)
|
||||
} else if c == Cobs::NAME {
|
||||
Some(Self::Cobs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "link-literals"))]
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KnownLit> for name::Component<'_> {
|
||||
#[inline]
|
||||
fn from(k: KnownLit) -> Self {
|
||||
match k {
|
||||
KnownLit::Refs => Refs.into(),
|
||||
KnownLit::Heads => Heads.into(),
|
||||
KnownLit::Namespaces => Namespaces.into(),
|
||||
KnownLit::Remotes => Remotes.into(),
|
||||
KnownLit::Tags => Tags.into(),
|
||||
KnownLit::Notes => Notes.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Rad => Rad.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Id => Id.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Ids => Ids.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Selv => Selv.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::SignedRefs => SignedRefs.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Cobs => Cobs.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either a [`KnownLit`] or a [`name::Component`]
|
||||
pub enum SomeLit<'a> {
|
||||
Known(KnownLit),
|
||||
Any(name::Component<'a>),
|
||||
}
|
||||
|
||||
impl SomeLit<'_> {
|
||||
pub fn known(self) -> Option<KnownLit> {
|
||||
match self {
|
||||
Self::Known(k) => Some(k),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<name::Component<'a>> for SomeLit<'a> {
|
||||
#[inline]
|
||||
fn from(c: name::Component<'a>) -> Self {
|
||||
match KnownLit::from_component(&c) {
|
||||
Some(k) => Self::Known(k),
|
||||
None => Self::Any(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type RefsHeads<T> = (Refs, Heads, T);
|
||||
pub type RefsTags<T> = (Refs, Tags, T);
|
||||
pub type RefsNotes<T> = (Refs, Notes, T);
|
||||
pub type RefsRemotes<T> = (Refs, Remotes, T);
|
||||
pub type RefsNamespaces<'a, T> = (Refs, Namespaces, T, Qualified<'a>);
|
||||
|
||||
#[inline]
|
||||
pub fn refs_heads<T: AsRef<RefStr>>(name: T) -> RefsHeads<T> {
|
||||
(Refs, Heads, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_tags<T: AsRef<RefStr>>(name: T) -> RefsTags<T> {
|
||||
(Refs, Tags, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_notes<T: AsRef<RefStr>>(name: T) -> RefsNotes<T> {
|
||||
(Refs, Notes, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_remotes<T: AsRef<RefStr>>(name: T) -> RefsRemotes<T> {
|
||||
(Refs, Remotes, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_namespaces<'a, 'b, T>(namespace: T, name: Qualified<'b>) -> RefsNamespaces<'b, T>
|
||||
where
|
||||
T: Into<name::Component<'a>>,
|
||||
{
|
||||
(Refs, Namespaces, namespace, name)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Refs;
|
||||
|
||||
impl Lit for Refs {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::REFS;
|
||||
}
|
||||
impl sealed::Sealed for Refs {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Heads;
|
||||
|
||||
impl Lit for Heads {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::HEADS;
|
||||
}
|
||||
impl sealed::Sealed for Heads {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Namespaces;
|
||||
|
||||
impl Lit for Namespaces {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::NAMESPACES;
|
||||
}
|
||||
impl sealed::Sealed for Namespaces {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Remotes;
|
||||
|
||||
impl Lit for Remotes {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::REMOTES;
|
||||
}
|
||||
impl sealed::Sealed for Remotes {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Tags;
|
||||
|
||||
impl Lit for Tags {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::TAGS;
|
||||
}
|
||||
impl sealed::Sealed for Tags {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Notes;
|
||||
|
||||
impl Lit for Notes {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::NOTES;
|
||||
}
|
||||
impl sealed::Sealed for Notes {}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::*;
|
||||
|
||||
pub type RefsRadId = (Refs, Rad, Id);
|
||||
pub type RefsRadSelf = (Refs, Rad, Selv);
|
||||
pub type RefsRadSignedRefs = (Refs, Rad, SignedRefs);
|
||||
pub type RefsRadIds<T> = (Refs, Rad, Ids, T);
|
||||
pub type RefsCobs<T, I> = (Refs, Cobs, T, I);
|
||||
|
||||
pub const REFS_RAD_ID: (Refs, Rad, Id) = (Refs, Rad, Id);
|
||||
pub const REFS_RAD_SELF: (Refs, Rad, Selv) = (Refs, Rad, Selv);
|
||||
pub const REFS_RAD_SIGNED_REFS: (Refs, Rad, SignedRefs) = (Refs, Rad, SignedRefs);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Rad;
|
||||
|
||||
impl Lit for Rad {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("rad");
|
||||
}
|
||||
impl sealed::Sealed for Rad {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Id;
|
||||
|
||||
impl Lit for Id {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("id");
|
||||
}
|
||||
impl sealed::Sealed for Id {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Ids;
|
||||
|
||||
impl Lit for Ids {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("ids");
|
||||
}
|
||||
impl sealed::Sealed for Ids {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Selv;
|
||||
|
||||
impl Lit for Selv {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("self");
|
||||
}
|
||||
impl sealed::Sealed for Selv {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct SignedRefs;
|
||||
|
||||
impl Lit for SignedRefs {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("signed_refs");
|
||||
}
|
||||
impl sealed::Sealed for SignedRefs {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Cobs;
|
||||
|
||||
impl Lit for Cobs {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("cobs");
|
||||
}
|
||||
impl sealed::Sealed for Cobs {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
@@ -0,0 +1,522 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
//
|
||||
// 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::{Borrow, Cow},
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
iter::{Extend, FromIterator},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
check,
|
||||
refspec::{PatternStr, PatternString},
|
||||
Namespaced,
|
||||
Qualified,
|
||||
};
|
||||
|
||||
mod iter;
|
||||
pub use iter::{component, Component, Components, Iter};
|
||||
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub use percent_encoding::PercentEncode;
|
||||
|
||||
pub const HEADS: &RefStr = RefStr::from_str(str::HEADS);
|
||||
pub const MAIN: &RefStr = RefStr::from_str(str::MAIN);
|
||||
pub const MASTER: &RefStr = RefStr::from_str(str::MASTER);
|
||||
pub const NAMESPACES: &RefStr = RefStr::from_str(str::NAMESPACES);
|
||||
pub const NOTES: &RefStr = RefStr::from_str(str::NOTES);
|
||||
pub const ORIGIN: &RefStr = RefStr::from_str(str::ORIGIN);
|
||||
pub const REFS: &RefStr = RefStr::from_str(str::REFS);
|
||||
pub const REMOTES: &RefStr = RefStr::from_str(str::REMOTES);
|
||||
pub const TAGS: &RefStr = RefStr::from_str(str::TAGS);
|
||||
|
||||
pub const REFS_HEADS_MAIN: &RefStr = RefStr::from_str(str::REFS_HEADS_MAIN);
|
||||
pub const REFS_HEADS_MASTER: &RefStr = RefStr::from_str(str::REFS_HEADS_MASTER);
|
||||
|
||||
pub mod str {
|
||||
pub const HEADS: &str = "heads";
|
||||
pub const MAIN: &str = "main";
|
||||
pub const MASTER: &str = "master";
|
||||
pub const NAMESPACES: &str = "namespaces";
|
||||
pub const NOTES: &str = "notes";
|
||||
pub const ORIGIN: &str = "origin";
|
||||
pub const REFS: &str = "refs";
|
||||
pub const REMOTES: &str = "remotes";
|
||||
pub const TAGS: &str = "tags";
|
||||
|
||||
pub const REFS_HEADS_MAIN: &str = "refs/heads/main";
|
||||
pub const REFS_HEADS_MASTER: &str = "refs/heads/master";
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
pub const RAD: &str = "rad";
|
||||
pub const ID: &str = "id";
|
||||
pub const IDS: &str = "ids";
|
||||
pub const SELF: &str = "self";
|
||||
pub const SIGNED_REFS: &str = "signed_refs";
|
||||
pub const COBS: &str = "cobs";
|
||||
|
||||
pub const REFS_RAD_ID: &str = "refs/rad/id";
|
||||
pub const REFS_RAD_SELF: &str = "refs/rad/self";
|
||||
pub const REFS_RAD_SIGNED_REFS: &str = "refs/rad/signed_refs";
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
}
|
||||
|
||||
pub mod bytes {
|
||||
use super::str;
|
||||
|
||||
pub const HEADS: &[u8] = str::HEADS.as_bytes();
|
||||
pub const MAIN: &[u8] = str::MAIN.as_bytes();
|
||||
pub const MASTER: &[u8] = str::MASTER.as_bytes();
|
||||
pub const NAMESPACES: &[u8] = str::NAMESPACES.as_bytes();
|
||||
pub const NOTES: &[u8] = str::NOTES.as_bytes();
|
||||
pub const ORIGIN: &[u8] = str::ORIGIN.as_bytes();
|
||||
pub const REFS: &[u8] = str::REFS.as_bytes();
|
||||
pub const REMOTES: &[u8] = str::REMOTES.as_bytes();
|
||||
pub const TAGS: &[u8] = str::TAGS.as_bytes();
|
||||
|
||||
pub const REFS_HEADS_MAIN: &[u8] = str::REFS_HEADS_MAIN.as_bytes();
|
||||
pub const REFS_HEADS_MASTER: &[u8] = str::REFS_HEADS_MASTER.as_bytes();
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::str;
|
||||
|
||||
pub const RAD: &[u8] = str::RAD.as_bytes();
|
||||
pub const ID: &[u8] = str::ID.as_bytes();
|
||||
pub const IDS: &[u8] = str::IDS.as_bytes();
|
||||
pub const SELF: &[u8] = str::SELF.as_bytes();
|
||||
pub const SIGNED_REFS: &[u8] = str::SIGNED_REFS.as_bytes();
|
||||
pub const COBS: &[u8] = str::COBS.as_bytes();
|
||||
|
||||
pub const REFS_RAD_ID: &[u8] = str::REFS_RAD_ID.as_bytes();
|
||||
pub const REFS_RAD_SELF: &[u8] = str::REFS_RAD_SELF.as_bytes();
|
||||
pub const REFS_RAD_SIGNED_REFS: &[u8] = str::REFS_RAD_SIGNED_REFS.as_bytes();
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::{str, RefStr};
|
||||
|
||||
pub const RAD: &RefStr = RefStr::from_str(str::RAD);
|
||||
pub const ID: &RefStr = RefStr::from_str(str::ID);
|
||||
pub const IDS: &RefStr = RefStr::from_str(str::IDS);
|
||||
pub const SELF: &RefStr = RefStr::from_str(str::SELF);
|
||||
pub const SIGNED_REFS: &RefStr = RefStr::from_str(str::SIGNED_REFS);
|
||||
pub const COBS: &RefStr = RefStr::from_str(str::COBS);
|
||||
|
||||
pub const REFS_RAD_ID: &RefStr = RefStr::from_str(str::REFS_RAD_ID);
|
||||
pub const REFS_RAD_SELF: &RefStr = RefStr::from_str(str::REFS_RAD_SELF);
|
||||
pub const REFS_RAD_SIGNED_REFS: &RefStr = RefStr::from_str(str::REFS_RAD_SIGNED_REFS);
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
|
||||
const CHECK_OPTS: check::Options = check::Options {
|
||||
allow_pattern: false,
|
||||
allow_onelevel: true,
|
||||
};
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct RefStr(str);
|
||||
|
||||
impl RefStr {
|
||||
pub fn try_from_str(s: &str) -> Result<&RefStr, check::Error> {
|
||||
TryFrom::try_from(s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_ref_string(&self) -> RefString {
|
||||
self.to_owned()
|
||||
}
|
||||
|
||||
pub fn strip_prefix<P>(&self, base: P) -> Option<&RefStr>
|
||||
where
|
||||
P: AsRef<RefStr>,
|
||||
{
|
||||
self._strip_prefix(base.as_ref())
|
||||
}
|
||||
|
||||
fn _strip_prefix(&self, base: &RefStr) -> Option<&RefStr> {
|
||||
self.0
|
||||
.strip_prefix(base.as_str())
|
||||
.and_then(|s| s.strip_prefix('/'))
|
||||
.map(Self::from_str)
|
||||
}
|
||||
|
||||
/// Join `other` onto `self`, yielding a new [`RefString`].
|
||||
///
|
||||
/// Consider to use [`RefString::and`] when chaining multiple fragments
|
||||
/// together, and the intermediate values are not needed.
|
||||
pub fn join<R>(&self, other: R) -> RefString
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._join(other.as_ref())
|
||||
}
|
||||
|
||||
fn _join(&self, other: &RefStr) -> RefString {
|
||||
let mut buf = self.to_ref_string();
|
||||
buf.push(other);
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn to_pattern<P>(&self, pattern: P) -> PatternString
|
||||
where
|
||||
P: AsRef<PatternStr>,
|
||||
{
|
||||
self._to_pattern(pattern.as_ref())
|
||||
}
|
||||
|
||||
fn _to_pattern(&self, pattern: &PatternStr) -> PatternString {
|
||||
self.to_owned().with_pattern(pattern)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn qualified(&self) -> Option<Qualified> {
|
||||
Qualified::from_refstr(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn namespaced(&self) -> Option<Namespaced> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter {
|
||||
self.0.split('/')
|
||||
}
|
||||
|
||||
pub fn components(&self) -> Components {
|
||||
Components::from(self)
|
||||
}
|
||||
|
||||
pub fn head(&self) -> Component {
|
||||
self.components().next().expect("`RefStr` cannot be empty")
|
||||
}
|
||||
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
#[inline]
|
||||
pub fn as_bstr(&self) -> &bstr::BStr {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) const fn from_str(s: &str) -> &RefStr {
|
||||
unsafe { &*(s as *const str as *const RefStr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefStr {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
impl AsRef<bstr::BStr> for RefStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &bstr::BStr {
|
||||
use bstr::ByteSlice as _;
|
||||
self.as_str().as_bytes().as_bstr()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<RefStr> for &'a RefStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a RefStr {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s).map(|()| RefStr::from_str(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefStr) -> Cow<'a, RefStr> {
|
||||
Cow::Borrowed(rs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefStr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct RefString(String);
|
||||
|
||||
impl RefString {
|
||||
#[inline]
|
||||
pub fn as_refstr(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
|
||||
/// Join `other` onto `self` in place.
|
||||
///
|
||||
/// This is a consuming version of [`RefString::push`] which can be chained.
|
||||
/// Prefer this over chaining calls to [`RefStr::join`] if the
|
||||
/// intermediate values are not neede.
|
||||
pub fn and<R>(self, other: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._and(other.as_ref())
|
||||
}
|
||||
|
||||
fn _and(mut self, other: &RefStr) -> Self {
|
||||
self.push(other);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push<R>(&mut self, other: R)
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self.0.push('/');
|
||||
self.0.push_str(other.as_ref().as_str());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pop(&mut self) -> bool {
|
||||
match self.0.rfind('/') {
|
||||
None => false,
|
||||
Some(idx) => {
|
||||
self.0.truncate(idx);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a [`PatternStr`], turning self into a new [`PatternString`].
|
||||
pub fn with_pattern<P>(self, pattern: P) -> PatternString
|
||||
where
|
||||
P: AsRef<PatternStr>,
|
||||
{
|
||||
self._with_pattern(pattern.as_ref())
|
||||
}
|
||||
|
||||
fn _with_pattern(self, pattern: &PatternStr) -> PatternString {
|
||||
let mut buf = self.0;
|
||||
buf.push('/');
|
||||
buf.push_str(pattern.as_str());
|
||||
|
||||
PatternString(buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_qualified<'a>(self) -> Option<Qualified<'a>> {
|
||||
Qualified::from_refstr(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.0.reserve(additional)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
self.0.shrink_to_fit()
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
#[inline]
|
||||
pub fn into_bstring(self) -> bstr::BString {
|
||||
self.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
#[inline]
|
||||
pub fn as_bstr(&self) -> &bstr::BStr {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefString {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for RefString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
impl AsRef<bstr::BStr> for RefString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &bstr::BStr {
|
||||
use bstr::ByteSlice as _;
|
||||
self.as_str().as_bytes().as_bstr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<RefStr> for RefString {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &RefStr {
|
||||
RefStr::from_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for RefStr {
|
||||
type Owned = RefString;
|
||||
|
||||
#[inline]
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
RefString(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RefString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
RefStr::try_from_str(s).map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RefString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s.as_str()).map(|()| RefString(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefString> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefString) -> Cow<'a, RefStr> {
|
||||
Cow::Borrowed(rs.as_refstr())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RefString> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Cow<'a, RefStr> {
|
||||
Cow::Owned(rs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefString> for String {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Self {
|
||||
rs.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
impl From<RefString> for bstr::BString {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Self {
|
||||
bstr::BString::from(rs.0.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefString {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> FromIterator<A> for RefString
|
||||
where
|
||||
A: AsRef<RefStr>,
|
||||
{
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = A>,
|
||||
{
|
||||
let mut buf = String::new();
|
||||
for c in iter {
|
||||
buf.push_str(c.as_ref().as_str());
|
||||
buf.push('/');
|
||||
}
|
||||
assert!(!buf.is_empty(), "empty iterator");
|
||||
buf.truncate(buf.len() - 1);
|
||||
|
||||
Self(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> Extend<A> for RefString
|
||||
where
|
||||
A: AsRef<RefStr>,
|
||||
{
|
||||
fn extend<T>(&mut self, iter: T)
|
||||
where
|
||||
T: IntoIterator<Item = A>,
|
||||
{
|
||||
for x in iter {
|
||||
self.push(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// 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,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use super::{RefStr, RefString};
|
||||
use crate::lit;
|
||||
|
||||
pub type Iter<'a> = std::str::Split<'a, char>;
|
||||
|
||||
/// Iterator created by the [`RefStr::iter`] method.
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
#[derive(Clone)]
|
||||
pub struct Components<'a> {
|
||||
inner: std::str::Split<'a, char>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Components<'a> {
|
||||
type Item = Component<'a>;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner
|
||||
.next()
|
||||
.map(RefStr::from_str)
|
||||
.map(Cow::from)
|
||||
.map(Component)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Components<'a> {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefStr) -> Self {
|
||||
Self {
|
||||
inner: rs.as_str().split('/'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path component of a [`RefStr`].
|
||||
///
|
||||
/// A [`Component`] is a valid [`RefStr`] which does not contain any '/'
|
||||
/// separators.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Component<'a>(Cow<'a, RefStr>);
|
||||
|
||||
impl<'a> Component<'a> {
|
||||
#[inline]
|
||||
pub fn from_refstring(r: RefString) -> Option<Self> {
|
||||
if !r.contains('/') {
|
||||
Some(Self(Cow::Owned(r)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_lit<T: lit::Lit>(&self) -> Option<T> {
|
||||
T::from_component(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> Cow<'a, RefStr> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for Component<'a> {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for Component<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Option<Component<'a>> {
|
||||
#[inline]
|
||||
fn from(r: &'a RefStr) -> Self {
|
||||
if !r.contains('/') {
|
||||
Some(Component(Cow::from(r)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Component<'a>> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(c: Component<'a>) -> Self {
|
||||
c.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: lit::Lit> From<T> for Component<'static> {
|
||||
#[inline]
|
||||
fn from(_: T) -> Self {
|
||||
Component(Cow::from(T::NAME))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<lit::SomeLit<'a>> for Component<'a> {
|
||||
#[inline]
|
||||
fn from(s: lit::SomeLit<'a>) -> Self {
|
||||
use lit::SomeLit::*;
|
||||
|
||||
match s {
|
||||
Known(k) => k.into(),
|
||||
Any(c) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Component<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod component {
|
||||
use super::Component;
|
||||
use crate::name;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub const HEADS: Component = Component(Cow::Borrowed(name::HEADS));
|
||||
pub const MAIN: Component = Component(Cow::Borrowed(name::MAIN));
|
||||
pub const MASTER: Component = Component(Cow::Borrowed(name::MASTER));
|
||||
pub const NAMESPACES: Component = Component(Cow::Borrowed(name::NAMESPACES));
|
||||
pub const NOTES: Component = Component(Cow::Borrowed(name::NOTES));
|
||||
pub const ORIGIN: Component = Component(Cow::Borrowed(name::ORIGIN));
|
||||
pub const REFS: Component = Component(Cow::Borrowed(name::REFS));
|
||||
pub const REMOTES: Component = Component(Cow::Borrowed(name::REMOTES));
|
||||
pub const TAGS: Component = Component(Cow::Borrowed(name::TAGS));
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::*;
|
||||
|
||||
pub const RAD: Component = Component(Cow::Borrowed(name::RAD));
|
||||
pub const ID: Component = Component(Cow::Borrowed(name::ID));
|
||||
pub const IDS: Component = Component(Cow::Borrowed(name::IDS));
|
||||
pub const SELF: Component = Component(Cow::Borrowed(name::SELF));
|
||||
pub const SIGNED_REFS: Component = Component(Cow::Borrowed(name::SIGNED_REFS));
|
||||
pub const COBS: Component = Component(Cow::Borrowed(name::COBS));
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// 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::{Borrow, Cow},
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
iter::FromIterator,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{check, RefStr, RefString};
|
||||
|
||||
mod iter;
|
||||
pub use iter::{Component, Components, Iter};
|
||||
|
||||
pub const STAR: &PatternStr = PatternStr::from_str("*");
|
||||
|
||||
const CHECK_OPTS: check::Options = check::Options {
|
||||
allow_onelevel: true,
|
||||
allow_pattern: true,
|
||||
};
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct PatternStr(str);
|
||||
|
||||
impl PatternStr {
|
||||
#[inline]
|
||||
pub fn try_from_str(s: &str) -> Result<&Self, check::Error> {
|
||||
TryFrom::try_from(s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn join<R>(&self, other: R) -> PatternString
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._join(other.as_ref())
|
||||
}
|
||||
|
||||
fn _join(&self, other: &RefStr) -> PatternString {
|
||||
let mut buf = self.to_owned();
|
||||
buf.push(other);
|
||||
buf
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self) -> Iter {
|
||||
self.0.split('/')
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn components(&self) -> Components {
|
||||
Components::from(self)
|
||||
}
|
||||
|
||||
pub(crate) const fn from_str(s: &str) -> &PatternStr {
|
||||
unsafe { &*(s as *const str as *const PatternStr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PatternStr {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PatternStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for PatternStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a PatternStr {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s).map(|()| PatternStr::from_str(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for &'a PatternStr {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefStr) -> Self {
|
||||
PatternStr::from_str(rs.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternStr> for Cow<'a, PatternStr> {
|
||||
#[inline]
|
||||
fn from(p: &'a PatternStr) -> Cow<'a, PatternStr> {
|
||||
Cow::Borrowed(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PatternStr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct PatternString(pub(crate) String);
|
||||
|
||||
impl PatternString {
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_pattern_str(&self) -> &PatternStr {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_components<'a, T>(iter: T) -> Result<Self, DuplicateGlob>
|
||||
where
|
||||
T: IntoIterator<Item = Component<'a>>,
|
||||
{
|
||||
iter.into_iter().collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn and<R>(mut self, other: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._push(other.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn push<R>(&mut self, other: R)
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._push(other.as_ref())
|
||||
}
|
||||
|
||||
fn _push(&mut self, other: &RefStr) {
|
||||
self.0.push('/');
|
||||
self.0.push_str(other.as_str());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pop(&mut self) -> bool {
|
||||
match self.0.rfind('/') {
|
||||
None => false,
|
||||
Some(idx) => {
|
||||
self.0.truncate(idx);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PatternString {
|
||||
type Target = PatternStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<PatternStr> for PatternString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &PatternStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PatternString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<PatternStr> for PatternString {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &PatternStr {
|
||||
PatternStr::from_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for PatternStr {
|
||||
type Owned = PatternString;
|
||||
|
||||
#[inline]
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
PatternString(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefString> for PatternString {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Self {
|
||||
Self(rs.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternString> for Cow<'a, PatternStr> {
|
||||
#[inline]
|
||||
fn from(p: &'a PatternString) -> Cow<'a, PatternStr> {
|
||||
Cow::Borrowed(p.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternString> for String {
|
||||
#[inline]
|
||||
fn from(p: PatternString) -> Self {
|
||||
p.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PatternString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
PatternStr::try_from_str(s).map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for PatternString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s.as_str()).map(|()| PatternString(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("more than one '*' encountered")]
|
||||
pub struct DuplicateGlob;
|
||||
|
||||
impl<'a> FromIterator<Component<'a>> for Result<PatternString, DuplicateGlob> {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Component<'a>>,
|
||||
{
|
||||
use Component::*;
|
||||
|
||||
let mut buf = String::new();
|
||||
let mut seen_glob = false;
|
||||
for c in iter {
|
||||
if let Glob(_) = c {
|
||||
if seen_glob {
|
||||
return Err(DuplicateGlob);
|
||||
}
|
||||
|
||||
seen_glob = true;
|
||||
}
|
||||
|
||||
buf.push_str(c.as_str());
|
||||
buf.push('/');
|
||||
}
|
||||
buf.truncate(buf.len() - 1);
|
||||
|
||||
Ok(PatternString(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PatternString {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// 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 super::PatternStr;
|
||||
use crate::RefStr;
|
||||
|
||||
pub type Iter<'a> = std::str::Split<'a, char>;
|
||||
|
||||
pub enum Component<'a> {
|
||||
Glob(Option<&'a PatternStr>),
|
||||
Normal(&'a RefStr),
|
||||
}
|
||||
|
||||
impl Component<'_> {
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Component<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Glob(None) => "*",
|
||||
Self::Glob(Some(x)) => x.as_str(),
|
||||
Self::Normal(x) => x.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Component<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
#[derive(Clone)]
|
||||
pub struct Components<'a> {
|
||||
inner: Iter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Components<'a> {
|
||||
type Item = Component<'a>;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(|next| match next {
|
||||
"*" => Component::Glob(None),
|
||||
x if x.contains('*') => Component::Glob(Some(PatternStr::from_str(x))),
|
||||
x => Component::Normal(RefStr::from_str(x)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternStr> for Components<'a> {
|
||||
#[inline]
|
||||
fn from(p: &'a PatternStr) -> Self {
|
||||
Self {
|
||||
inner: p.as_str().split('/'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// 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;
|
||||
|
||||
use ::serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
refspec::{PatternStr, PatternString},
|
||||
Namespaced,
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for &'a RefStr {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer)
|
||||
.and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for &'a RefStr {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RefString {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).map(|x: &RefStr| x.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RefString {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for &'a PatternStr {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer)
|
||||
.and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for &'a PatternStr {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PatternString {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).map(|x: &PatternStr| x.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PatternString {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for Qualified<'a> {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).and_then(|s: &RefStr| {
|
||||
s.qualified()
|
||||
.ok_or_else(|| de::Error::custom("not a qualified ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Qualified<'_> {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for Namespaced<'a> {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).and_then(|s: &RefStr| {
|
||||
s.namespaced()
|
||||
.ok_or_else(|| de::Error::custom("not a namespaced ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "git-ref-format-macro"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
proc-macro = true
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1.0.4"
|
||||
quote = "1"
|
||||
syn = "1"
|
||||
|
||||
[dependencies.git-ref-format-core]
|
||||
path = "../core"
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, LitStr};
|
||||
|
||||
use git_ref_format_core::{refspec::PatternStr, Component, Error, RefStr};
|
||||
|
||||
/// Create a [`git_ref_format_core::RefString`] from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn refname(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
let val = lit.value();
|
||||
|
||||
let parsed: Result<&RefStr, Error> = val.as_str().try_into();
|
||||
match parsed {
|
||||
Ok(safe) => {
|
||||
let safe: &str = safe.as_str();
|
||||
let expand = quote! {
|
||||
unsafe {
|
||||
use ::std::mem::transmute;
|
||||
use ::git_ref_format::RefString;
|
||||
|
||||
transmute::<_, RefString>(#safe.to_owned())
|
||||
}
|
||||
};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid refname literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`git_ref_format_core::Component`] from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn component(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
let val = lit.value();
|
||||
|
||||
let name: Result<&RefStr, Error> = val.as_str().try_into();
|
||||
match name {
|
||||
Ok(name) => {
|
||||
let comp: Option<Component> = name.into();
|
||||
match comp {
|
||||
Some(safe) => {
|
||||
let safe: &str = safe.as_ref().as_str();
|
||||
let expand = quote! {
|
||||
unsafe {
|
||||
use ::std::{borrow::Cow, mem::transmute};
|
||||
use ::git_ref_format::{Component, RefStr, RefString};
|
||||
|
||||
let inner: RefString = transmute(#safe.to_owned());
|
||||
let cow: Cow<'static, RefStr> = Cow::Owned(inner);
|
||||
transmute::<_, Component>(cow)
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
None => {
|
||||
abort!(lit.span(), "component contains a '/'");
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid refname literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`git_ref_format_core::refspec::PatternString`] from a string
|
||||
/// literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn pattern(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
let val = lit.value();
|
||||
|
||||
let parsed: Result<&PatternStr, Error> = val.as_str().try_into();
|
||||
match parsed {
|
||||
Ok(safe) => {
|
||||
let safe: &str = safe.as_str();
|
||||
let expand = quote! {
|
||||
unsafe {
|
||||
use ::std::mem::transmute;
|
||||
use ::git_ref_format::refspec::PatternString;
|
||||
|
||||
transmute::<_, PatternString>(#safe.to_owned())
|
||||
}
|
||||
};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid refspec pattern literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
//! Everything you never knew you wanted for handling git ref names.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! This crate provides a number of types which allow to validate git ref names,
|
||||
//! create new ones which are valid by construction, make assertions about their
|
||||
//! structure, and deconstruct them into their components.
|
||||
//!
|
||||
//! ## Basic Types
|
||||
//!
|
||||
//! The basic types are:
|
||||
//!
|
||||
//! * [`RefStr`]
|
||||
//! * [`RefString`]
|
||||
//!
|
||||
//! They are wrappers around [`str`] and [`String`] respectively, with the
|
||||
//! additional guarantee that they are also valid ref names as per
|
||||
//! [`git-check-ref-format`] (which is also exposed directly as
|
||||
//! [`check_ref_format`]). Both types are referred to as "ref strings".
|
||||
//!
|
||||
//! Note that this implies that ref names must be valid UTF-8, which git itself
|
||||
//! doesn't require.
|
||||
//!
|
||||
//! Ref strings can be iterated over, either yielding `&str` or [`Component`]. A
|
||||
//! [`Component`] is guaranteed to not contain a '/' separator, and can thus
|
||||
//! also be used to conveniently construct known-valid ref strings. The [`lit`]
|
||||
//! module contains a number of types (and `const` values thereof) which can be
|
||||
//! coerced into [`Component`], and thus can be used to construct known-valid
|
||||
//! ref strings.
|
||||
//!
|
||||
//! The [`name`] module also provides a number of constant values of commonly
|
||||
//! used ref strings / components, which are useful for pattern matching.
|
||||
//!
|
||||
//! The `"macro"` feature enables the `refstring!` and `component!` macros,
|
||||
//! which can be convenient to construct compile-time validated [`RefString`]s
|
||||
//! respectively [`Component`]s.
|
||||
//!
|
||||
//! ## Refspec Patterns
|
||||
//!
|
||||
//! The types
|
||||
//!
|
||||
//! * [`refspec::PatternStr`]
|
||||
//! * [`refspec::PatternString`]
|
||||
//!
|
||||
//! guarantee that their values are valid ref strings but additionally _may_
|
||||
//! contain at most one "*" character. It is thus possible to convert a ref
|
||||
//! string to a refspec pattern, but not the other way round. Refspec patterns
|
||||
//! are commonly used for mapping remote to local refs (cf. [`git-fetch`]).
|
||||
//!
|
||||
//! The `"macro"` feature enables the `refspec::pattern!` macro, which
|
||||
//! constructs a compile-time validated [`refspec::PatternString`].
|
||||
//!
|
||||
//! ## Structured Ref Strings
|
||||
//!
|
||||
//! Ref strings may be [`Qualified`], which essentially means that they start
|
||||
//! with "refs/". [`Qualified`] ref string also require at least three
|
||||
//! components (eg. "refs/heads/main"), which makes it easier to deal with
|
||||
//! common naming conventions.
|
||||
//!
|
||||
//! [`Qualified`] refs may be [`Namespaced`], or can be given a namespace
|
||||
//! (namespaces can be nested). [`Namespaced`] refs are also [`Qualified`], and
|
||||
//! can have their namespace(s) stripped.
|
||||
//!
|
||||
//! # On Git Ref Name Conventions
|
||||
//!
|
||||
//! Git references are essentially path names pointing to their traditional
|
||||
//! storage location in a the repository (`$GIT_DIR/refs`). Unlike (UNIX) file
|
||||
//! paths, they are subject to a few restrictions, as described in
|
||||
//! [`git-check-ref-format`].
|
||||
//!
|
||||
//! On top of that, there are a number of conventions around the hierarchical
|
||||
//! naming, _some_ of which are treated specially by tools such as the `git`
|
||||
//! CLI. For example:
|
||||
//!
|
||||
//! * `refs/heads/..` are also called "branches".
|
||||
//!
|
||||
//! Omitting the "refs/heads/" prefix is typically accepted. Such a branch
|
||||
//! name is also referred to as a "shorthand" ref.
|
||||
//!
|
||||
//! * `refs/tags/..` are assumed to contain tags.
|
||||
//!
|
||||
//! `git` treats tags specially, specifically it insists that they be globally
|
||||
//! unique across all copies of the repository.
|
||||
//!
|
||||
//! * `refs/remotes/../..` is where "remote tracking branches" are stored.
|
||||
//!
|
||||
//! In `git`, the first element after "remotes" is considered the name of the
|
||||
//! [remote][git-remote] (as it appears in the config file), while everything
|
||||
//! after that is considered a shorthand branch. Note, however, that the
|
||||
//! remote name may itself contain '/' separators, so it is not generally
|
||||
//! possible to extract the branch name without access to the config.
|
||||
//!
|
||||
//! * `refs/namespaces/..` is hidden unless [`gitnamespaces`] are in effect.
|
||||
//!
|
||||
//! The structure of namespaces is recursive: they contain full refs, which
|
||||
//! can themselves be namespaces (eg.
|
||||
//! `refs/namespaces/a/refs/namespaces/b/refs/heads/branch`). Note that,
|
||||
//! unlike remote names, namespace names can **not** contain forward slashes
|
||||
//! but there is no tooling which would enforce that.
|
||||
//!
|
||||
//! There are also other such ref hierachies `git` knows about, and this crate
|
||||
//! doesn't attempt to cover all of them. More importantly, `git` does not
|
||||
//! impose any restrictions on ref hierarchies: as long as they don't collide
|
||||
//! with convential ones, applications can introduce any hierchies they want.
|
||||
//!
|
||||
//! This restricts the transformations between conventional refs which can be
|
||||
//! made without additional information besides the ref name: for example, it is
|
||||
//! not generally possible to turn a remote tracking branch into a branch (or a
|
||||
//! shorthand) without knowning about all possible remote names.
|
||||
//!
|
||||
//! Therefore, this crate doesn't attempt to interpret all possible semantics
|
||||
//! associated with refs, and instead tries to make it easy for library
|
||||
//! consumers to do so.
|
||||
//!
|
||||
//! [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
|
||||
//! [`git-fetch`]: https://git-scm.com/docs/git-fetch
|
||||
//! [git-remote]: https://git-scm.com/docs/git-remote
|
||||
//! [`gitnamespaces`]: https://git-scm.com/docs/gitnamespaces
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub use git_ref_format_core::PercentEncode;
|
||||
pub use git_ref_format_core::{
|
||||
check_ref_format,
|
||||
lit,
|
||||
name::component,
|
||||
Component,
|
||||
DuplicateGlob,
|
||||
Error,
|
||||
Namespaced,
|
||||
Options,
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
|
||||
pub mod name {
|
||||
pub use git_ref_format_core::name::*;
|
||||
|
||||
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
|
||||
pub use git_ref_format_macro::component;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
|
||||
pub use git_ref_format_macro::refname;
|
||||
|
||||
pub mod refspec {
|
||||
pub use git_ref_format_core::refspec::*;
|
||||
|
||||
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
|
||||
pub use git_ref_format_macro::pattern;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "git-ref-format-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
proptest = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5"
|
||||
|
||||
[dev-dependencies.git-ref-format]
|
||||
path = ".."
|
||||
features = ["macro", "minicbor", "serde"]
|
||||
|
||||
[dev-dependencies.test-helpers]
|
||||
path = "../../test/test-helpers"
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
/// Any unicode "word" is trivially a valid refname.
|
||||
pub fn trivial() -> impl Strategy<Value = String> {
|
||||
"\\w+"
|
||||
}
|
||||
|
||||
pub fn valid() -> impl Strategy<Value = String> {
|
||||
prop::collection::vec(trivial(), 1..20).prop_map(|xs| xs.join("/"))
|
||||
}
|
||||
|
||||
pub fn invalid_char() -> impl Strategy<Value = char> {
|
||||
prop_oneof![
|
||||
Just('\0'),
|
||||
Just('\\'),
|
||||
Just('~'),
|
||||
Just('^'),
|
||||
Just(':'),
|
||||
Just('?'),
|
||||
Just('[')
|
||||
]
|
||||
}
|
||||
|
||||
pub fn with_invalid_char() -> impl Strategy<Value = String> {
|
||||
("\\w*", invalid_char(), "\\w*").prop_map(|(mut pre, invalid, suf)| {
|
||||
pre.push(invalid);
|
||||
pre.push_str(&suf);
|
||||
pre
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ends_with_dot_lock() -> impl Strategy<Value = String> {
|
||||
"\\w*\\.lock"
|
||||
}
|
||||
|
||||
pub fn with_double_dot() -> impl Strategy<Value = String> {
|
||||
"\\w*\\.\\.\\w*"
|
||||
}
|
||||
|
||||
pub fn starts_with_dot() -> impl Strategy<Value = String> {
|
||||
"\\.\\w*"
|
||||
}
|
||||
|
||||
pub fn ends_with_dot() -> impl Strategy<Value = String> {
|
||||
"\\w+\\."
|
||||
}
|
||||
|
||||
pub fn with_control_char() -> impl Strategy<Value = String> {
|
||||
"\\w*[\x01-\x1F\x7F]+\\w*"
|
||||
}
|
||||
|
||||
pub fn with_space() -> impl Strategy<Value = String> {
|
||||
"\\w* +\\w*"
|
||||
}
|
||||
|
||||
pub fn with_consecutive_slashes() -> impl Strategy<Value = String> {
|
||||
"\\w*//\\w*"
|
||||
}
|
||||
|
||||
pub fn with_glob() -> impl Strategy<Value = String> {
|
||||
"\\w*\\*\\w*"
|
||||
}
|
||||
|
||||
pub fn multi_glob() -> impl Strategy<Value = String> {
|
||||
(
|
||||
prop::collection::vec(with_glob(), 2..5),
|
||||
prop::collection::vec(trivial(), 0..5),
|
||||
)
|
||||
.prop_map(|(mut globs, mut valids)| {
|
||||
globs.append(&mut valids);
|
||||
globs
|
||||
})
|
||||
.prop_shuffle()
|
||||
.prop_map(|xs| xs.join("/"))
|
||||
}
|
||||
|
||||
pub fn invalid() -> impl Strategy<Value = String> {
|
||||
fn path(s: impl Strategy<Value = String>) -> impl Strategy<Value = String> {
|
||||
prop::collection::vec(s, 1..20).prop_map(|xs| xs.join("/"))
|
||||
}
|
||||
|
||||
prop_oneof![
|
||||
Just(String::from("")),
|
||||
Just(String::from("@")),
|
||||
path(with_invalid_char()),
|
||||
path(ends_with_dot_lock()),
|
||||
path(with_double_dot()),
|
||||
path(starts_with_dot()),
|
||||
path(ends_with_dot()),
|
||||
path(with_control_char()),
|
||||
path(with_space()),
|
||||
path(with_consecutive_slashes()),
|
||||
path(trivial()).prop_map(|mut p| {
|
||||
p.push('/');
|
||||
p
|
||||
}),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate assert_matches;
|
||||
|
||||
#[cfg(any(test, feature = "test"))]
|
||||
pub mod gen;
|
||||
#[cfg(test)]
|
||||
mod properties;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use git_ref_format::{check_ref_format, Error, Options};
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::gen;
|
||||
|
||||
mod name;
|
||||
mod pattern;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn disallow_onelevel(input in gen::trivial(), allow_pattern in any::<bool>()) {
|
||||
assert_matches!(
|
||||
check_ref_format(Options {
|
||||
allow_onelevel: false,
|
||||
allow_pattern,
|
||||
},
|
||||
&input
|
||||
),
|
||||
Err(Error::OneLevel)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use git_ref_format::{Error, RefStr, RefString};
|
||||
use proptest::prelude::*;
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
use crate::gen;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn valid(input in gen::valid()) {
|
||||
assert_eq!(input.as_str(), RefStr::try_from_str(&input).unwrap().as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_char(input in gen::with_invalid_char()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::InvalidChar(_)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_lock(input in gen::ends_with_dot_lock()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::DotLock))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_dot(input in gen::with_double_dot()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::DotDot))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starts_dot(input in gen::starts_with_dot()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::StartsDot))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ends_dot(input in gen::ends_with_dot()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::EndsDot))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_char(input in gen::with_control_char()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::Control))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space(input in gen::with_space()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::Space))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_slashes(input in gen::with_consecutive_slashes()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::Slash))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob(input in gen::with_glob()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::InvalidChar('*')))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid(input in gen::invalid()) {
|
||||
assert_matches!(RefString::try_from(input), Err(_))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_components(input in gen::valid()) {
|
||||
assert_eq!(
|
||||
input.as_str(),
|
||||
RefStr::try_from_str(&input).unwrap().components().collect::<RefString>().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json(input in gen::valid()) {
|
||||
roundtrip::json(RefString::try_from(input).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor(input in gen::valid()) {
|
||||
roundtrip::cbor(RefString::try_from(input).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use git_ref_format::{refspec, Error};
|
||||
use proptest::prelude::*;
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
use crate::gen;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn valid(input in gen::with_glob()) {
|
||||
assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refname_is_pattern(input in gen::valid()) {
|
||||
assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_more_than_one_star(input in gen::multi_glob()) {
|
||||
assert_matches!(refspec::PatternString::try_from(input), Err(Error::Pattern))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_refname_is_invalid_pattern(input in gen::invalid()) {
|
||||
assert_matches!(refspec::PatternString::try_from(input), Err(_))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_components(input in gen::with_glob()) {
|
||||
assert_eq!(
|
||||
input.as_str(),
|
||||
refspec::PatternStr::try_from_str(&input)
|
||||
.unwrap()
|
||||
.components()
|
||||
.collect::<Result<refspec::PatternString, _>>()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json(input in gen::with_glob()) {
|
||||
roundtrip::json(refspec::PatternString::try_from(input).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor(input in gen::with_glob()) {
|
||||
roundtrip::cbor(refspec::PatternString::try_from(input).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use git_ref_format::{component, name, refname, refspec, Error, Qualified, RefStr, RefString};
|
||||
|
||||
#[test]
|
||||
fn refname_macro_works() {
|
||||
assert_eq!("refs/heads/main", refname!("refs/heads/main").as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_macro_works() {
|
||||
assert_eq!("self", name::component!("self").as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_macro_works() {
|
||||
assert_eq!("refs/heads/*", refspec::pattern!("refs/heads/*").as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_matches!(RefStr::try_from_str(""), Err(Error::Empty));
|
||||
assert_matches!(RefString::try_from("".to_owned()), Err(Error::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join() {
|
||||
let s = name::REFS.join(name::HEADS);
|
||||
let t = s.join(name::MAIN);
|
||||
assert_eq!("refs/heads", s.as_str());
|
||||
assert_eq!("refs/heads/main", t.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_and() {
|
||||
assert_eq!(
|
||||
"refs/heads/this/that",
|
||||
name::REFS
|
||||
.join(name::HEADS)
|
||||
.and(refname!("this"))
|
||||
.and(refname!("that"))
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_prefix() {
|
||||
assert_eq!(
|
||||
"main",
|
||||
name::REFS_HEADS_MAIN
|
||||
.strip_prefix(refname!("refs/heads"))
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_prefix_not_prefix() {
|
||||
assert!(name::REFS_HEADS_MAIN
|
||||
.strip_prefix(refname!("refs/tags"))
|
||||
.is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified() {
|
||||
assert_eq!(
|
||||
"refs/heads/main",
|
||||
name::REFS_HEADS_MAIN.qualified().unwrap().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_tag() {
|
||||
assert_eq!(
|
||||
"refs/tags/v1",
|
||||
refname!("refs/tags/v1").qualified().unwrap().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_remote_tracking() {
|
||||
assert_eq!(
|
||||
"refs/remotes/origin/master",
|
||||
refname!("refs/remotes/origin/master")
|
||||
.qualified()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_qualified() {
|
||||
assert!(name::MAIN.qualified().is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components() {
|
||||
assert_eq!(
|
||||
"refs/heads/main",
|
||||
Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components_with_iter() {
|
||||
assert_eq!(
|
||||
"refs/heads/foo/bar/baz",
|
||||
Qualified::from_components(
|
||||
component::HEADS,
|
||||
name::component!("foo"),
|
||||
[name::component!("bar"), name::component!("baz")]
|
||||
)
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components_non_empty_iter() {
|
||||
let q = Qualified::from_components(component::HEADS, component::MAIN, None);
|
||||
let (refs, heads, main, mut empty) = q.non_empty_iter();
|
||||
assert!(empty.next().is_none());
|
||||
assert_eq!(("refs", "heads", "main"), (refs, heads, main))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components_non_empty_components() {
|
||||
let q = Qualified::from_components(component::HEADS, component::MAIN, Some(component::MASTER));
|
||||
let (refs, heads, main, mut master) = q.non_empty_components();
|
||||
assert_eq!(
|
||||
(
|
||||
component::REFS,
|
||||
component::HEADS,
|
||||
component::MAIN,
|
||||
component::MASTER
|
||||
),
|
||||
(refs, heads, main, master.next().unwrap())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespaced() {
|
||||
assert_eq!(
|
||||
"refs/namespaces/foo/refs/heads/main",
|
||||
refname!("refs/namespaces/foo/refs/heads/main")
|
||||
.namespaced()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_namespaced() {
|
||||
assert!(name::REFS_HEADS_MAIN.namespaced().is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_namespaced_because_not_qualified() {
|
||||
assert!(refname!("refs/namespaces/foo/banana")
|
||||
.namespaced()
|
||||
.is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_namespace() {
|
||||
assert_eq!(
|
||||
"refs/rad/id",
|
||||
refname!("refs/namespaces/xyz/refs/rad/id")
|
||||
.namespaced()
|
||||
.unwrap()
|
||||
.strip_namespace()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_nested_namespaces() {
|
||||
let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
|
||||
let namespaced = full.namespaced().unwrap();
|
||||
let strip_first = namespaced.strip_namespace();
|
||||
let nested = strip_first.namespaced().unwrap();
|
||||
let strip_second = nested.strip_namespace();
|
||||
|
||||
assert_eq!("a", namespaced.namespace().as_str());
|
||||
assert_eq!("b", nested.namespace().as_str());
|
||||
assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
|
||||
assert_eq!("refs/heads/main", strip_second.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_namespace() {
|
||||
assert_eq!(
|
||||
"refs/namespaces/foo/refs/heads/main",
|
||||
name::REFS_HEADS_MAIN
|
||||
.qualified()
|
||||
.unwrap()
|
||||
.add_namespace(refname!("foo").head())
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
assert_eq!(
|
||||
vec!["refs", "heads", "main"],
|
||||
name::REFS_HEADS_MAIN.iter().collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_pop() {
|
||||
let mut s = name::REFS.to_owned();
|
||||
s.push(name::HEADS);
|
||||
s.push(name::MAIN);
|
||||
|
||||
assert_eq!("refs/heads/main", s.as_str());
|
||||
assert!(s.pop());
|
||||
assert!(s.pop());
|
||||
assert_eq!("refs", s.as_str());
|
||||
assert!(!s.pop());
|
||||
assert_eq!("refs", s.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_pattern() {
|
||||
assert_eq!(
|
||||
"refs/heads/*",
|
||||
refname!("refs/heads")
|
||||
.to_pattern(refspec::pattern!("*"))
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_pattern() {
|
||||
assert_eq!(
|
||||
"refs/heads/*",
|
||||
refname!("refs/heads").with_pattern(refspec::STAR).as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_pattern_and() {
|
||||
assert_eq!(
|
||||
"refs/*/heads",
|
||||
refname!("refs")
|
||||
.with_pattern(refspec::STAR)
|
||||
.and(name::HEADS)
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect() {
|
||||
assert_eq!(
|
||||
"refs/heads/main",
|
||||
IntoIterator::into_iter([name::REFS, name::HEADS, name::MAIN])
|
||||
.collect::<RefString>()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_components() {
|
||||
let a = name::REFS_HEADS_MAIN.to_owned();
|
||||
let b = a.components().collect();
|
||||
assert_eq!(a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_pattern_duplicate_glob() {
|
||||
assert_matches!(
|
||||
IntoIterator::into_iter([
|
||||
refspec::Component::Normal(name::REFS),
|
||||
refspec::Component::Glob(None),
|
||||
refspec::Component::Glob(Some(refspec::pattern!("fo*").as_ref()))
|
||||
])
|
||||
.collect::<Result<_, _>>(),
|
||||
Err(refspec::DuplicateGlob)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user