1use std::collections::HashSet;
2use std::fmt::Debug;
3use std::hash::Hash;
4use std::iter::FromIterator;
5use std::mem::discriminant;
6use std::process::exit;
7use std::str::FromStr;
8
9use clap::Parser;
10use thiserror::Error;
11
12#[derive(Parser, Default)]
13#[clap(
14 version,
15 about = "Automatically trims your tracking branches whose upstream branches are merged or stray.",
16 long_about = "Automatically trims your tracking branches whose upstream branches are merged or stray.
17`git-trim` is a missing companion to the `git fetch --prune` and a proper, safer, faster alternative to your `<bash oneliner HERE>`."
18)]
19pub struct Args {
20 #[clap(short, long, value_delimiter = ',', aliases=&["base"])]
29 pub bases: Vec<String>,
30
31 #[clap(short, long, value_delimiter = ',')]
34 pub protected: Vec<String>,
35
36 #[clap(long)]
39 pub no_update: bool,
40 #[clap(long, hide(true))]
41 pub update: bool,
42
43 #[clap(long)]
46 pub update_interval: Option<u64>,
47
48 #[clap(long)]
51 pub no_confirm: bool,
52 #[clap(long, hide(true))]
53 pub confirm: bool,
54
55 #[clap(long)]
58 pub no_detach: bool,
59 #[clap(long, hide(true))]
60 pub detach: bool,
61
62 #[clap(short, long, value_delimiter = ',')]
78 pub delete: Vec<DeleteRange>,
79
80 #[clap(long)]
82 pub dry_run: bool,
83}
84
85impl Args {
86 pub fn update(&self) -> Option<bool> {
87 exclusive_bool(("update", self.update), ("no-update", self.no_update))
88 }
89
90 pub fn confirm(&self) -> Option<bool> {
91 exclusive_bool(("confirm", self.confirm), ("no-confirm", self.no_confirm))
92 }
93
94 pub fn detach(&self) -> Option<bool> {
95 exclusive_bool(("detach", self.detach), ("no-detach", self.no_detach))
96 }
97}
98
99fn exclusive_bool(
100 (name_pos, value_pos): (&str, bool),
101 (name_neg, value_neg): (&str, bool),
102) -> Option<bool> {
103 if value_pos && value_neg {
104 eprintln!(
105 "Error: Flag '{}' and '{}' cannot be used simultaneously",
106 name_pos, name_neg,
107 );
108 exit(-1);
109 }
110
111 if value_pos {
112 Some(true)
113 } else if value_neg {
114 Some(false)
115 } else {
116 None
117 }
118}
119
120#[derive(Hash, Eq, PartialEq, Clone, Debug)]
121pub enum Scope {
122 All,
123 Scoped(String),
124}
125
126impl FromStr for Scope {
127 type Err = ScopeParseError;
128
129 fn from_str(s: &str) -> Result<Self, Self::Err> {
130 match s.trim() {
131 "" => Err(ScopeParseError {
132 message: "Scope is empty".to_owned(),
133 }),
134 "*" => Ok(Scope::All),
135 scope => Ok(Scope::Scoped(scope.to_owned())),
136 }
137 }
138}
139
140#[derive(Error, Debug)]
141#[error("{message}")]
142pub struct ScopeParseError {
143 message: String,
144}
145
146#[derive(Hash, Eq, PartialEq, Clone, Debug)]
147pub enum DeleteRange {
148 Merged(Scope),
149 MergedLocal,
150 MergedRemote(Scope),
151 Stray,
152 Diverged(Scope),
153 Local,
154 Remote(Scope),
155}
156
157#[derive(Hash, Eq, PartialEq, Clone, Debug)]
158pub enum DeleteUnit {
159 MergedLocal,
160 MergedRemote(Scope),
161 Stray,
162 Diverged(Scope),
163 MergedNonTrackingLocal,
164 MergedNonUpstreamRemoteTracking(Scope),
165}
166
167impl FromStr for DeleteRange {
168 type Err = DeleteParseError;
169
170 fn from_str(arg: &str) -> Result<DeleteRange, Self::Err> {
171 let some_pair: Vec<_> = arg.splitn(2, ':').map(str::trim).collect();
172 match *some_pair.as_slice() {
173 ["merged", remote] => Ok(DeleteRange::Merged(remote.parse()?)),
174 ["stray"] => Ok(DeleteRange::Stray),
175 ["diverged", remote] => Ok(DeleteRange::Diverged(remote.parse()?)),
176 ["merged-local"] => Ok(DeleteRange::MergedLocal),
177 ["merged-remote", remote] => Ok(DeleteRange::MergedRemote(remote.parse()?)),
178 ["local"] => Ok(DeleteRange::Local),
179 ["remote", remote] => Ok(DeleteRange::Remote(remote.parse()?)),
180 _ => Err(DeleteParseError::InvalidDeleteRangeFormat(arg.to_owned())),
181 }
182 }
183}
184
185impl DeleteRange {
186 fn to_delete_units(&self) -> Vec<DeleteUnit> {
187 match self {
188 DeleteRange::Merged(scope) => vec![
189 DeleteUnit::MergedLocal,
190 DeleteUnit::MergedRemote(scope.clone()),
191 ],
192 DeleteRange::MergedLocal => vec![DeleteUnit::MergedLocal],
193 DeleteRange::MergedRemote(scope) => vec![DeleteUnit::MergedRemote(scope.clone())],
194 DeleteRange::Stray => vec![DeleteUnit::Stray],
195 DeleteRange::Diverged(scope) => vec![DeleteUnit::Diverged(scope.clone())],
196 DeleteRange::Local => vec![DeleteUnit::MergedNonTrackingLocal],
197 DeleteRange::Remote(scope) => {
198 vec![DeleteUnit::MergedNonUpstreamRemoteTracking(scope.clone())]
199 }
200 }
201 }
202
203 pub fn merged_origin() -> Vec<Self> {
204 use DeleteRange::*;
205 vec![
206 MergedLocal,
207 MergedRemote(Scope::Scoped("origin".to_string())),
208 ]
209 }
210}
211
212#[derive(Error, Debug)]
213pub enum DeleteParseError {
214 #[error("Invalid delete range format `{0}`")]
215 InvalidDeleteRangeFormat(String),
216 #[error("Scope parse error for delete range while parsing scope: {0}")]
217 ScopeParseError(#[from] ScopeParseError),
218}
219
220#[derive(Debug, Clone, Eq, PartialEq, Default)]
221pub struct DeleteFilter(HashSet<DeleteUnit>);
222
223impl DeleteFilter {
224 pub fn scan_tracking(&self) -> bool {
225 self.0.iter().any(|unit| {
226 matches!(
227 unit,
228 DeleteUnit::MergedLocal
229 | DeleteUnit::MergedRemote(_)
230 | DeleteUnit::Stray
231 | DeleteUnit::Diverged(_)
232 )
233 })
234 }
235
236 pub fn scan_non_tracking_local(&self) -> bool {
237 self.0.contains(&DeleteUnit::MergedNonTrackingLocal)
238 }
239
240 pub fn scan_non_upstream_remote(&self, remote: &str) -> bool {
241 for unit in self.0.iter() {
242 match unit {
243 DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::All) => return true,
244 DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::Scoped(specific))
245 if specific == remote =>
246 {
247 return true
248 }
249 _ => {}
250 }
251 }
252 false
253 }
254
255 pub fn delete_merged_local(&self) -> bool {
256 self.0.contains(&DeleteUnit::MergedLocal)
257 }
258
259 pub fn delete_merged_remote(&self, remote: &str) -> bool {
260 for unit in self.0.iter() {
261 match unit {
262 DeleteUnit::MergedRemote(Scope::All) => return true,
263 DeleteUnit::MergedRemote(Scope::Scoped(specific)) if specific == remote => {
264 return true
265 }
266 _ => {}
267 }
268 }
269 false
270 }
271
272 pub fn delete_stray(&self) -> bool {
273 self.0.contains(&DeleteUnit::Stray)
274 }
275
276 pub fn delete_diverged(&self, remote: &str) -> bool {
277 for unit in self.0.iter() {
278 match unit {
279 DeleteUnit::Diverged(Scope::All) => return true,
280 DeleteUnit::Diverged(Scope::Scoped(specific)) if specific == remote => return true,
281 _ => {}
282 }
283 }
284 false
285 }
286
287 pub fn delete_merged_non_tracking_local(&self) -> bool {
288 self.0.contains(&DeleteUnit::MergedNonTrackingLocal)
289 }
290
291 pub fn delete_merged_non_upstream_remote_tracking(&self, remote: &str) -> bool {
292 for filter in self.0.iter() {
293 match filter {
294 DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::All) => return true,
295 DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::Scoped(specific))
296 if specific == remote =>
297 {
298 return true
299 }
300 _ => {}
301 }
302 }
303 false
304 }
305}
306
307impl FromIterator<DeleteUnit> for DeleteFilter {
308 fn from_iter<I>(iter: I) -> Self
309 where
310 I: IntoIterator<Item = DeleteUnit>,
311 {
312 use DeleteUnit::*;
313 use Scope::*;
314
315 let mut result = HashSet::new();
316 for unit in iter.into_iter() {
317 match unit {
318 MergedLocal | Stray | MergedNonTrackingLocal => {
319 result.insert(unit.clone());
320 }
321 MergedRemote(All) | Diverged(All) | MergedNonUpstreamRemoteTracking(All) => {
322 result.retain(|x| discriminant(x) != discriminant(&unit));
323 result.insert(unit.clone());
324 }
325 MergedRemote(_) => {
326 if !result.contains(&MergedRemote(All)) {
327 result.insert(unit.clone());
328 }
329 }
330 Diverged(_) => {
331 if !result.contains(&Diverged(All)) {
332 result.insert(unit.clone());
333 }
334 }
335 MergedNonUpstreamRemoteTracking(_) => {
336 if !result.contains(&MergedNonUpstreamRemoteTracking(All)) {
337 result.insert(unit.clone());
338 }
339 }
340 }
341 }
342
343 Self(result)
344 }
345}
346
347impl FromIterator<DeleteRange> for DeleteFilter {
348 fn from_iter<I>(iter: I) -> Self
349 where
350 I: IntoIterator<Item = DeleteRange>,
351 {
352 Self::from_iter(iter.into_iter().flat_map(|x| x.to_delete_units()))
353 }
354}