radicle-git init

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

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

[0]: https://github.com/radicle-dev/radicle-link/
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Fintan Halpenny
2022-09-08 12:07:51 +01:00
parent c43420956c
commit 70b571b67b
112 changed files with 10341 additions and 7 deletions
+25
View File
@@ -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
+34
View File
@@ -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
+117
View File
@@ -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)
}
}
+106
View File
@@ -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(())
}
},
}
}
+437
View File
@@ -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)
}
}
+25
View File
@@ -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;
+313
View File
@@ -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::*;
+522
View File
@@ -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)
}
}
}
+162
View File
@@ -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::*;
}
+294
View File
@@ -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)
}
}
+69
View File
@@ -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('/'),
}
}
}
+144
View File
@@ -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())
}
}
+19
View File
@@ -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"
+122
View File
@@ -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);
},
}
}
+155
View File
@@ -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;
}
+28
View File
@@ -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"
+101
View File
@@ -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
}),
]
}
+13
View File
@@ -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;
+25
View File
@@ -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)
)
}
}
+85
View File
@@ -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())
}
}
+281
View File
@@ -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)
)
}