1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
use std::collections::HashSet;
use std::fmt::Debug;
use std::hash::Hash;
use std::iter::FromIterator;
use std::mem::discriminant;
use std::process::exit;
use std::str::FromStr;

use clap::Clap;
use thiserror::Error;

#[derive(Clap, Default)]
#[clap(
    version,
    about = "Automatically trims your tracking branches whose upstream branches are merged or stray.",
    long_about = "Automatically trims your tracking branches whose upstream branches are merged or stray.
`git-trim` is a missing companion to the `git fetch --prune` and a proper, safer, faster alternative to your `<bash oneliner HERE>`."
)]
pub struct Args {
    /// Comma separated multiple names of branches.
    /// All the other branches are compared with the upstream branches of those branches.
    /// [default: branches that tracks `git symbolic-ref refs/remotes/*/HEAD`] [config: trim.bases]
    ///
    /// The default value is a branch that tracks `git symbolic-ref refs/remotes/*/HEAD`.
    /// They might not be reflected correctly when the HEAD branch of your remote repository is changed.
    /// You can see the changed HEAD branch name with `git remote show <remote>`
    /// and apply it to your local repository with `git remote set-head <remote> --auto`.
    #[clap(short, long, value_delimiter = ",", aliases=&["base"])]
    pub bases: Vec<String>,

    /// Comma separated multiple glob patterns (e.g. `release-*`, `feature/*`) of branches that should never be deleted.
    /// [config: trim.protected]
    #[clap(short, long, value_delimiter = ",")]
    pub protected: Vec<String>,

    /// Do not update remotes
    /// [config: trim.update]
    #[clap(long)]
    pub no_update: bool,
    #[clap(long, hidden(true))]
    pub update: bool,

    /// Prevents too frequent updates. Seconds between updates in seconds. 0 to disable.
    /// [default: 5] [config: trim.updateInterval]
    #[clap(long)]
    pub update_interval: Option<u64>,

    /// Do not ask confirm
    /// [config: trim.confirm]
    #[clap(long)]
    pub no_confirm: bool,
    #[clap(long, hidden(true))]
    pub confirm: bool,

    /// Do not detach when HEAD is about to be deleted
    /// [config: trim.detach]
    #[clap(long)]
    pub no_detach: bool,
    #[clap(long, hidden(true))]
    pub detach: bool,

    /// Comma separated values of `<delete range>[:<remote name>]`.
    /// Delete range is one of the `merged, merged-local, merged-remote, stray, diverged, local, remote`.
    /// `:<remote name>` is only necessary to a `<delete range>` when the range is applied to remote branches.
    /// You can use `*` as `<remote name>` to delete a range of branches from all remotes.
    /// [default : `merged:origin`] [config: trim.delete]
    ///
    /// `merged` implies `merged-local,merged-remote`.
    ///
    /// `merged-local` will delete merged tracking local branches.
    /// `merged-remote:<remote>` will delete merged upstream branches from `<remote>`.
    /// `stray` will delete tracking local branches, which is not merged, but the upstream is gone.
    /// `diverged:<remote>` will delete merged tracking local branches, and their upstreams from `<remote>` even if the upstreams are not merged and diverged from local ones.
    /// `local` will delete non-tracking merged local branches.
    /// `remote:<remote>` will delete non-upstream merged remote tracking branches.
    /// Use with caution when you are using other than `merged`. It might lose changes, and even nuke repositories.
    #[clap(short, long, value_delimiter = ",")]
    pub delete: Vec<DeleteRange>,

    /// Do not delete branches, show what branches will be deleted.
    #[clap(long)]
    pub dry_run: bool,
}

impl Args {
    pub fn update(&self) -> Option<bool> {
        exclusive_bool(("update", self.update), ("no-update", self.no_update))
    }

    pub fn confirm(&self) -> Option<bool> {
        exclusive_bool(("confirm", self.confirm), ("no-confirm", self.no_confirm))
    }

