mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-24 13:50:36 +08:00
Merge remote-tracking branch 'origin/mergers'
* origin/mergers: radicle-git-ext: merge in git-commit Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -6,8 +6,6 @@ members = [
|
||||
"git-trailers",
|
||||
"link-git",
|
||||
"radicle-git-ext",
|
||||
"radicle-git-types",
|
||||
"radicle-macros",
|
||||
"radicle-std-ext",
|
||||
"radicle-surf",
|
||||
# TODO: port gitd-lib over
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use git_ext as ext;
|
||||
use git_ref_format::refspec::PatternString;
|
||||
|
||||
pub trait Pattern {
|
||||
@@ -27,12 +26,6 @@ impl Pattern for globset::GlobSet {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RefspecMatcher(globset::GlobMatcher);
|
||||
|
||||
impl From<ext::RefspecPattern> for RefspecMatcher {
|
||||
fn from(pat: ext::RefspecPattern) -> Self {
|
||||
Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternString> for RefspecMatcher {
|
||||
fn from(pat: PatternString) -> Self {
|
||||
Self(globset::Glob::new(pat.as_str()).unwrap().compile_matcher())
|
||||
|
||||
@@ -35,7 +35,7 @@ path = "../../test/test-helpers"
|
||||
version = "1"
|
||||
features = ["v4"]
|
||||
|
||||
[dev-dependencies.git-ext-test]
|
||||
[dev-dependencies.radicle-git-ext-test]
|
||||
path = "../../radicle-git-ext/t"
|
||||
features = ["test"]
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use std::{
|
||||
fmt,
|
||||
num::ParseIntError,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// The data for indicating authorship of an action within a
|
||||
/// [`crate::commit::Commit`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Author {
|
||||
/// Name corresponding to `user.name` in the git config.
|
||||
///
|
||||
/// Note: this must not contain `<` or `>`.
|
||||
pub name: String,
|
||||
/// Email corresponding to `user.email` in the git config.
|
||||
///
|
||||
/// Note: this must not contain `<` or `>`.
|
||||
pub email: String,
|
||||
/// The time of this author's action.
|
||||
pub time: Time,
|
||||
}
|
||||
|
||||
/// The time of a [`Author`]'s action.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Time {
|
||||
seconds: i64,
|
||||
offset: i32,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
pub fn new(seconds: i64, offset: i32) -> Self {
|
||||
Self { seconds, offset }
|
||||
}
|
||||
|
||||
/// Return the time, in seconds, since the epoch.
|
||||
pub fn seconds(&self) -> i64 {
|
||||
self.seconds
|
||||
}
|
||||
|
||||
/// Return the timezone offset, in minutes.
|
||||
pub fn offset(&self) -> i32 {
|
||||
self.offset
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Time> for git2::Time {
|
||||
fn from(t: Time) -> Self {
|
||||
Self::new(t.seconds, t.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git2::Time> for Time {
|
||||
fn from(t: git2::Time) -> Self {
|
||||
Self::new(t.seconds(), t.offset_minutes())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Time {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let sign = if self.offset.is_negative() { '-' } else { '+' };
|
||||
write!(f, "{} {}{:0>4}", self.seconds, sign, self.offset.abs())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Author {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} <{}> {}", self.name, self.email, self.time,)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Author> for git2::Signature<'_> {
|
||||
type Error = git2::Error;
|
||||
|
||||
fn try_from(person: &Author) -> Result<Self, Self::Error> {
|
||||
let time = git2::Time::new(person.time.seconds, person.time.offset);
|
||||
git2::Signature::new(&person.name, &person.email, &time)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&git2::Signature<'a>> for Author {
|
||||
type Error = str::Utf8Error;
|
||||
|
||||
fn try_from(value: &git2::Signature<'a>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
name: str::from_utf8(value.name_bytes())?.to_string(),
|
||||
email: str::from_utf8(value.email_bytes())?.to_string(),
|
||||
time: value.when().into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseError {
|
||||
#[error("missing '{0}' while parsing person signature")]
|
||||
Missing(&'static str),
|
||||
#[error("offset was incorrect format while parsing person signature")]
|
||||
Offset(#[source] ParseIntError),
|
||||
#[error("time was incorrect format while parsing person signature")]
|
||||
Time(#[source] ParseIntError),
|
||||
#[error("time offset is expected to be '+'/'-' for a person siganture")]
|
||||
UnknownOffset,
|
||||
}
|
||||
|
||||
impl FromStr for Author {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut components = s.split(' ');
|
||||
let offset = match components.next_back() {
|
||||
None => return Err(ParseError::Missing("offset")),
|
||||
Some(offset) => offset.parse::<i32>().map_err(ParseError::Offset)?,
|
||||
};
|
||||
let time = match components.next_back() {
|
||||
None => return Err(ParseError::Missing("time")),
|
||||
Some(time) => time.parse::<i64>().map_err(ParseError::Time)?,
|
||||
};
|
||||
let time = Time::new(time, offset);
|
||||
|
||||
let email = components
|
||||
.next_back()
|
||||
.ok_or(ParseError::Missing("email"))?
|
||||
.trim_matches(|c| c == '<' || c == '>')
|
||||
.to_owned();
|
||||
let name = components.collect::<Vec<_>>().join(" ");
|
||||
Ok(Self { name, email, time })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//! The `git-commit` crate provides parsing a displaying of a [git
|
||||
//! commit][git-commit].
|
||||
//!
|
||||
//! The [`Commit`] data can be constructed using the `FromStr`
|
||||
//! implementation, or by converting from a `git2::Buf`.
|
||||
//!
|
||||
//! The [`Headers`] can be accessed via [`Commit::headers`]. If the
|
||||
//! signatures of the commit are of particular interest, the
|
||||
//! [`Commit::signatures`] method can be used, which returns a series of
|
||||
//! [`Signature`]s.
|
||||
//!
|
||||
//! [git-commit]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||
|
||||
pub mod headers;
|
||||
pub mod trailers;
|
||||
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
use git2::{ObjectType, Oid};
|
||||
|
||||
use headers::{Headers, Signature};
|
||||
use trailers::{OwnedTrailer, Trailer, Trailers};
|
||||
|
||||
use crate::author::Author;
|
||||
|
||||
/// A git commit in its object description form, i.e. the output of
|
||||
/// `git cat-file` for a commit object.
|
||||
#[derive(Debug)]
|
||||
pub struct Commit {
|
||||
tree: Oid,
|
||||
parents: Vec<Oid>,
|
||||
author: Author,
|
||||
committer: Author,
|
||||
headers: Headers,
|
||||
message: String,
|
||||
trailers: Vec<OwnedTrailer>,
|
||||
}
|
||||
|
||||
impl Commit {
|
||||
pub fn new<I, T>(
|
||||
tree: Oid,
|
||||
parents: Vec<Oid>,
|
||||
author: Author,
|
||||
committer: Author,
|
||||
headers: Headers,
|
||||
message: String,
|
||||
trailers: I,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
OwnedTrailer: From<T>,
|
||||
{
|
||||
let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
|
||||
Self {
|
||||
tree,
|
||||
parents,
|
||||
author,
|
||||
committer,
|
||||
headers,
|
||||
message,
|
||||
trailers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the [`Commit`] from the `repo` that is expected to be found at
|
||||
/// `oid`.
|
||||
pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
|
||||
let odb = repo.odb()?;
|
||||
let object = odb.read(oid)?;
|
||||
Ok(Commit::try_from(object.data())?)
|
||||
}
|
||||
|
||||
/// Write the given [`Commit`] to the `repo`. The resulting `Oid`
|
||||
/// is the identifier for this commit.
|
||||
pub fn write(&self, repo: &git2::Repository) -> Result<Oid, git2::Error> {
|
||||
let odb = repo.odb()?;
|
||||
odb.write(ObjectType::Commit, self.to_string().as_bytes())
|
||||
}
|
||||
|
||||
/// The tree [`Oid`] this commit points to.
|
||||
pub fn tree(&self) -> Oid {
|
||||
self.tree
|
||||
}
|
||||
|
||||
/// The parent [`Oid`]s of this commit.
|
||||
pub fn parents(&self) -> impl Iterator<Item = Oid> + '_ {
|
||||
self.parents.iter().copied()
|
||||
}
|
||||
|
||||
/// The author of this commit, i.e. the header corresponding to `author`.
|
||||
pub fn author(&self) -> &Author {
|
||||
&self.author
|
||||
}
|
||||
|
||||
/// The committer of this commit, i.e. the header corresponding to
|
||||
/// `committer`.
|
||||
pub fn committer(&self) -> &Author {
|
||||
&self.committer
|
||||
}
|
||||
|
||||
/// The message body of this commit.
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
|
||||
/// The [`Signature`]s found in this commit, i.e. the headers corresponding
|
||||
/// to `gpgsig`.
|
||||
pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
|
||||
self.headers.signatures()
|
||||
}
|
||||
|
||||
/// The [`Headers`] found in this commit.
|
||||
///
|
||||
/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
|
||||
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.headers.iter()
|
||||
}
|
||||
|
||||
/// Iterate over the [`Headers`] values that match the provided `name`.
|
||||
pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
|
||||
self.headers.values(name)
|
||||
}
|
||||
|
||||
/// Push a header to the end of the headers section.
|
||||
pub fn push_header(&mut self, name: &str, value: &str) {
|
||||
self.headers.push(name, value.trim());
|
||||
}
|
||||
|
||||
pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
|
||||
self.trailers.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
use std::str;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::author;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Read {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Parse(#[from] Parse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Parse {
|
||||
#[error(transparent)]
|
||||
Author(#[from] author::ParseError),
|
||||
#[error("invalid '{header}'")]
|
||||
InvalidHeader {
|
||||
header: &'static str,
|
||||
#[source]
|
||||
err: git2::Error,
|
||||
},
|
||||
#[error("invalid git commit object format")]
|
||||
InvalidFormat,
|
||||
#[error("missing '{0}' while parsing commit")]
|
||||
Missing(&'static str),
|
||||
#[error("error occurred while checking for git-trailers: {0}")]
|
||||
Trailers(#[source] git2::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] str::Utf8Error),
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<git2::Buf> for Commit {
|
||||
type Error = error::Parse;
|
||||
|
||||
fn try_from(value: git2::Buf) -> Result<Self, Self::Error> {
|
||||
value.as_str().ok_or(error::Parse::InvalidFormat)?.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Commit {
|
||||
type Error = error::Parse;
|
||||
|
||||
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
|
||||
Commit::from_str(str::from_utf8(data)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Commit {
|
||||
type Err = error::Parse;
|
||||
|
||||
fn from_str(buffer: &str) -> Result<Self, Self::Err> {
|
||||
let (header, message) = buffer
|
||||
.split_once("\n\n")
|
||||
.ok_or(error::Parse::InvalidFormat)?;
|
||||
let mut lines = header.lines();
|
||||
|
||||
let tree = match lines.next() {
|
||||
Some(tree) => tree
|
||||
.strip_prefix("tree ")
|
||||
.map(git2::Oid::from_str)
|
||||
.transpose()
|
||||
.map_err(|err| error::Parse::InvalidHeader {
|
||||
header: "tree",
|
||||
err,
|
||||
})?
|
||||
.ok_or(error::Parse::Missing("tree"))?,
|
||||
None => return Err(error::Parse::Missing("tree")),
|
||||
};
|
||||
|
||||
let mut parents = Vec::new();
|
||||
let mut author: Option<Author> = None;
|
||||
let mut committer: Option<Author> = None;
|
||||
let mut headers = Headers::new();
|
||||
|
||||
for line in lines {
|
||||
// Check if a signature is still being parsed
|
||||
if let Some(rest) = line.strip_prefix(' ') {
|
||||
let value: &mut String = headers
|
||||
.0
|
||||
.last_mut()
|
||||
.map(|(_, v)| v)
|
||||
.ok_or(error::Parse::InvalidFormat)?;
|
||||
value.push('\n');
|
||||
value.push_str(rest);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((name, value)) = line.split_once(' ') {
|
||||
match name {
|
||||
"parent" => parents.push(git2::Oid::from_str(value).map_err(|err| {
|
||||
error::Parse::InvalidHeader {
|
||||
header: "parent",
|
||||
err,
|
||||
}
|
||||
})?),
|
||||
"author" => author = Some(value.parse::<Author>()?),
|
||||
"committer" => committer = Some(value.parse::<Author>()?),
|
||||
_ => headers.push(name, value),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let trailers = Trailers::parse(message).map_err(error::Parse::Trailers)?;
|
||||
|
||||
let message = message
|
||||
.strip_suffix(&trailers.to_string(": "))
|
||||
.unwrap_or(message)
|
||||
.to_string();
|
||||
|
||||
let trailers = trailers.iter().map(OwnedTrailer::from).collect();
|
||||
|
||||
Ok(Self {
|
||||
tree,
|
||||
parents,
|
||||
author: author.ok_or(error::Parse::Missing("author"))?,
|
||||
committer: committer.ok_or(error::Parse::Missing("committer"))?,
|
||||
headers,
|
||||
message,
|
||||
trailers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Commit {
|
||||
fn to_string(&self) -> String {
|
||||
let mut buf = String::new();
|
||||
|
||||
writeln!(buf, "tree {}", self.tree).ok();
|
||||
|
||||
for parent in &self.parents {
|
||||
writeln!(buf, "parent {parent}").ok();
|
||||
}
|
||||
|
||||
writeln!(buf, "author {}", self.author).ok();
|
||||
writeln!(buf, "committer {}", self.committer).ok();
|
||||
|
||||
for (name, value) in self.headers.iter() {
|
||||
writeln!(buf, "{name} {}", value.replace('\n', "\n ")).ok();
|
||||
}
|
||||
writeln!(buf).ok();
|
||||
write!(buf, "{}", self.message.trim()).ok();
|
||||
writeln!(buf).ok();
|
||||
|
||||
if !self.trailers.is_empty() {
|
||||
writeln!(buf).ok();
|
||||
}
|
||||
for trailer in self.trailers.iter() {
|
||||
writeln!(buf, "{}", Trailer::from(trailer).display(": ")).ok();
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
const BEGIN_SSH: &str = "-----BEGIN SSH SIGNATURE-----\n";
|
||||
const BEGIN_PGP: &str = "-----BEGIN PGP SIGNATURE-----\n";
|
||||
|
||||
/// A collection of headers stored in a [`crate::commit::Commit`].
|
||||
///
|
||||
/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Headers(pub(super) Vec<(String, String)>);
|
||||
|
||||
/// A `gpgsig` signature stored in a [`crate::commit::Commit`].
|
||||
pub enum Signature<'a> {
|
||||
/// A PGP signature, i.e. starts with `-----BEGIN PGP SIGNATURE-----`.
|
||||
Pgp(Cow<'a, str>),
|
||||
/// A SSH signature, i.e. starts with `-----BEGIN SSH SIGNATURE-----`.
|
||||
Ssh(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl<'a> Signature<'a> {
|
||||
fn from_str(s: &'a str) -> Result<Self, UnknownScheme> {
|
||||
if s.starts_with(BEGIN_SSH) {
|
||||
Ok(Signature::Ssh(Cow::Borrowed(s)))
|
||||
} else if s.starts_with(BEGIN_PGP) {
|
||||
Ok(Signature::Pgp(Cow::Borrowed(s)))
|
||||
} else {
|
||||
Err(UnknownScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnknownScheme;
|
||||
|
||||
impl<'a> ToString for Signature<'a> {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Signature::Pgp(pgp) => pgp.to_string(),
|
||||
Signature::Ssh(ssh) => ssh.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
pub fn new() -> Self {
|
||||
Headers(Vec::new())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
}
|
||||
|
||||
pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
|
||||
self.iter()
|
||||
.filter_map(move |(k, v)| (k == name).then_some(v))
|
||||
}
|
||||
|
||||
pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
|
||||
self.0.iter().filter_map(|(k, v)| {
|
||||
if k == "gpgsig" {
|
||||
Signature::from_str(v).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Push a header to the end of the headers section.
|
||||
pub fn push(&mut self, name: &str, value: &str) {
|
||||
self.0.push((name.to_owned(), value.trim().to_owned()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
use std::{borrow::Cow, fmt, fmt::Write, ops::Deref, str::FromStr};
|
||||
|
||||
use git2::{MessageTrailersStrs, MessageTrailersStrsIterator};
|
||||
|
||||
/// A Git commit's set of trailers that are left in the commit's
|
||||
/// message.
|
||||
///
|
||||
/// Trailers are key/value pairs in the last paragraph of a message,
|
||||
/// not including any patches or conflicts that may be present.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// To construct `Trailers`, you can use [`Trailers::parse`] or its
|
||||
/// `FromStr` implementation.
|
||||
///
|
||||
/// To iterate over the trailers, you can use [`Trailers::iter`].
|
||||
///
|
||||
/// To render the trailers to a `String`, you can use
|
||||
/// [`Trailers::to_string`] or its `Display` implementation (note that
|
||||
/// it will default to using `": "` as the separator.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```text
|
||||
/// Add new functionality
|
||||
///
|
||||
/// Making code better with new functionality.
|
||||
///
|
||||
/// X-Signed-Off-By: Alex Sellier
|
||||
/// X-Co-Authored-By: Fintan Halpenny
|
||||
/// ```
|
||||
///
|
||||
/// The trailers in the above example are:
|
||||
///
|
||||
/// ```text
|
||||
/// X-Signed-Off-By: Alex Sellier
|
||||
/// X-Co-Authored-By: Fintan Halpenny
|
||||
/// ```
|
||||
pub struct Trailers {
|
||||
inner: MessageTrailersStrs,
|
||||
}
|
||||
|
||||
impl Trailers {
|
||||
pub fn parse(message: &str) -> Result<Self, git2::Error> {
|
||||
Ok(Self {
|
||||
inner: git2::message_trailers_strs(message)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter {
|
||||
inner: self.inner.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string<'a, S>(&self, sep: S) -> String
|
||||
where
|
||||
S: Separator<'a>,
|
||||
{
|
||||
let mut buf = String::new();
|
||||
for (i, trailer) in self.iter().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(buf).ok();
|
||||
}
|
||||
|
||||
write!(buf, "{}", trailer.display(sep.sep_for(&trailer.token))).ok();
|
||||
}
|
||||
writeln!(buf).ok();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Trailers {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.to_string(": "))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Separator<'a> {
|
||||
fn sep_for(&self, token: &Token) -> &'a str;
|
||||
}
|
||||
|
||||
impl<'a> Separator<'a> for &'a str {
|
||||
fn sep_for(&self, _: &Token) -> &'a str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> Separator<'a> for F
|
||||
where
|
||||
F: Fn(&Token) -> &'a str,
|
||||
{
|
||||
fn sep_for(&self, token: &Token) -> &'a str {
|
||||
self(token)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Trailers {
|
||||
type Err = git2::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a> {
|
||||
inner: MessageTrailersStrsIterator<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = Trailer<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let (token, value) = self.inner.next()?;
|
||||
Some(Trailer {
|
||||
token: Token(token),
|
||||
value: Cow::Borrowed(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Token<'a>(&'a str);
|
||||
|
||||
impl Deref for Token<'_> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display<'a> {
|
||||
trailer: &'a Trailer<'a>,
|
||||
separator: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Display<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{}",
|
||||
self.trailer.token.deref(),
|
||||
self.separator,
|
||||
self.trailer.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trailer is a key/value pair found in the last paragraph of a Git
|
||||
/// commit message, not including any patches or conflicts that may be
|
||||
/// present.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Trailer<'a> {
|
||||
pub token: Token<'a>,
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> Trailer<'a> {
|
||||
pub fn display(&'a self, separator: &'a str) -> Display<'a> {
|
||||
Display {
|
||||
trailer: self,
|
||||
separator,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_owned(&self) -> OwnedTrailer {
|
||||
OwnedTrailer::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A version of the [`Trailer`] which owns its token and
|
||||
/// value. Useful for when you need to carry trailers around in a long
|
||||
/// lived data structure.
|
||||
#[derive(Debug)]
|
||||
pub struct OwnedTrailer {
|
||||
pub token: OwnedToken,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OwnedToken(String);
|
||||
|
||||
impl Deref for OwnedToken {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&Trailer<'a>> for OwnedTrailer {
|
||||
fn from(t: &Trailer<'a>) -> Self {
|
||||
OwnedTrailer {
|
||||
token: OwnedToken(t.token.0.to_string()),
|
||||
value: t.value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Trailer<'a>> for OwnedTrailer {
|
||||
fn from(t: Trailer<'a>) -> Self {
|
||||
(&t).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedTrailer> for Trailer<'a> {
|
||||
fn from(t: &'a OwnedTrailer) -> Self {
|
||||
Trailer {
|
||||
token: Token(t.token.0.as_str()),
|
||||
value: Cow::from(&t.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
//! Extensions and wrappers for `git2` types
|
||||
|
||||
pub mod author;
|
||||
pub mod blob;
|
||||
pub mod commit;
|
||||
pub mod error;
|
||||
pub mod oid;
|
||||
pub mod reference;
|
||||
pub mod revwalk;
|
||||
pub mod transport;
|
||||
pub mod tree;
|
||||
@@ -16,7 +17,6 @@ pub mod tree;
|
||||
pub use blob::*;
|
||||
pub use error::*;
|
||||
pub use oid::*;
|
||||
pub use reference::*;
|
||||
pub use revwalk::*;
|
||||
pub use transport::*;
|
||||
pub use tree::Tree;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::convert::TryFrom as _;
|
||||
|
||||
mod iter;
|
||||
pub use iter::{ReferenceNames, References};
|
||||
|
||||
pub mod name;
|
||||
pub use name::{OneLevel, Qualified, RefLike, RefspecPattern};
|
||||
|
||||
pub mod check {
|
||||
pub use git_ref_format::{check_ref_format as ref_format, Error, Options};
|
||||
}
|
||||
|
||||
pub fn peeled(head: git2::Reference) -> Option<(String, git2::Oid)> {
|
||||
head.name()
|
||||
.and_then(|name| head.target().map(|target| (name.to_owned(), target)))
|
||||
}
|
||||
|
||||
pub fn refined((name, oid): (&str, git2::Oid)) -> Result<(OneLevel, crate::Oid), name::Error> {
|
||||
let name = RefLike::try_from(name)?;
|
||||
Ok((OneLevel::from(name), oid.into()))
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
/// Iterator chaining multiple [`git2::References`]
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
pub struct References<'a> {
|
||||
inner: Vec<git2::References<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> References<'a> {
|
||||
pub fn new(refs: impl IntoIterator<Item = git2::References<'a>>) -> Self {
|
||||
Self {
|
||||
inner: refs.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_globs(
|
||||
repo: &'a git2::Repository,
|
||||
globs: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
) -> Result<Self, git2::Error> {
|
||||
let globs = globs.into_iter();
|
||||
let mut iters = globs
|
||||
.size_hint()
|
||||
.1
|
||||
.map(Vec::with_capacity)
|
||||
.unwrap_or_else(Vec::new);
|
||||
for glob in globs {
|
||||
let iter = repo.references_glob(glob.as_ref())?;
|
||||
iters.push(iter);
|
||||
}
|
||||
|
||||
Ok(Self::new(iters))
|
||||
}
|
||||
|
||||
pub fn names<'b>(&'b mut self) -> ReferenceNames<'a, 'b> {
|
||||
ReferenceNames {
|
||||
inner: self.inner.iter_mut().map(|refs| refs.names()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peeled(self) -> impl Iterator<Item = (String, git2::Oid)> + 'a {
|
||||
self.filter_map(|reference| {
|
||||
reference.ok().and_then(|head| {
|
||||
head.name().and_then(|name| {
|
||||
head.target()
|
||||
.map(|target| (name.to_owned(), target.to_owned()))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for References<'a> {
|
||||
type Item = Result<git2::Reference<'a>, git2::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.pop().and_then(|mut iter| match iter.next() {
|
||||
None => self.next(),
|
||||
Some(item) => {
|
||||
self.inner.push(iter);
|
||||
Some(item)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator chaining multiple [`git2::ReferenceNames`]
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
pub struct ReferenceNames<'repo, 'references> {
|
||||
inner: Vec<git2::ReferenceNames<'repo, 'references>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Iterator for ReferenceNames<'a, 'b> {
|
||||
type Item = Result<&'b str, git2::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.pop().and_then(|mut iter| match iter.next() {
|
||||
None => self.next(),
|
||||
Some(item) => {
|
||||
self.inner.push(iter);
|
||||
Some(item)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,637 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
iter::FromIterator,
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
pub use percent_encoding::PercentEncode;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::check;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("invalid utf8")]
|
||||
Utf8,
|
||||
|
||||
#[error("not a valid git ref name or pattern")]
|
||||
RefFormat(#[from] check::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub const fn empty() -> Self {
|
||||
Self::RefFormat(check::Error::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum StripPrefixError {
|
||||
#[error("prefix is equal to path")]
|
||||
ImproperPrefix,
|
||||
|
||||
#[error("not prefixed by given path")]
|
||||
NotPrefix,
|
||||
}
|
||||
|
||||
/// An owned path-like value which is a valid git refname.
|
||||
///
|
||||
/// See [`git-check-ref-format`] for what the rules for refnames are --
|
||||
/// conversion functions behave as if `--allow-onelevel` was given.
|
||||
/// Additionally, we impose the rule that the name must consist of valid utf8.
|
||||
///
|
||||
/// Note that refspec patterns (eg. "refs/heads/*") are not allowed (see
|
||||
/// [`RefspecPattern`]), and that the maximum length of the name is 1024 bytes.
|
||||
///
|
||||
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "String")
|
||||
)]
|
||||
pub struct RefLike(String);
|
||||
|
||||
impl RefLike {
|
||||
/// Append the path in `Other` to `self.
|
||||
pub fn join<Other: Into<Self>>(&self, other: Other) -> Self {
|
||||
Self(format!("{}/{}", self.0, other.into().0))
|
||||
}
|
||||
|
||||
/// Append a [`RefspecPattern`], yielding a [`RefspecPattern`]
|
||||
pub fn with_pattern_suffix<Suf: Into<RefspecPattern>>(&self, suf: Suf) -> RefspecPattern {
|
||||
RefspecPattern(format!("{}/{}", self.0, suf.into().0))
|
||||
}
|
||||
|
||||
/// Returns a [`RefLike`] that, when joined onto `base`, yields `self`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `base` is not a prefix of `self`, or `base` equals the path in `self`
|
||||
/// (ie. the result would be the empty path, which is not a valid
|
||||
/// [`RefLike`]).
|
||||
pub fn strip_prefix<P: AsRef<str>>(&self, base: P) -> Result<Self, StripPrefixError> {
|
||||
let base = base.as_ref();
|
||||
let base = format!("{}/", base.strip_suffix('/').unwrap_or(base));
|
||||
self.0
|
||||
.strip_prefix(&base)
|
||||
.ok_or(StripPrefixError::NotPrefix)
|
||||
.and_then(|path| {
|
||||
if path.is_empty() {
|
||||
Err(StripPrefixError::ImproperPrefix)
|
||||
} else {
|
||||
Ok(Self(path.into()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub fn percent_encode(&self) -> PercentEncode {
|
||||
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||
const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
|
||||
&percent_encoding::CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'`');
|
||||
|
||||
/// https://url.spec.whatwg.org/#path-percent-encode-set
|
||||
const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
|
||||
.add(b'#')
|
||||
.add(b'?')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
|
||||
percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefLike {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefLike {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(
|
||||
check::Options {
|
||||
allow_onelevel: true,
|
||||
allow_pattern: false,
|
||||
},
|
||||
s,
|
||||
)?;
|
||||
Ok(Self(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
str::from_utf8(bytes)
|
||||
.or(Err(Error::Utf8))
|
||||
.and_then(Self::try_from)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RefLike {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Path> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
fn try_from(p: &Path) -> Result<Self, Self::Error> {
|
||||
use std::{convert::TryInto as _, path::Component::Normal};
|
||||
|
||||
p.components()
|
||||
.filter_map(|comp| match comp {
|
||||
Normal(s) => Some(s),
|
||||
_ => None,
|
||||
})
|
||||
.map(|os| os.to_str().ok_or(Error::Utf8))
|
||||
.collect::<Result<Vec<_>, Self::Error>>()?
|
||||
.join("/")
|
||||
.try_into()
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn try_from(p: &Path) -> Result<Self, Self::Error> {
|
||||
Self::try_from(p.to_str().ok_or(Error::Utf8)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefLike> for RefLike {
|
||||
fn from(me: &RefLike) -> Self {
|
||||
me.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git_ref_format::RefString> for RefLike {
|
||||
#[inline]
|
||||
fn from(r: git_ref_format::RefString) -> Self {
|
||||
Self(r.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git_ref_format::RefString> for RefLike {
|
||||
#[inline]
|
||||
fn from(r: &git_ref_format::RefString) -> Self {
|
||||
Self::from(r.as_refstr())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git_ref_format::RefStr> for RefLike {
|
||||
#[inline]
|
||||
fn from(r: &git_ref_format::RefStr) -> Self {
|
||||
Self(r.to_owned().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefLike> for String {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Self> for RefLike {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Self>,
|
||||
{
|
||||
Self(iter.into_iter().map(|x| x.0).collect::<Vec<_>>().join("/"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefLike {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`RefLike`] without a "refs/" prefix.
|
||||
///
|
||||
/// Conversion functions strip the first **two** path components iff the path
|
||||
/// starts with `refs/`.
|
||||
///
|
||||
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
|
||||
/// [`Qualified`] form is accepted, and silently converted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::convert::TryFrom;
|
||||
/// use radicle_git_ext::reference::name::*;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
|
||||
/// "next"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("refs/remotes/origin/it").unwrap()),
|
||||
/// "origin/it"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("mistress").unwrap()),
|
||||
/// "mistress"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/tags/grace").unwrap())),
|
||||
/// (
|
||||
/// OneLevel::from(RefLike::try_from("grace").unwrap()),
|
||||
/// Some(RefLike::try_from("tags").unwrap())
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/remotes/origin/hopper").unwrap())),
|
||||
/// (
|
||||
/// OneLevel::from(RefLike::try_from("origin/hopper").unwrap()),
|
||||
/// Some(RefLike::try_from("remotes").unwrap())
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/HEAD").unwrap())),
|
||||
/// (OneLevel::from(RefLike::try_from("HEAD").unwrap()), None)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("origin/hopper").unwrap()).into_qualified(
|
||||
/// RefLike::try_from("remotes").unwrap()
|
||||
/// ),
|
||||
/// "refs/remotes/origin/hopper",
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "RefLike")
|
||||
)]
|
||||
pub struct OneLevel(String);
|
||||
|
||||
impl OneLevel {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub fn from_qualified(Qualified(path): Qualified) -> (Self, Option<RefLike>) {
|
||||
let mut path = path.strip_prefix("refs/").unwrap_or(&path).split('/');
|
||||
match path.next() {
|
||||
Some(category) => {
|
||||
let category = RefLike(category.into());
|
||||
// check that the "category" is not the only component of the path
|
||||
match path.next() {
|
||||
Some(head) => (
|
||||
Self(
|
||||
std::iter::once(head)
|
||||
.chain(path)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"),
|
||||
),
|
||||
Some(category),
|
||||
),
|
||||
None => (Self::from(category), None),
|
||||
}
|
||||
},
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_qualified(self, category: RefLike) -> Qualified {
|
||||
Qualified(format!("refs/{category}/{self}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for OneLevel {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for OneLevel {
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefLike> for OneLevel {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
if path.starts_with("refs/") {
|
||||
Self(path.split('/').skip(2).collect::<Vec<_>>().join("/"))
|
||||
} else {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for OneLevel {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
Self::from(RefLike(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for RefLike {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for String {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for OneLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`RefLike`] **with** a "refs/" prefix.
|
||||
///
|
||||
/// Conversion functions will assume `refs/heads/` if the input was not
|
||||
/// qualified.
|
||||
///
|
||||
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
|
||||
/// [`OneLevel`] form is accepted, and silently converted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::convert::TryFrom;
|
||||
/// use radicle_git_ext::reference::name::*;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*Qualified::from(RefLike::try_from("laplace").unwrap()),
|
||||
/// "refs/heads/laplace"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*Qualified::from(RefLike::try_from("refs/heads/pu").unwrap()),
|
||||
/// "refs/heads/pu"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*Qualified::from(RefLike::try_from("refs/tags/v6.6.6").unwrap()),
|
||||
/// "refs/tags/v6.6.6"
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "RefLike")
|
||||
)]
|
||||
pub struct Qualified(String);
|
||||
|
||||
impl Qualified {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Qualified {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Qualified {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefLike> for Qualified {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
if path.starts_with("refs/") {
|
||||
Self(path)
|
||||
} else {
|
||||
Self(format!("refs/heads/{path}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for Qualified {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
Self::from(RefLike(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for RefLike {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for String {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Qualified {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned, path-like value which is a valid refspec pattern.
|
||||
///
|
||||
/// Conversion functions behave as if `--allow-onelevel --refspec-pattern` where
|
||||
/// given to [`git-check-ref-format`]. That is, most of the rules of [`RefLike`]
|
||||
/// apply, but the path _may_ contain exactly one `*` character.
|
||||
///
|
||||
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "String")
|
||||
)]
|
||||
pub struct RefspecPattern(String);
|
||||
|
||||
impl RefspecPattern {
|
||||
/// Append the `RefLike` to the `RefspecPattern`. This allows the creation
|
||||
/// of patterns where the `*` appears in the middle of the path, e.g.
|
||||
/// `refs/remotes/*/mfdoom`
|
||||
pub fn append(&self, refl: impl Into<RefLike>) -> Self {
|
||||
RefspecPattern(format!("{}/{}", self.0, refl.into()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefspecPattern> for RefspecPattern {
|
||||
fn from(pat: &RefspecPattern) -> Self {
|
||||
pat.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefspecPattern {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefspecPattern {
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RefspecPattern {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(
|
||||
check::Options {
|
||||
allow_onelevel: true,
|
||||
allow_pattern: true,
|
||||
},
|
||||
s,
|
||||
)?;
|
||||
Ok(Self(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RefspecPattern {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
str::from_utf8(bytes)
|
||||
.or(Err(Error::Utf8))
|
||||
.and_then(Self::try_from)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RefspecPattern {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RefspecPattern {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefspecPattern> for String {
|
||||
fn from(RefspecPattern(path): RefspecPattern) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefspecPattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// `RefLike`-likes can be coerced into `RefspecPattern`s
|
||||
|
||||
impl From<RefLike> for RefspecPattern {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefLike> for RefspecPattern {
|
||||
fn from(RefLike(path): &RefLike) -> Self {
|
||||
Self(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for RefspecPattern {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OneLevel> for RefspecPattern {
|
||||
fn from(OneLevel(path): &OneLevel) -> Self {
|
||||
Self(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for RefspecPattern {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Qualified> for RefspecPattern {
|
||||
fn from(Qualified(path): &Qualified) -> Self {
|
||||
Self(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git_ref_format::refspec::PatternString> for RefspecPattern {
|
||||
#[inline]
|
||||
fn from(r: git_ref_format::refspec::PatternString) -> Self {
|
||||
Self(r.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git_ref_format::refspec::PatternStr> for RefspecPattern {
|
||||
#[inline]
|
||||
fn from(r: &git_ref_format::refspec::PatternStr) -> Self {
|
||||
Self(r.to_owned().into())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "git-ext-test"
|
||||
name = "radicle-git-ext-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use radicle_git_ext::commit::Commit;
|
||||
|
||||
const NO_TRAILER: &str = "\
|
||||
tree 50d6ef440728217febf9e35716d8b0296608d7f8
|
||||
parent 0ad95dbdfe9fdf81938ca419cf740469173e2022
|
||||
parent a4ec9e07e1b2e6f37f7119651ae3bb63b79988b6
|
||||
author Fintan Halpenny <fintan.halpenny@gmail.com> 1669292989 +0000
|
||||
committer Fintan Halpenny <fintan.halpenny@gmail.com> 1669292989 +0000
|
||||
|
||||
Merge remote-tracking branch 'origin/surf/organise-tests'
|
||||
|
||||
* origin/surf/organise-tests:
|
||||
radicle-surf: organise tests
|
||||
";
|
||||
|
||||
const SINGLE_TRAILER: &str = "\
|
||||
tree 50d6ef440728217febf9e35716d8b0296608d7f8
|
||||
parent 0ad95dbdfe9fdf81938ca419cf740469173e2022
|
||||
parent a4ec9e07e1b2e6f37f7119651ae3bb63b79988b6
|
||||
author Fintan Halpenny <fintan.halpenny@gmail.com> 1669292989 +0000
|
||||
committer Fintan Halpenny <fintan.halpenny@gmail.com> 1669292989 +0000
|
||||
|
||||
Merge remote-tracking branch 'origin/surf/organise-tests'
|
||||
|
||||
* origin/surf/organise-tests:
|
||||
radicle-surf: organise tests
|
||||
|
||||
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
|
||||
";
|
||||
|
||||
const UNSIGNED: &str = "\
|
||||
tree c66cc435f83ed0fba90ed4500e9b4b96e9bd001b
|
||||
parent af06ad645133f580a87895353508053c5de60716
|
||||
author Alexis Sellier <alexis@radicle.xyz> 1664467633 +0200
|
||||
committer Alexis Sellier <alexis@radicle.xyz> 1664786099 -0200
|
||||
|
||||
Add SSH functionality with new `radicle-ssh`
|
||||
|
||||
We borrow code from `thrussh`, refactored to be runtime-less.
|
||||
|
||||
X-Signed-Off-By: Alex Sellier
|
||||
X-Co-Authored-By: Fintan Halpenny
|
||||
";
|
||||
|
||||
const SSH_SIGNATURE: &str = "\
|
||||
-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||
GPp0HqxaB911OnSAr6bwU=
|
||||
-----END SSH SIGNATURE-----";
|
||||
|
||||
const PGP_SIGNATURE: &str = "\
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
iQIzBAABCAAdFiEEHe7BWIo9taTY6TIiJVL7b2QGbLcFAmNcDhsACgkQJVL7b2QG
|
||||
bLcc9Q//RgKf5N4enta9AuszGJZvdFhMPfIDUdw+WAZA6Z8zDPb/aAXZrPP/KIOM
|
||||
zmX08FTqjP9B9YeWrEcFuAtxsRNqbDKrfpko9Y6bTsdrAJg3WIypBb9F8YDKJ6BO
|
||||
CORJJqWOsLW129jW+mJDhcE0YTvPlcMiMI2qjVXKhU6Ag11W8IRZyTb9tvEaDjBR
|
||||
YUnkPvgubv61K9BeUKexE2MakPBldaQtl0MF1Dk7/zo5btLd+KP0SOUKEhuMEu5b
|
||||
LATHHdiYjt/2Xz7q8EcrFxXUaipxZe89dfTdi2ooJQw3ZDqjDHsGTHpDeBuzuSaJ
|
||||
9fKVRwFz/78onfHPhmU4wfUhh+Fcl90p5/T+4dt2K6cr+7rq078e+aJYxkX2d0MG
|
||||
PG0xGP0RN4g+X92K1kGuzoe4870xAnRTNh5nUB+X9snO8tVqQZTb0M2yI+sTsKrv
|
||||
w/f+uiqL6e9DgIxlO5dgiNHCVoCs1QJ900jUGisrlzS4+n6GzMsG6s3c01X4yY9G
|
||||
Ou/kGkMsn7tqejqC9RufygcchCFZqYwaHQwPkiYhfYGMarMpoCFvll0h8tSparpS
|
||||
nnpAQXVdu8m3v1YdPUuTg5ksxSOe9HCIlVXGFhxy3iqCVRn+51FRnUI63rMTOm9/
|
||||
LBqzvji02lDUPGqPgXfcCS0ty8FM2flBIXnwb8TDzCaPYhf53+U=
|
||||
=6dw2
|
||||
-----END PGP SIGNATURE-----";
|
||||
|
||||
const SIGNED: &str = "\
|
||||
tree c66cc435f83ed0fba90ed4500e9b4b96e9bd001b
|
||||
parent af06ad645133f580a87895353508053c5de60716
|
||||
author Alexis Sellier <alexis@radicle.xyz> 1664467633 +0200
|
||||
committer Alexis Sellier <alexis@radicle.xyz> 1664786099 -0200
|
||||
other e6fe3c97619deb8ab4198620f9a7eb79d98363dd
|
||||
gpgsig -----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||
GPp0HqxaB911OnSAr6bwU=
|
||||
-----END SSH SIGNATURE-----
|
||||
gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
iQIzBAABCAAdFiEEHe7BWIo9taTY6TIiJVL7b2QGbLcFAmNcDhsACgkQJVL7b2QG
|
||||
bLcc9Q//RgKf5N4enta9AuszGJZvdFhMPfIDUdw+WAZA6Z8zDPb/aAXZrPP/KIOM
|
||||
zmX08FTqjP9B9YeWrEcFuAtxsRNqbDKrfpko9Y6bTsdrAJg3WIypBb9F8YDKJ6BO
|
||||
CORJJqWOsLW129jW+mJDhcE0YTvPlcMiMI2qjVXKhU6Ag11W8IRZyTb9tvEaDjBR
|
||||
YUnkPvgubv61K9BeUKexE2MakPBldaQtl0MF1Dk7/zo5btLd+KP0SOUKEhuMEu5b
|
||||
LATHHdiYjt/2Xz7q8EcrFxXUaipxZe89dfTdi2ooJQw3ZDqjDHsGTHpDeBuzuSaJ
|
||||
9fKVRwFz/78onfHPhmU4wfUhh+Fcl90p5/T+4dt2K6cr+7rq078e+aJYxkX2d0MG
|
||||
PG0xGP0RN4g+X92K1kGuzoe4870xAnRTNh5nUB+X9snO8tVqQZTb0M2yI+sTsKrv
|
||||
w/f+uiqL6e9DgIxlO5dgiNHCVoCs1QJ900jUGisrlzS4+n6GzMsG6s3c01X4yY9G
|
||||
Ou/kGkMsn7tqejqC9RufygcchCFZqYwaHQwPkiYhfYGMarMpoCFvll0h8tSparpS
|
||||
nnpAQXVdu8m3v1YdPUuTg5ksxSOe9HCIlVXGFhxy3iqCVRn+51FRnUI63rMTOm9/
|
||||
LBqzvji02lDUPGqPgXfcCS0ty8FM2flBIXnwb8TDzCaPYhf53+U=
|
||||
=6dw2
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
Add SSH functionality with new `radicle-ssh`
|
||||
|
||||
We borrow code from `thrussh`, refactored to be runtime-less.
|
||||
|
||||
X-Signed-Off-By: Alex Sellier
|
||||
X-Co-Authored-By: Fintan Halpenny
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn test_push_header() {
|
||||
let mut commit = Commit::from_str(UNSIGNED).unwrap();
|
||||
|
||||
commit.push_header("other", "e6fe3c97619deb8ab4198620f9a7eb79d98363dd");
|
||||
commit.push_header("gpgsig", SSH_SIGNATURE);
|
||||
commit.push_header("gpgsig", PGP_SIGNATURE);
|
||||
|
||||
assert_eq!(commit.to_string(), SIGNED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_header() {
|
||||
let commit = Commit::from_str(SIGNED).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
commit
|
||||
.signatures()
|
||||
.map(|sig| sig.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![SSH_SIGNATURE.to_owned(), PGP_SIGNATURE.to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
commit.values("other").collect::<Vec<_>>(),
|
||||
vec![String::from("e6fe3c97619deb8ab4198620f9a7eb79d98363dd")],
|
||||
);
|
||||
assert!(commit.values("unknown").next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion() {
|
||||
assert_eq!(
|
||||
Commit::from_str(NO_TRAILER).unwrap().to_string(),
|
||||
NO_TRAILER
|
||||
);
|
||||
assert_eq!(
|
||||
Commit::from_str(SINGLE_TRAILER).unwrap().to_string(),
|
||||
SINGLE_TRAILER
|
||||
);
|
||||
assert_eq!(Commit::from_str(SIGNED).unwrap().to_string(), SIGNED);
|
||||
assert_eq!(Commit::from_str(UNSIGNED).unwrap().to_string(), UNSIGNED);
|
||||
}
|
||||
|
||||
use std::io;
|
||||
use test_helpers::tempdir::WithTmpDir;
|
||||
|
||||
#[test]
|
||||
fn valid_commits() {
|
||||
let radicle_git = format!(
|
||||
"file://{}",
|
||||
git2::Repository::discover(".").unwrap().path().display()
|
||||
);
|
||||
let repo = WithTmpDir::new(|path| {
|
||||
let repo = git2::Repository::clone(&radicle_git, path)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
Ok::<_, io::Error>(repo)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut walk = repo.revwalk().unwrap();
|
||||
walk.push_head().unwrap();
|
||||
|
||||
// take the first 20 commits and make sure we can parse them
|
||||
for oid in walk.take(20) {
|
||||
let oid = oid.unwrap();
|
||||
let commit = Commit::read(&repo, oid);
|
||||
assert!(commit.is_ok(), "Oid: {oid}, Error: {commit:?}")
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
// 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 tests;
|
||||
mod commit;
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use radicle_git_ext::reference::{check, name::*};
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
mod common {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub fn invalid<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + Debug,
|
||||
{
|
||||
const INVALID: [&str; 16] = [
|
||||
".hidden",
|
||||
"/etc/shadow",
|
||||
"@",
|
||||
"@{",
|
||||
"C:",
|
||||
"\\WORKGROUP",
|
||||
"foo.lock",
|
||||
"head^",
|
||||
"here/../../etc/shadow",
|
||||
"refs//heads/main",
|
||||
"refs/heads/",
|
||||
"shawn/ white",
|
||||
"the/dotted./quad",
|
||||
"wh?t",
|
||||
"x[a-z]",
|
||||
"~ommij",
|
||||
];
|
||||
|
||||
for v in INVALID {
|
||||
assert_matches!(T::try_from(v), Err(Error::RefFormat(_)), "input: {}", v)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn valid<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + AsRef<str> + Debug,
|
||||
{
|
||||
const VALID: [&str; 5] = [
|
||||
"\u{1F32F}",
|
||||
"cl@wn",
|
||||
"foo/bar",
|
||||
"master",
|
||||
"refs/heads/mistress",
|
||||
];
|
||||
|
||||
for v in VALID {
|
||||
assert_matches!(T::try_from(v), Ok(x) if x.as_ref() == v, "input: {}", v)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + Debug,
|
||||
{
|
||||
assert_matches!(T::try_from(""), Err(Error::RefFormat(check::Error::Empty)))
|
||||
}
|
||||
|
||||
pub fn nulsafe<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + Debug,
|
||||
{
|
||||
assert_matches!(
|
||||
T::try_from("jeff\0"),
|
||||
Err(Error::RefFormat(check::Error::InvalidChar('\0')))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mod reflike {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
common::empty::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid() {
|
||||
common::valid::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
common::invalid::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nulsafe() {
|
||||
common::nulsafe::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globstar_invalid() {
|
||||
assert_matches!(
|
||||
RefLike::try_from("refs/heads/*"),
|
||||
Err(Error::RefFormat(check::Error::InvalidChar('*')))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_onelevel() {
|
||||
assert_eq!(
|
||||
&*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
|
||||
"next"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_heads() {
|
||||
assert_eq!(
|
||||
&*Qualified::from(RefLike::try_from("pu").unwrap()),
|
||||
"refs/heads/pu"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
let refl = RefLike::try_from("pu").unwrap();
|
||||
roundtrip::json(refl.clone());
|
||||
roundtrip::json(OneLevel::from(refl.clone()));
|
||||
roundtrip::json(Qualified::from(refl))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_invalid() {
|
||||
let json = serde_json::to_string("HEAD^").unwrap();
|
||||
assert!(serde_json::from_str::<RefLike>(&json).is_err());
|
||||
assert!(serde_json::from_str::<OneLevel>(&json).is_err());
|
||||
assert!(serde_json::from_str::<Qualified>(&json).is_err())
|
||||
}
|
||||
}
|
||||
|
||||
mod pattern {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
common::empty::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid() {
|
||||
common::valid::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
common::invalid::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nulsafe() {
|
||||
common::nulsafe::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globstar_ok() {
|
||||
const GLOBBED: [&str; 7] = [
|
||||
"*",
|
||||
"fo*",
|
||||
"fo*/bar",
|
||||
"foo/*/bar",
|
||||
"foo/ba*",
|
||||
"foo/bar/*",
|
||||
"foo/b*r",
|
||||
];
|
||||
|
||||
for v in GLOBBED {
|
||||
assert_matches!(
|
||||
RefspecPattern::try_from(v),
|
||||
Ok(ref x) if x.as_str() == v,
|
||||
"input: {}", v
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globstar_invalid() {
|
||||
const GLOBBED: [&str; 12] = [
|
||||
"**",
|
||||
"***",
|
||||
"*/*",
|
||||
"*/L/*",
|
||||
"fo*/*/bar",
|
||||
"fo*/ba*",
|
||||
"fo*/ba*/baz",
|
||||
"fo*/ba*/ba*",
|
||||
"fo*/bar/*",
|
||||
"foo/*/bar/*",
|
||||
"foo/*/bar/*/baz*",
|
||||
"foo/*/bar/*/baz/*",
|
||||
];
|
||||
|
||||
for v in GLOBBED {
|
||||
assert_matches!(
|
||||
RefspecPattern::try_from(v),
|
||||
Err(Error::RefFormat(check::Error::Pattern))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
roundtrip::json(RefspecPattern::try_from("refs/heads/*").unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_invalid() {
|
||||
let json = serde_json::to_string("HEAD^").unwrap();
|
||||
assert!(serde_json::from_str::<RefspecPattern>(&json).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_prefix_works_for_different_ends() {
|
||||
let refl = RefLike::try_from("refs/heads/next").unwrap();
|
||||
assert_eq!(
|
||||
refl.strip_prefix("refs/heads").unwrap(),
|
||||
refl.strip_prefix("refs/heads/").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "radicle-git-types"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>", "Fintan Halpenny <fintan.halpenny@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4"
|
||||
multibase = "0.9"
|
||||
percent-encoding = "2"
|
||||
thiserror = "1.0.30"
|
||||
tracing = "0.1"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.16.1"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dependencies.minicbor]
|
||||
version = "0.13"
|
||||
features = ["std", "derive"]
|
||||
|
||||
[dependencies.radicle-git-ext]
|
||||
path = "../radicle-git-ext"
|
||||
|
||||
[dependencies.radicle-macros]
|
||||
path = "../radicle-macros"
|
||||
|
||||
[dependencies.radicle-std-ext]
|
||||
path = "../radicle-std-ext"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
extern crate radicle_git_ext as git_ext;
|
||||
extern crate radicle_std_ext as std_ext;
|
||||
|
||||
#[macro_use]
|
||||
extern crate radicle_macros;
|
||||
|
||||
pub mod reference;
|
||||
pub mod refspec;
|
||||
pub mod remote;
|
||||
|
||||
mod sealed;
|
||||
|
||||
pub use reference::{
|
||||
AsRemote,
|
||||
Many,
|
||||
Multiple,
|
||||
One,
|
||||
Reference as GenericRef,
|
||||
RefsCategory,
|
||||
Single,
|
||||
SymbolicRef,
|
||||
};
|
||||
pub use refspec::{Fetchspec, Pushspec, Refspec};
|
||||
|
||||
pub trait AsNamespace: Into<git_ext::RefLike> {
|
||||
fn into_namespace(self) -> git_ext::RefLike {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to aid type inference constructing a [`Reference`] without a
|
||||
/// namespace.
|
||||
pub struct Flat;
|
||||
|
||||
impl<N> From<Flat> for Option<N>
|
||||
where
|
||||
N: AsNamespace,
|
||||
{
|
||||
fn from(_flat: Flat) -> Self {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Type specialised reference for the most common use within this crate.
|
||||
pub type Reference<N, C, R> = GenericRef<N, R, C>;
|
||||
|
||||
/// Whether we should force the overwriting of a reference or not.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Force {
|
||||
/// We should overwrite.
|
||||
True,
|
||||
/// We should not overwrite.
|
||||
False,
|
||||
}
|
||||
|
||||
impl Force {
|
||||
/// Convert the Force to its `bool` equivalent.
|
||||
fn as_bool(&self) -> bool {
|
||||
match self {
|
||||
Force::True => true,
|
||||
Force::False => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Force {
|
||||
fn from(b: bool) -> Self {
|
||||
if b {
|
||||
Self::True
|
||||
} else {
|
||||
Self::False
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,550 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use git_ext as ext;
|
||||
|
||||
use super::{AsNamespace, Force};
|
||||
|
||||
/// Type witness for a [`Reference`] that should point to a single reference.
|
||||
pub type One = ext::RefLike;
|
||||
|
||||
/// Alias for [`One`].
|
||||
pub type Single = One;
|
||||
|
||||
/// Type witness for a [`Reference`] that should point to multiple references.
|
||||
pub type Many = ext::RefspecPattern;
|
||||
|
||||
/// Alias for [`Many`].
|
||||
pub type Multiple = Many;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RefsCategory {
|
||||
Heads,
|
||||
Rad,
|
||||
Tags,
|
||||
Notes,
|
||||
/// Collaborative objects
|
||||
Cobs,
|
||||
Unknown(ext::RefLike),
|
||||
}
|
||||
|
||||
impl RefsCategory {
|
||||
/// The categories that are present in a default git repository
|
||||
pub const fn default_categories() -> [RefsCategory; 3] {
|
||||
[Self::Heads, Self::Tags, Self::Notes]
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RefsCategory {
|
||||
type Err = ext::reference::name::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"heads" => Self::Heads,
|
||||
"rad" => Self::Rad,
|
||||
"tags" => Self::Tags,
|
||||
"notes" => Self::Notes,
|
||||
"cobs" => Self::Cobs,
|
||||
other => {
|
||||
let reflike = ext::RefLike::try_from(other)?;
|
||||
Self::Unknown(reflike)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefsCategory {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Heads => f.write_str("heads"),
|
||||
Self::Rad => f.write_str("rad"),
|
||||
Self::Tags => f.write_str("tags"),
|
||||
Self::Notes => f.write_str("notes"),
|
||||
Self::Cobs => f.write_str("cobs"),
|
||||
Self::Unknown(cat) => f.write_str(cat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefsCategory> for ext::RefLike {
|
||||
fn from(cat: RefsCategory) -> Self {
|
||||
ext::RefLike::try_from(cat.to_string()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefsCategory> for ext::RefLike {
|
||||
fn from(cat: &RefsCategory) -> Self {
|
||||
ext::RefLike::try_from(cat.to_string()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ext::RefLike> for RefsCategory {
|
||||
fn from(r: ext::RefLike) -> Self {
|
||||
(&r).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ext::RefLike> for RefsCategory {
|
||||
fn from(r: &ext::RefLike) -> Self {
|
||||
match r.as_str() {
|
||||
"heads" => Self::Heads,
|
||||
"rad" => Self::Rad,
|
||||
"tags" => Self::Tags,
|
||||
"notes" => Self::Notes,
|
||||
"cobs" => Self::Cobs,
|
||||
_ => Self::Unknown(r.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ad-hoc trait to prevent the typechecker from recursing.
|
||||
///
|
||||
/// Morally, we can convert `Reference<N, R, C>` into `ext::RefLike` for any `R:
|
||||
/// Into<ext::RefLike>`. However, the typechecker may then attempt to unify `R`
|
||||
/// with `Reference<_, Reference<_, ...` recursively, leading to
|
||||
/// non-termination. Hence, we restrict the types which can be used as
|
||||
/// `Reference::remote` artificially.
|
||||
pub trait AsRemote: Into<ext::RefLike> {}
|
||||
|
||||
impl AsRemote for ext::RefLike {}
|
||||
impl AsRemote for &ext::RefLike {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Reference<Namespace, Remote, Cardinality> {
|
||||
/// The remote portion of this reference.
|
||||
pub remote: Option<Remote>,
|
||||
/// Where this reference falls under, i.e. `heads`, `tags`, `cob`, or`rad`.
|
||||
pub category: RefsCategory,
|
||||
/// The path of the reference, e.g. `feature/123`, `dev`, `heads/*`.
|
||||
pub name: Cardinality,
|
||||
/// The namespace of this reference.
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
// Polymorphic definitions
|
||||
impl<N, R, C> Reference<N, R, C>
|
||||
where
|
||||
N: Clone,
|
||||
R: Clone,
|
||||
C: Clone,
|
||||
{
|
||||
pub fn with_remote(self, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_remote(&mut self, remote: impl Into<Option<R>>) {
|
||||
self.remote = remote.into();
|
||||
}
|
||||
|
||||
pub fn remote(&mut self, remote: impl Into<Option<R>>) -> &mut Self {
|
||||
self.set_remote(remote);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the namespace of this reference to another one. Note that the
|
||||
/// namespace does not have to be of the original namespace's type.
|
||||
pub fn with_namespace<NN, Other>(self, namespace: NN) -> Reference<Other, R, C>
|
||||
where
|
||||
NN: Into<Option<Other>>,
|
||||
Other: AsNamespace,
|
||||
{
|
||||
Reference {
|
||||
name: self.name,
|
||||
remote: self.remote,
|
||||
category: self.category,
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the named portion of this path.
|
||||
pub fn with_name<S: Into<C>>(self, name: S) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the named portion of this path.
|
||||
pub fn set_name<S: Into<C>>(&mut self, name: S) {
|
||||
self.name = name.into();
|
||||
}
|
||||
|
||||
pub fn name<S: Into<C>>(&mut self, name: S) -> &mut Self {
|
||||
self.set_name(name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// References with a `One` cardinality
|
||||
impl<N, R> Reference<N, R, One> {
|
||||
/// Find this particular reference.
|
||||
pub fn find<'a>(&self, repo: &'a git2::Repository) -> Result<git2::Reference<'a>, git2::Error>
|
||||
where
|
||||
Self: ToString,
|
||||
{
|
||||
repo.find_reference(&self.to_string())
|
||||
}
|
||||
|
||||
/// Resolve the [`git2::Oid`] the reference points to (if it exists).
|
||||
///
|
||||
/// Avoids allocating a [`git2::Reference`].
|
||||
pub fn oid(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error>
|
||||
where
|
||||
Self: ToString,
|
||||
{
|
||||
repo.refname_to_id(&self.to_string())
|
||||
}
|
||||
|
||||
pub fn create<'a>(
|
||||
&self,
|
||||
repo: &'a git2::Repository,
|
||||
target: git2::Oid,
|
||||
force: super::Force,
|
||||
log_message: &str,
|
||||
) -> Result<git2::Reference<'a>, git2::Error>
|
||||
where
|
||||
Self: ToString,
|
||||
{
|
||||
tracing::debug!(
|
||||
"creating direct reference {} -> {} (force: {}, reflog: '{}')",
|
||||
self.to_string(),
|
||||
target,
|
||||
force.as_bool(),
|
||||
log_message
|
||||
);
|
||||
let name = self.to_string();
|
||||
repo.reference_ensure_log(&name)?;
|
||||
repo.reference(&name, target, force.as_bool(), log_message)
|
||||
}
|
||||
|
||||
/// Create a [`SymbolicRef`] from `source` to `self` as the `target`.
|
||||
pub fn symbolic_ref<SN, SR>(
|
||||
self,
|
||||
source: Reference<SN, SR, Single>,
|
||||
force: Force,
|
||||
) -> SymbolicRef<Reference<SN, SR, Single>, Self>
|
||||
where
|
||||
R: Clone,
|
||||
N: Clone,
|
||||
{
|
||||
SymbolicRef {
|
||||
source,
|
||||
target: self,
|
||||
force,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs/namespaces/<namespace>/refs/rad/id`
|
||||
pub fn rad_id(namespace: impl Into<Option<N>>) -> Self {
|
||||
Self {
|
||||
remote: None,
|
||||
category: RefsCategory::Rad,
|
||||
name: reflike!("id"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs/namespaces/<namespace>/refs/rad/signed_refs`
|
||||
/// * `refs/namespaces/<namespace>/refs/remote/<peer_id>/rad/
|
||||
/// signed_refs`
|
||||
pub fn rad_signed_refs(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Rad,
|
||||
name: reflike!("signed_refs"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs/namespaces/<namespace>/refs/rad/self`
|
||||
/// * `refs/namespaces/<namespace>/refs/remote/<peer_id>/rad/self`
|
||||
pub fn rad_self(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Rad,
|
||||
name: reflike!("self"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs/namespaces/<namespace>/refs/heads/<name>`
|
||||
/// * `refs/namespaces/<namespace>/refs/remote/<peer_id>/heads/<name>`
|
||||
pub fn head(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>, name: One) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Heads,
|
||||
name,
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs/namespaces/<namespace>/refs/tags/<name>`
|
||||
/// * `refs/namespaces/<namespace>/refs/remote/<peer_id>/tags/<name>`
|
||||
pub fn tag(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>, name: One) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Tags,
|
||||
name,
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, R> Display for Reference<N, R, One>
|
||||
where
|
||||
for<'a> &'a N: AsNamespace,
|
||||
for<'a> &'a R: AsRemote,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(Into::<ext::RefLike>::into(self).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, R> From<Reference<N, R, One>> for ext::RefLike
|
||||
where
|
||||
for<'a> &'a N: AsNamespace,
|
||||
for<'a> &'a R: AsRemote,
|
||||
{
|
||||
fn from(r: Reference<N, R, One>) -> Self {
|
||||
Self::from(&r)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, N, R> From<&'a Reference<N, R, One>> for ext::RefLike
|
||||
where
|
||||
&'a N: AsNamespace,
|
||||
&'a R: AsRemote,
|
||||
{
|
||||
fn from(r: &'a Reference<N, R, One>) -> Self {
|
||||
let mut refl = reflike!("refs");
|
||||
|
||||
if let Some(ref namespace) = r.namespace {
|
||||
refl = refl
|
||||
.join(reflike!("namespaces"))
|
||||
.join(namespace)
|
||||
.join(reflike!("refs"));
|
||||
}
|
||||
if let Some(ref remote) = r.remote {
|
||||
refl = refl.join(reflike!("remotes")).join(remote);
|
||||
}
|
||||
|
||||
refl.join(&r.category)
|
||||
.join(ext::OneLevel::from(r.name.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, R> From<Reference<N, R, One>> for ext::RefspecPattern
|
||||
where
|
||||
for<'a> &'a N: AsNamespace,
|
||||
for<'a> &'a R: AsRemote,
|
||||
{
|
||||
fn from(r: Reference<N, R, One>) -> Self {
|
||||
Self::from(&r)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, N, R> From<&'a Reference<N, R, One>> for ext::RefspecPattern
|
||||
where
|
||||
&'a N: AsNamespace,
|
||||
&'a R: AsRemote,
|
||||
{
|
||||
fn from(r: &'a Reference<N, R, One>) -> Self {
|
||||
Into::<ext::RefLike>::into(r).into()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(kim): what is this for?
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl<'a, N, R> Into<ext::blob::Branch<'a>> for &'a Reference<N, R, Single>
|
||||
where
|
||||
Self: ToString,
|
||||
{
|
||||
fn into(self) -> ext::blob::Branch<'a> {
|
||||
ext::blob::Branch::from(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// References with a `Many` cardinality
|
||||
impl<N, R> Reference<N, R, Many> {
|
||||
/// Get the iterator for these references.
|
||||
pub fn references<'a>(
|
||||
&self,
|
||||
repo: &'a git2::Repository,
|
||||
) -> Result<ext::References<'a>, git2::Error>
|
||||
where
|
||||
Self: ToString,
|
||||
{
|
||||
ext::References::from_globs(repo, [self.to_string()])
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs[/namespaces/<namespace>/refs]/rad/ids/*`
|
||||
pub fn rad_ids_glob(namespace: impl Into<Option<N>>) -> Self {
|
||||
Self {
|
||||
remote: None,
|
||||
category: RefsCategory::Rad,
|
||||
name: refspec_pattern!("ids/*"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs[/namespaces/<namespace>/refs][/remotes/<remote>]/heads/*`
|
||||
pub fn heads(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Heads,
|
||||
name: refspec_pattern!("*"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs[/namespaces/<namespace>]/refs[/remotes/<remote>]/rad/*`
|
||||
pub fn rads(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Rad,
|
||||
name: refspec_pattern!("*"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs[/namespaces/<namespace>]/refs[/remotes/<remote>]/tags/*`
|
||||
pub fn tags(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Tags,
|
||||
name: refspec_pattern!("*"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to:
|
||||
/// * `refs[/namespaces/<namespace>]/refs[/remotes/<remote>]/notes/*`
|
||||
pub fn notes(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Notes,
|
||||
name: refspec_pattern!("*"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a reference that points to
|
||||
/// * `refs[/namespaces/namespace]/refs[/remotes/<remote>]/cobs/*`
|
||||
pub fn cob(namespace: impl Into<Option<N>>, remote: impl Into<Option<R>>) -> Self {
|
||||
Self {
|
||||
remote: remote.into(),
|
||||
category: RefsCategory::Cobs,
|
||||
name: refspec_pattern!("*"),
|
||||
namespace: namespace.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, R> Display for Reference<N, R, Many>
|
||||
where
|
||||
for<'a> &'a N: AsNamespace,
|
||||
for<'a> &'a R: AsRemote,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(Into::<ext::RefspecPattern>::into(self).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, R> From<Reference<N, R, Many>> for ext::RefspecPattern
|
||||
where
|
||||
for<'a> &'a N: AsNamespace,
|
||||
for<'a> &'a R: AsRemote,
|
||||
{
|
||||
fn from(r: Reference<N, R, Many>) -> Self {
|
||||
Self::from(&r)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, N, R> From<&'a Reference<N, R, Many>> for ext::RefspecPattern
|
||||
where
|
||||
&'a N: AsNamespace,
|
||||
&'a R: AsRemote,
|
||||
{
|
||||
fn from(r: &'a Reference<N, R, Many>) -> Self {
|
||||
let mut refl = reflike!("refs");
|
||||
|
||||
if let Some(ref namespace) = r.namespace {
|
||||
refl = refl
|
||||
.join(reflike!("namespaces"))
|
||||
.join(namespace)
|
||||
.join(reflike!("refs"));
|
||||
}
|
||||
if let Some(ref remote) = r.remote {
|
||||
refl = refl.join(reflike!("remotes")).join(remote);
|
||||
}
|
||||
|
||||
refl.join(&r.category)
|
||||
.with_pattern_suffix(r.name.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// The data for creating a symbolic reference in a git repository.
|
||||
pub struct SymbolicRef<S, T> {
|
||||
/// The new symbolic reference.
|
||||
pub source: S,
|
||||
/// The reference that already exists and we want to create symbolic
|
||||
/// reference of.
|
||||
pub target: T,
|
||||
/// Whether we should overwrite any pre-existing `source`.
|
||||
pub force: Force,
|
||||
}
|
||||
|
||||
impl<S, T> SymbolicRef<S, T> {
|
||||
/// Create a symbolic reference of `target`, where the `source` is the newly
|
||||
/// created reference.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * If the `target` does not exist we won't create the symbolic
|
||||
/// reference and we error early.
|
||||
/// * If we could not create the new symbolic reference since the name
|
||||
/// already exists. Note that this will not be the case if `Force::True`
|
||||
/// is passed.
|
||||
pub fn create<'a>(&self, repo: &'a git2::Repository) -> Result<git2::Reference<'a>, git2::Error>
|
||||
where
|
||||
for<'b> &'b S: Into<ext::RefLike>,
|
||||
for<'b> &'b T: Into<ext::RefLike>,
|
||||
{
|
||||
let source = Into::<ext::RefLike>::into(&self.source);
|
||||
let target = Into::<ext::RefLike>::into(&self.target);
|
||||
|
||||
let reflog_msg = &format!("creating symbolic ref {source} -> {target}");
|
||||
tracing::debug!("{}", reflog_msg);
|
||||
|
||||
let _ = repo.refname_to_id(target.as_str())?;
|
||||
repo.reference_ensure_log(source.as_str())?;
|
||||
repo.reference_symbolic(
|
||||
source.as_str(),
|
||||
target.as_str(),
|
||||
self.force.as_bool(),
|
||||
reflog_msg,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use git_ext as ext;
|
||||
|
||||
use super::Force;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Refspec<S, D> {
|
||||
/// The source spec (LHS of the `:`).
|
||||
///
|
||||
/// When used as a fetch spec, it refers to the remote side, while as a push
|
||||
/// spec it refers to the local side.
|
||||
pub src: S,
|
||||
|
||||
/// The destination spec (RHS of the `:`).
|
||||
///
|
||||
/// When used as a fetch spec, it refers to the local side, while as a push
|
||||
/// spec it refers to the remote side.
|
||||
pub dst: D,
|
||||
|
||||
/// Whether to allow history rewrites.
|
||||
pub force: Force,
|
||||
}
|
||||
|
||||
impl<S, D> Refspec<S, D> {
|
||||
pub fn into_fetchspec(self) -> Fetchspec
|
||||
where
|
||||
S: Into<ext::RefspecPattern>,
|
||||
D: Into<ext::RefspecPattern>,
|
||||
{
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub fn into_pushspec(self) -> Pushspec
|
||||
where
|
||||
S: Into<ext::RefLike>,
|
||||
D: Into<ext::RefLike>,
|
||||
{
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, D> Display for Refspec<S, D>
|
||||
where
|
||||
for<'a> &'a S: Into<ext::RefspecPattern>,
|
||||
for<'a> &'a D: Into<ext::RefspecPattern>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.force.as_bool() {
|
||||
f.write_str("+")?;
|
||||
}
|
||||
|
||||
let src = Into::<ext::RefspecPattern>::into(&self.src);
|
||||
let dst = Into::<ext::RefspecPattern>::into(&self.dst);
|
||||
|
||||
write!(f, "{src}:{dst}")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Refspec<ext::RefspecPattern, ext::RefspecPattern> {
|
||||
type Error = ext::reference::name::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
let force = s.starts_with('+').into();
|
||||
let specs = s.trim_start_matches('+');
|
||||
let mut iter = specs.split(':');
|
||||
let src = iter
|
||||
.next()
|
||||
.ok_or_else(ext::reference::name::Error::empty)
|
||||
.and_then(ext::RefspecPattern::try_from)?;
|
||||
let dst = iter
|
||||
.next()
|
||||
.ok_or_else(ext::reference::name::Error::empty)
|
||||
.and_then(ext::RefspecPattern::try_from)?;
|
||||
|
||||
Ok(Self { src, dst, force })
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Refspec<ext::RefspecPattern, ext::RefspecPattern> {
|
||||
type Err = ext::reference::name::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Refspec<ext::RefLike, ext::RefLike> {
|
||||
type Error = ext::reference::name::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
let force = s.starts_with('+').into();
|
||||
let specs = s.trim_start_matches('+');
|
||||
let mut iter = specs.split(':');
|
||||
let src = iter
|
||||
.next()
|
||||
.ok_or_else(ext::reference::name::Error::empty)
|
||||
.and_then(ext::RefLike::try_from)?;
|
||||
let dst = iter
|
||||
.next()
|
||||
.ok_or_else(ext::reference::name::Error::empty)
|
||||
.and_then(ext::RefLike::try_from)?;
|
||||
|
||||
Ok(Self { src, dst, force })
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Refspec<ext::RefLike, ext::RefLike> {
|
||||
type Err = ext::reference::name::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Fetchspec(Refspec<ext::RefspecPattern, ext::RefspecPattern>);
|
||||
|
||||
impl<S, D> From<Refspec<S, D>> for Fetchspec
|
||||
where
|
||||
S: Into<ext::RefspecPattern>,
|
||||
D: Into<ext::RefspecPattern>,
|
||||
{
|
||||
fn from(spec: Refspec<S, D>) -> Self {
|
||||
Self(Refspec {
|
||||
src: spec.src.into(),
|
||||
dst: spec.dst.into(),
|
||||
force: spec.force,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Fetchspec {
|
||||
type Error = ext::reference::name::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
Refspec::try_from(s).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Fetchspec {
|
||||
type Err = ext::reference::name::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Fetchspec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Pushspec(Refspec<ext::RefLike, ext::RefLike>);
|
||||
|
||||
impl<S, D> From<Refspec<S, D>> for Pushspec
|
||||
where
|
||||
S: Into<ext::RefLike>,
|
||||
D: Into<ext::RefLike>,
|
||||
{
|
||||
fn from(spec: Refspec<S, D>) -> Self {
|
||||
Self(Refspec {
|
||||
src: spec.src.into(),
|
||||
dst: spec.dst.into(),
|
||||
force: spec.force,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Pushspec {
|
||||
type Error = ext::reference::name::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
Refspec::try_from(s).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Pushspec {
|
||||
type Err = ext::reference::name::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Pushspec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{convert::TryFrom, str::FromStr};
|
||||
|
||||
use git_ext::{
|
||||
error::{is_exists_err, is_not_found_err},
|
||||
reference::{self, RefLike},
|
||||
};
|
||||
use std_ext::result::ResultExt as _;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{Fetchspec, Pushspec};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FindError {
|
||||
#[error("missing {0}")]
|
||||
Missing(&'static str),
|
||||
|
||||
#[error("failed to parse URL")]
|
||||
ParseUrl(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("failed to parse refspec")]
|
||||
Refspec(#[from] reference::name::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Remote<Url> {
|
||||
/// The file path to the git monorepo.
|
||||
pub url: Url,
|
||||
/// Name of the remote, e.g. `"rad"`, `"origin"`.
|
||||
pub name: RefLike,
|
||||
/// The set of fetch specs to add upon creation.
|
||||
///
|
||||
/// **Note**: empty fetch specs do not denote the default fetch spec
|
||||
/// (`refs/heads/*:refs/remote/<name>/*`), but ... empty fetch specs.
|
||||
pub fetchspecs: Vec<Fetchspec>,
|
||||
/// The set of push specs to add upon creation.
|
||||
pub pushspecs: Vec<Pushspec>,
|
||||
}
|
||||
|
||||
impl<Url> Remote<Url> {
|
||||
/// Create a `"rad"` remote with a single fetch spec.
|
||||
pub fn rad_remote<Ref, Spec>(url: Url, fetch_spec: Ref) -> Self
|
||||
where
|
||||
Ref: Into<Option<Spec>>,
|
||||
Spec: Into<Fetchspec>,
|
||||
{
|
||||
Self {
|
||||
url,
|
||||
name: reflike!("rad"),
|
||||
fetchspecs: fetch_spec.into().into_iter().map(Into::into).collect(),
|
||||
pushspecs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `Remote` with the given `url` and `name`, while making the
|
||||
/// `fetch_spec` and `pushspecs` empty.
|
||||
pub fn new<R>(url: Url, name: R) -> Self
|
||||
where
|
||||
R: Into<RefLike>,
|
||||
{
|
||||
Self {
|
||||
url,
|
||||
name: name.into(),
|
||||
fetchspecs: vec![],
|
||||
pushspecs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the fetch specs.
|
||||
pub fn with_fetchspecs<I>(self, specs: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
<I as IntoIterator>::Item: Into<Fetchspec>,
|
||||
{
|
||||
Self {
|
||||
fetchspecs: specs.into_iter().map(Into::into).collect(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a fetch spec.
|
||||
pub fn add_fetchspec(&mut self, spec: impl Into<Fetchspec>) {
|
||||
self.fetchspecs.push(spec.into())
|
||||
}
|
||||
|
||||
/// Override the push specs.
|
||||
pub fn with_pushspecs<I>(self, specs: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
<I as IntoIterator>::Item: Into<Pushspec>,
|
||||
{
|
||||
Self {
|
||||
pushspecs: specs.into_iter().map(Into::into).collect(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a push spec.
|
||||
pub fn add_pushspec(&mut self, spec: impl Into<Pushspec>) {
|
||||
self.pushspecs.push(spec.into())
|
||||
}
|
||||
|
||||
/// Persist the remote in the `repo`'s config.
|
||||
///
|
||||
/// If a remote with the same name already exists, previous values of the
|
||||
/// configuration keys `url`, `fetch`, and `push` will be overwritten.
|
||||
/// Note that this means that _other_ configuration keys are left
|
||||
/// untouched, if present.
|
||||
#[allow(clippy::unit_arg)]
|
||||
#[tracing::instrument(skip(self, repo), fields(name = self.name.as_str()))]
|
||||
pub fn save(&mut self, repo: &git2::Repository) -> Result<(), git2::Error>
|
||||
where
|
||||
Url: ToString,
|
||||
{
|
||||
let url = self.url.to_string();
|
||||
repo.remote(self.name.as_str(), &url)
|
||||
.and(Ok(()))
|
||||
.or_matches::<git2::Error, _, _>(is_exists_err, || Ok(()))?;
|
||||
|
||||
{
|
||||
let mut config = repo.config()?;
|
||||
config
|
||||
.remove_multivar(&format!("remote.{}.url", self.name), ".*")
|
||||
.or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(()))?;
|
||||
config
|
||||
.remove_multivar(&format!("remote.{}.fetch", self.name), ".*")
|
||||
.or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(()))?;
|
||||
config
|
||||
.remove_multivar(&format!("remote.{}.push", self.name), ".*")
|
||||
.or_matches::<git2::Error, _, _>(is_not_found_err, || Ok(()))?;
|
||||
}
|
||||
|
||||
repo.remote_set_url(self.name.as_str(), &url)?;
|
||||
|
||||
for spec in self.fetchspecs.iter() {
|
||||
repo.remote_add_fetch(self.name.as_str(), &spec.to_string())?;
|
||||
}
|
||||
for spec in self.pushspecs.iter() {
|
||||
repo.remote_add_push(self.name.as_str(), &spec.to_string())?;
|
||||
}
|
||||
|
||||
debug_assert!(repo.find_remote(self.name.as_str()).is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find a persisted remote by name.
|
||||
#[allow(clippy::unit_arg)]
|
||||
#[tracing::instrument(skip(repo))]
|
||||
pub fn find(repo: &git2::Repository, name: RefLike) -> Result<Option<Self>, FindError>
|
||||
where
|
||||
Url: FromStr,
|
||||
<Url as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let git_remote = repo
|
||||
.find_remote(name.as_str())
|
||||
.map(Some)
|
||||
.or_matches::<FindError, _, _>(is_not_found_err, || Ok(None))?;
|
||||
|
||||
match git_remote {
|
||||
None => Ok(None),
|
||||
Some(remote) => {
|
||||
let url = remote
|
||||
.url()
|
||||
.ok_or(FindError::Missing("url"))?
|
||||
.parse()
|
||||
.map_err(|e| FindError::ParseUrl(Box::new(e)))?;
|
||||
let fetchspecs = remote
|
||||
.fetch_refspecs()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Fetchspec::try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
let pushspecs = remote
|
||||
.push_refspecs()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Pushspec::try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(Some(Self {
|
||||
url,
|
||||
name,
|
||||
fetchspecs,
|
||||
pushspecs,
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Url> AsRef<Url> for Remote<Url> {
|
||||
fn as_ref(&self) -> &Url {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for git_ext::Oid {}
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "radicle-macros"
|
||||
version = "0.1.0"
|
||||
authors = ["The Radicle Team <dev@radicle.xyz>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Radicle procedural macros"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
proc-macro = true
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1.0.4"
|
||||
quote = "1"
|
||||
syn = "1"
|
||||
|
||||
[dependencies.radicle-git-ext]
|
||||
path = "../radicle-git-ext"
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, LitStr};
|
||||
|
||||
use radicle_git_ext::reference::name::{RefLike, RefspecPattern};
|
||||
|
||||
/// Create `RefLike` from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
///
|
||||
/// ```rust
|
||||
/// use radicle_macros::reflike;
|
||||
///
|
||||
/// assert_eq!("lolek/bolek", reflike!("lolek/bolek").as_str())
|
||||
/// ```
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn reflike(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
|
||||
match RefLike::try_from(lit.value()) {
|
||||
Ok(safe) => {
|
||||
let safe: &str = &safe;
|
||||
let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefLike>(#safe.to_owned()) }};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid RefLike literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `RefspecPattern` from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
///
|
||||
/// ```rust
|
||||
/// use radicle_macros::refspec_pattern;
|
||||
///
|
||||
/// assert_eq!("refs/heads/*", refspec_pattern!("refs/heads/*").as_str())
|
||||
/// ```
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn refspec_pattern(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
|
||||
match RefspecPattern::try_from(lit.value()) {
|
||||
Ok(safe) => {
|
||||
let safe: &str = &safe;
|
||||
let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefspecPattern>(#safe.to_owned()) }};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid RefspecPattern literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ doc = false
|
||||
path = "../git-commit/t"
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.git-ext-test]
|
||||
[dev-dependencies.radicle-git-ext-test]
|
||||
path = "../radicle-git-ext/t"
|
||||
features = ["test"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user