use crate::models::{FileInfo, SimpleFileKind};
use glob::{Pattern, PatternError};
use std::sync::Arc;
#[derive(Debug, thiserror::Error)]
pub enum RuleError {
#[error("rule `{name}` has no markers, so it would match every directory scanned")]
NoMarkers { name: String },
#[error("rule `{name}` reclaims nothing")]
NoTargets { name: String },
#[error("rule `{name}` has an empty target path")]
EmptyTarget { name: String },
#[error("rule `{name}` has an invalid pattern `{pattern}`")]
InvalidPattern {
name: String,
pattern: String,
#[source]
source: PatternError,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Target {
pub components: Vec<Pattern>,
pub kind: Option<SimpleFileKind>,
}
impl Target {
pub fn directory(path: &str) -> Result<Self, PatternError> {
Ok(Self {
components: Self::parse(path)?,
kind: Some(SimpleFileKind::Directory),
})
}
pub fn file(path: &str) -> Result<Self, PatternError> {
Ok(Self {
components: Self::parse(path)?,
kind: Some(SimpleFileKind::File),
})
}
pub fn any(path: &str) -> Result<Self, PatternError> {
Ok(Self {
components: Self::parse(path)?,
kind: None,
})
}
fn parse(path: &str) -> Result<Vec<Pattern>, PatternError> {
path.split('/')
.filter(|component| !component.is_empty())
.map(Pattern::new)
.collect()
}
}
#[derive(Debug, Clone)]
pub enum CleanAction {
Remove(Vec<Target>),
RemoveSelf,
Run(Arc<str>),
RemoveStaleWorktrees,
}
#[derive(Debug, Clone)]
pub struct Rule {
pub name: Arc<str>,
markers: Vec<Pattern>,
action: CleanAction,
}
impl Rule {
pub fn remove(name: &str, markers: &[&str], targets: &[&str]) -> Result<Self, RuleError> {
if targets.is_empty() {
Err(RuleError::NoTargets {
name: name.to_string(),
})
} else {
let targets = targets
.iter()
.map(|target| {
Target::directory(target).map_err(|source| RuleError::InvalidPattern {
name: name.to_string(),
pattern: (*target).to_string(),
source,
})
})
.collect::<Result<Vec<_>, _>>()?;
Self::new(name, markers, CleanAction::Remove(targets))
}
}
pub fn remove_targets(
name: &str,
markers: &[&str],
targets: Vec<Target>,
) -> Result<Self, RuleError> {
if targets.is_empty() {
Err(RuleError::NoTargets {
name: name.to_string(),
})
} else {
Self::new(name, markers, CleanAction::Remove(targets))
}
}
pub fn remove_self(name: &str, markers: &[&str]) -> Result<Self, RuleError> {
Self::new(name, markers, CleanAction::RemoveSelf)
}
pub fn prune_stale_worktrees(name: &str, markers: &[&str]) -> Result<Self, RuleError> {
Self::new(name, markers, CleanAction::RemoveStaleWorktrees)
}
pub fn run(name: &str, markers: &[&str], command: &str) -> Result<Self, RuleError> {
Self::new(name, markers, CleanAction::Run(command.into()))
}
fn new(name: &str, markers: &[&str], action: CleanAction) -> Result<Self, RuleError> {
if markers.is_empty() {
Err(RuleError::NoMarkers {
name: name.to_string(),
})
} else {
let markers = markers
.iter()
.map(|marker| {
Pattern::new(marker).map_err(|source| RuleError::InvalidPattern {
name: name.to_string(),
pattern: (*marker).to_string(),
source,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
name: name.into(),
markers,
action,
})
}
}
pub fn action(&self) -> &CleanAction {
&self.action
}
pub fn matches(&self, entries: &[FileInfo]) -> bool {
self.markers
.iter()
.all(|marker| entries.iter().any(|entry| marker.matches(&entry.name)))
}
}
pub fn widest_name(rules: &[Rule]) -> usize {
rules
.iter()
.map(|rule| rule.name.chars().count())
.max()
.unwrap_or(0)
}
#[cfg(test)]
mod tests;