    pub fn detach(&self) -> Option<bool> {
        exclusive_bool(("detach", self.detach), ("no-detach", self.no_detach))
    }
}

impl paw::ParseArgs for Args {
    /// Error type.
    type Error = std::io::Error;

    /// Try to parse an input to a type.
    fn parse_args() -> Result<Self, Self::Error> {
        Ok(Args::parse())
    }
}

fn exclusive_bool(
    (name_pos, value_pos): (&str, bool),
    (name_neg, value_neg): (&str, bool),
) -> Option<bool> {
    if value_pos && value_neg {
        eprintln!(
            "Error: Flag '{}' and '{}' cannot be used simultaneously",
            name_pos, name_neg,
        );
        exit(-1);
    }

    if value_pos {
        Some(true)
    } else if value_neg {
        Some(false)
    } else {
        None
    }
}

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub enum Scope {
    All,
    Scoped(String),
}

impl FromStr for Scope {
    type Err = ScopeParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim() {
            "" => Err(ScopeParseError {
                message: "Scope is empty".to_owned(),
            }),
            "*" => Ok(Scope::All),
            scope => Ok(Scope::Scoped(scope.to_owned())),
        }
    }
}

#[derive(Error, Debug)]
#[error("{message}")]
pub struct ScopeParseError {
    message: String,
}

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub enum DeleteRange {
    Merged(Scope),
    MergedLocal,
    MergedRemote(Scope),
    Stray,
    Diverged(Scope),
    Local,
    Remote(Scope),
}

#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub enum DeleteUnit {
    MergedLocal,
    MergedRemote(Scope),
    Stray,
    Diverged(Scope),
    MergedNonTrackingLocal,
    MergedNonUpstreamRemoteTracking(Scope),
}

impl FromStr for DeleteRange {
    type Err = DeleteParseError;

    fn from_str(arg: &str) -> Result<DeleteRange, Self::Err> {
        let some_pair: Vec<_> = arg.splitn(2, ':').map(str::trim).collect();
        match *some_pair.as_slice() {
            ["merged", remote] => Ok(DeleteRange::Merged(remote.parse()?)),
            ["stray"] => Ok(DeleteRange::Stray),
            ["diverged", remote] => Ok(DeleteRange::Diverged(remote.parse()?)),
            ["merged-local"] => Ok(DeleteRange::MergedLocal),
            ["merged-remote", remote] => Ok(DeleteRange::MergedRemote(remote.parse()?)),
            ["local"] => Ok(DeleteRange::Local),
            ["remote", remote] => Ok(DeleteRange::Remote(remote.parse()?)),
            _ => Err(DeleteParseError::InvalidDeleteRangeFormat(arg.to_owned())),
        }
    }
}

impl DeleteRange {
    fn to_delete_units(&self) -> Vec<DeleteUnit> {
        match self {
            DeleteRange::Merged(scope) => vec![
                DeleteUnit::MergedLocal,
                DeleteUnit::MergedRemote(scope.clone()),
            ],
            DeleteRange::MergedLocal => vec![DeleteUnit::MergedLocal],
            DeleteRange::MergedRemote(scope) => vec![DeleteUnit::MergedRemote(scope.clone())],
            DeleteRange::Stray => vec![DeleteUnit::Stray],
            DeleteRange::Diverged(scope) => vec![DeleteUnit::Diverged(scope.clone())],
            DeleteRange::Local => vec![DeleteUnit::MergedNonTrackingLocal],
            DeleteRange::Remote(scope) => {
                vec![DeleteUnit::MergedNonUpstreamRemoteTracking(scope.clone())]
            }
        }
    }

    pub fn merged_origin() -> Vec<Self> {
        use DeleteRange::*;
        vec![
            MergedLocal,
            MergedRemote(Scope::Scoped("origin".to_string())),
        ]
    }
}

