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:
Fintan Halpenny
2023-02-21 10:08:06 +00:00
24 changed files with 890 additions and 2169 deletions
-2
View File
@@ -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
-7
View File
@@ -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())
+1 -1
View File
@@ -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"]
+129
View File
@@ -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 })
}
}
+294
View File
@@ -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
}
}
+71
View File
@@ -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()));
}
}
+214
View File
@@ -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),
}
}
}
+2 -2
View File
@@ -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;
-26
View File
@@ -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()))
}
-87
View File
@@ -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)
},
})
}
}
-637
View File
@@ -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 -1
View File
@@ -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"
+176
View File
@@ -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 -5
View File
@@ -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;
-228
View File
@@ -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()
);
}
}
-39
View File
@@ -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"]
-79
View File
@@ -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
}
}
}
-550
View File
@@ -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,
)
}
}
-201
View File
@@ -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)
}
}
-203
View File
@@ -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
}
}
-8
View File
@@ -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 {}
-20
View File
@@ -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"
-72
View File
@@ -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
View File
@@ -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"]