#![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 PathspecParseError {
Empty,
UnknownMagic,
UnknownScope,
}
impl fmt::Display for PathspecParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Git pathspec cannot be empty"),
Self::UnknownMagic => formatter.write_str("unknown Git pathspec magic"),
Self::UnknownScope => formatter.write_str("unknown Git pathspec scope"),
}
}
}
impl Error for PathspecParseError {}
fn non_empty(value: impl AsRef<str>) -> Result<String, PathspecParseError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(PathspecParseError::Empty)
} else {
Ok(trimmed.to_string())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PathspecMagic {
Top,
Literal,
Glob,
Icase,
Exclude,
}
impl PathspecMagic {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Top => "top",
Self::Literal => "literal",
Self::Glob => "glob",
Self::Icase => "icase",
Self::Exclude => "exclude",
}
}
}
impl fmt::Display for PathspecMagic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PathspecMagic {
type Err = PathspecParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"top" => Ok(Self::Top),
"literal" => Ok(Self::Literal),
"glob" => Ok(Self::Glob),
"icase" | "ignore-case" => Ok(Self::Icase),
"exclude" | "!" => Ok(Self::Exclude),
"" => Err(PathspecParseError::Empty),
_ => Err(PathspecParseError::UnknownMagic),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PathspecScope {
Worktree,
Index,
Unspecified,
}
impl PathspecScope {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Worktree => "worktree",
Self::Index => "index",
Self::Unspecified => "unspecified",
}
}
}
impl fmt::Display for PathspecScope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PathspecScope {
type Err = PathspecParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"worktree" => Ok(Self::Worktree),
"index" => Ok(Self::Index),
"unspecified" | "unknown" => Ok(Self::Unspecified),
"" => Err(PathspecParseError::Empty),
_ => Err(PathspecParseError::UnknownScope),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PathspecPattern(String);
impl PathspecPattern {
pub fn new(value: impl AsRef<str>) -> Result<Self, PathspecParseError> {
non_empty(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for PathspecPattern {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for PathspecPattern {
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 GitPathspec(String);
impl GitPathspec {
pub fn new(value: impl AsRef<str>) -> Result<Self, PathspecParseError> {
non_empty(value).map(Self)
}
#[must_use]
pub fn magic(&self) -> Vec<PathspecMagic> {
parse_magic(self.as_str())
}
#[must_use]
pub fn has_magic(&self, magic: PathspecMagic) -> bool {
self.magic().contains(&magic)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for GitPathspec {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for GitPathspec {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GitPathspec {
type Err = PathspecParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
fn parse_magic(value: &str) -> Vec<PathspecMagic> {
let Some(rest) = value.strip_prefix(":(") else {
return Vec::new();
};
let Some(end) = rest.find(')') else {
return Vec::new();
};
rest[..end]
.split(',')
.filter_map(|label| label.parse::<PathspecMagic>().ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::{GitPathspec, PathspecMagic, PathspecParseError, PathspecScope};
#[test]
fn parses_pathspec_magic() -> Result<(), PathspecParseError> {
let pathspec = GitPathspec::new(":(top,literal)README.md")?;
assert!(pathspec.has_magic(PathspecMagic::Top));
assert!(pathspec.has_magic(PathspecMagic::Literal));
assert_eq!(PathspecScope::Index.to_string(), "index");
Ok(())
}
#[test]
fn rejects_empty_pathspecs() {
assert_eq!(GitPathspec::new(""), Err(PathspecParseError::Empty));
}
}