1use crate::models::{FileInfo, SimpleFileKind};
2use glob::{Pattern, PatternError};
3use std::sync::Arc;
4
5#[derive(Debug, thiserror::Error)]
7pub enum RuleError {
8 #[error("rule `{name}` has no markers, so it would match every directory scanned")]
9 NoMarkers { name: String },
10
11 #[error("rule `{name}` reclaims nothing")]
12 NoTargets { name: String },
13
14 #[error("rule `{name}` has an empty target path")]
15 EmptyTarget { name: String },
16
17 #[error("rule `{name}` has an invalid pattern `{pattern}`")]
18 InvalidPattern {
19 name: String,
20 pattern: String,
21 #[source]
22 source: PatternError,
23 },
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Target {
33 pub components: Vec<Pattern>,
34 pub kind: Option<SimpleFileKind>,
36}
37
38impl Target {
39 pub fn directory(path: &str) -> Result<Self, PatternError> {
41 Ok(Self {
42 components: Self::parse(path)?,
43 kind: Some(SimpleFileKind::Directory),
44 })
45 }
46
47 pub fn file(path: &str) -> Result<Self, PatternError> {
49 Ok(Self {
50 components: Self::parse(path)?,
51 kind: Some(SimpleFileKind::File),
52 })
53 }
54
55 pub fn any(path: &str) -> Result<Self, PatternError> {
57 Ok(Self {
58 components: Self::parse(path)?,
59 kind: None,
60 })
61 }
62
63 fn parse(path: &str) -> Result<Vec<Pattern>, PatternError> {
64 path.split('/')
65 .filter(|component| !component.is_empty())
66 .map(Pattern::new)
67 .collect()
68 }
69}
70
71#[derive(Debug, Clone)]
73pub enum CleanAction {
74 Remove(Vec<Target>),
76
77 RemoveSelf,
83
84 Run(Arc<str>),
86
87 RemoveStaleWorktrees,
92}
93
94#[derive(Debug, Clone)]
96pub struct Rule {
97 pub name: Arc<str>,
98 markers: Vec<Pattern>,
99 action: CleanAction,
100}
101
102impl Rule {
103 pub fn remove(name: &str, markers: &[&str], targets: &[&str]) -> Result<Self, RuleError> {
105 if targets.is_empty() {
106 Err(RuleError::NoTargets {
107 name: name.to_string(),
108 })
109 } else {
110 let targets = targets
111 .iter()
112 .map(|target| {
113 Target::directory(target).map_err(|source| RuleError::InvalidPattern {
114 name: name.to_string(),
115 pattern: (*target).to_string(),
116 source,
117 })
118 })
119 .collect::<Result<Vec<_>, _>>()?;
120
121 Self::new(name, markers, CleanAction::Remove(targets))
122 }
123 }
124
125 pub fn remove_targets(
127 name: &str,
128 markers: &[&str],
129 targets: Vec<Target>,
130 ) -> Result<Self, RuleError> {
131 if targets.is_empty() {
132 Err(RuleError::NoTargets {
133 name: name.to_string(),
134 })
135 } else {
136 Self::new(name, markers, CleanAction::Remove(targets))
137 }
138 }
139
140 pub fn remove_self(name: &str, markers: &[&str]) -> Result<Self, RuleError> {
142 Self::new(name, markers, CleanAction::RemoveSelf)
143 }
144
145 pub fn prune_stale_worktrees(name: &str, markers: &[&str]) -> Result<Self, RuleError> {
147 Self::new(name, markers, CleanAction::RemoveStaleWorktrees)
148 }
149
150 pub fn run(name: &str, markers: &[&str], command: &str) -> Result<Self, RuleError> {
152 Self::new(name, markers, CleanAction::Run(command.into()))
153 }
154
155 fn new(name: &str, markers: &[&str], action: CleanAction) -> Result<Self, RuleError> {
156 if markers.is_empty() {
159 Err(RuleError::NoMarkers {
160 name: name.to_string(),
161 })
162 } else {
163 let markers = markers
164 .iter()
165 .map(|marker| {
166 Pattern::new(marker).map_err(|source| RuleError::InvalidPattern {
167 name: name.to_string(),
168 pattern: (*marker).to_string(),
169 source,
170 })
171 })
172 .collect::<Result<Vec<_>, _>>()?;
173
174 Ok(Self {
175 name: name.into(),
176 markers,
177 action,
178 })
179 }
180 }
181
182 pub fn action(&self) -> &CleanAction {
183 &self.action
184 }
185
186 pub fn matches(&self, entries: &[FileInfo]) -> bool {
191 self.markers
192 .iter()
193 .all(|marker| entries.iter().any(|entry| marker.matches(&entry.name)))
194 }
195}
196
197pub fn widest_name(rules: &[Rule]) -> usize {
199 rules
200 .iter()
201 .map(|rule| rule.name.chars().count())
202 .max()
203 .unwrap_or(0)
204}
205
206#[cfg(test)]
207mod tests;