#[derive(Error, Debug)]
pub enum DeleteParseError {
    #[error("Invalid delete range format `{0}`")]
    InvalidDeleteRangeFormat(String),
    #[error("Scope parse error for delete range while parsing scope: {0}")]
    ScopeParseError(#[from] ScopeParseError),
}

#[derive(Debug, Clone, Eq, PartialEq, Default)]
pub struct DeleteFilter(HashSet<DeleteUnit>);

impl DeleteFilter {
    pub fn scan_tracking(&self) -> bool {
        self.0.iter().any(|unit| {
            matches!(unit,
                DeleteUnit::MergedLocal
                | DeleteUnit::MergedRemote(_)
                | DeleteUnit::Stray
                | DeleteUnit::Diverged(_))
        })
    }

    pub fn scan_non_tracking_local(&self) -> bool {
        self.0.contains(&DeleteUnit::MergedNonTrackingLocal)
    }

    pub fn scan_non_upstream_remote(&self, remote: &str) -> bool {
        for unit in self.0.iter() {
            match unit {
                DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::All) => return true,
                DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::Scoped(specific))
                    if specific == remote =>
                {
                    return true
                }
                _ => {}
            }
        }
        false
    }

    pub fn delete_merged_local(&self) -> bool {
        self.0.contains(&DeleteUnit::MergedLocal)
    }

    pub fn delete_merged_remote(&self, remote: &str) -> bool {
        for unit in self.0.iter() {
            match unit {
                DeleteUnit::MergedRemote(Scope::All) => return true,
                DeleteUnit::MergedRemote(Scope::Scoped(specific)) if specific == remote => {
                    return true
                }
                _ => {}
            }
        }
        false
    }

    pub fn delete_stray(&self) -> bool {
        self.0.contains(&DeleteUnit::Stray)
    }

    pub fn delete_diverged(&self, remote: &str) -> bool {
        for unit in self.0.iter() {
            match unit {
                DeleteUnit::Diverged(Scope::All) => return true,
                DeleteUnit::Diverged(Scope::Scoped(specific)) if specific == remote => return true,
                _ => {}
            }
        }
        false
    }

    pub fn delete_merged_non_tracking_local(&self) -> bool {
        self.0.contains(&DeleteUnit::MergedNonTrackingLocal)
    }

    pub fn delete_merged_non_upstream_remote_tracking(&self, remote: &str) -> bool {
        for filter in self.0.iter() {
            match filter {
                DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::All) => return true,
                DeleteUnit::MergedNonUpstreamRemoteTracking(Scope::Scoped(specific))
                    if specific == remote =>
                {
                    return true
                }
                _ => {}
            }
        }
        false
    }
}

impl FromIterator<DeleteUnit> for DeleteFilter {
    fn from_iter<I>(iter: I) -> Self
    where
        I: IntoIterator<Item = DeleteUnit>,
    {
        use DeleteUnit::*;
        use Scope::*;

        let mut result = HashSet::new();
        for unit in iter.into_iter() {
            match unit {
                MergedLocal | Stray | MergedNonTrackingLocal => {
                    result.insert(unit.clone());
                }
                MergedRemote(All) | Diverged(All) | MergedNonUpstreamRemoteTracking(All) => {
                    result.retain(|x| discriminant(x) != discriminant(&unit));
                    result.insert(unit.clone());
                }
                MergedRemote(_) => {
                    if !result.contains(&MergedRemote(All)) {
                        result.insert(unit.clone());
                    }
                }
                Diverged(_) => {
                    if !result.contains(&Diverged(All)) {
                        result.insert(unit.clone());
                    }
                }
                MergedNonUpstreamRemoteTracking(_) => {
                    if !result.contains(&MergedNonUpstreamRemoteTracking(All)) {
                        result.insert(unit.clone());
                    }
                }
            }
        }

        Self(result)
    }
}

impl FromIterator<DeleteRange> for DeleteFilter {
    fn from_iter<I>(iter: I) -> Self
    where
        I: IntoIterator<Item = DeleteRange>,
    {
        Self::from_iter(iter.into_iter().map(|x| x.to_delete_units()).flatten())
    }
}