#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RefspecParseError {
Empty,
EmptySource,
EmptyDestination,
TooManySeparators,
UnknownDirection,
UnknownMode,
}
impl fmt::Display for RefspecParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Git refspec cannot be empty"),
Self::EmptySource => formatter.write_str("Git refspec source cannot be empty"),
Self::EmptyDestination => {
formatter.write_str("Git refspec destination cannot be empty")
},
Self::TooManySeparators => {
formatter.write_str("Git refspec contains too many separators")
},
Self::UnknownDirection => formatter.write_str("unknown Git refspec direction"),
Self::UnknownMode => formatter.write_str("unknown Git refspec mode"),
}
}
}
impl Error for RefspecParseError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RefspecDirection {
Fetch,
Push,
}
impl RefspecDirection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Fetch => "fetch",
Self::Push => "push",
}
}
}
impl fmt::Display for RefspecDirection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for RefspecDirection {
type Err = RefspecParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"fetch" => Ok(Self::Fetch),
"push" => Ok(Self::Push),
"" => Err(RefspecParseError::Empty),
_ => Err(RefspecParseError::UnknownDirection),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RefspecMode {
Normal,
Force,
}
impl RefspecMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Force => "force",
}
}
}
impl fmt::Display for RefspecMode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for RefspecMode {
type Err = RefspecParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"normal" => Ok(Self::Normal),
"force" | "+" => Ok(Self::Force),
"" => Err(RefspecParseError::Empty),
_ => Err(RefspecParseError::UnknownMode),
}
}
}
fn non_empty(value: &str, error: RefspecParseError) -> Result<String, RefspecParseError> {
let trimmed = value.trim();
if trimmed.is_empty() {
Err(error)
} else {
Ok(trimmed.to_string())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RefspecSource(String);
impl RefspecSource {
pub fn new(value: impl AsRef<str>) -> Result<Self, RefspecParseError> {
non_empty(value.as_ref(), RefspecParseError::EmptySource).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for RefspecSource {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RefspecSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RefspecDestination(String);
impl RefspecDestination {
pub fn new(value: impl AsRef<str>) -> Result<Self, RefspecParseError> {
non_empty(value.as_ref(), RefspecParseError::EmptyDestination).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for RefspecDestination {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RefspecDestination {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitRefspec {
source: RefspecSource,
destination: Option<RefspecDestination>,
direction: RefspecDirection,
mode: RefspecMode,
}
impl GitRefspec {
#[must_use]
pub const fn new(
source: RefspecSource,
destination: Option<RefspecDestination>,
direction: RefspecDirection,
mode: RefspecMode,
) -> Self {
Self {
source,
destination,
direction,
mode,
}
}
pub fn parse(value: impl AsRef<str>) -> Result<Self, RefspecParseError> {
Self::parse_with_direction(value, RefspecDirection::Fetch)
}
pub fn parse_with_direction(
value: impl AsRef<str>,
direction: RefspecDirection,
) -> Result<Self, RefspecParseError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Err(RefspecParseError::Empty);
}
let (mode, body) = trimmed
.strip_prefix('+')
.map_or((RefspecMode::Normal, trimmed), |rest| {
(RefspecMode::Force, rest)
});
if body.matches(':').count() > 1 {
return Err(RefspecParseError::TooManySeparators);
}
let (source, destination) = match body.split_once(':') {
Some((source, destination)) => (
RefspecSource::new(source)?,
Some(RefspecDestination::new(destination)?),
),
None => (RefspecSource::new(body)?, None),
};
Ok(Self::new(source, destination, direction, mode))
}
#[must_use]
pub const fn source(&self) -> &RefspecSource {
&self.source
}
#[must_use]
pub const fn destination(&self) -> Option<&RefspecDestination> {
self.destination.as_ref()
}
#[must_use]
pub const fn direction(&self) -> RefspecDirection {
self.direction
}
#[must_use]
pub const fn mode(&self) -> RefspecMode {
self.mode
}
#[must_use]
pub fn is_wildcard(&self) -> bool {
self.source.as_str().contains('*')
|| self
.destination
.as_ref()
.is_some_and(|destination| destination.as_str().contains('*'))
}
}
impl fmt::Display for GitRefspec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.mode == RefspecMode::Force {
formatter.write_str("+")?;
}
formatter.write_str(self.source.as_str())?;
if let Some(destination) = &self.destination {
write!(formatter, ":{destination}")?;
}
Ok(())
}
}
impl FromStr for GitRefspec {
type Err = RefspecParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
#[cfg(test)]
mod tests {
use super::{GitRefspec, RefspecDirection, RefspecMode, RefspecParseError};
#[test]
fn parses_force_wildcard_refspec() -> Result<(), RefspecParseError> {
let spec = GitRefspec::parse("+refs/heads/*:refs/remotes/origin/*")?;
assert_eq!(spec.mode(), RefspecMode::Force);
assert_eq!(spec.direction(), RefspecDirection::Fetch);
assert!(spec.is_wildcard());
assert_eq!(spec.to_string(), "+refs/heads/*:refs/remotes/origin/*");
Ok(())
}
#[test]
fn rejects_invalid_refspecs() {
assert_eq!(GitRefspec::parse(""), Err(RefspecParseError::Empty));
assert_eq!(GitRefspec::parse(":"), Err(RefspecParseError::EmptySource));
assert_eq!(
GitRefspec::parse("a:b:c"),
Err(RefspecParseError::TooManySeparators)
);
}
}