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
/*!
Crate `gitall` provides an simple API for recursively finding all Git
directories below a given directory.

# Example

The following code prints the directory paths of each Git directory located
beneath the current directory:
```
use gitall::Giterator;
use std::default::Default;

let giterator = Giterator::default();
for direntry in giterator {
    println!("{}", direntry.path().display());
}
```

This snippet only finds Git directories whose name matches a regular expression:
```
use gitall::Giterator;
use regex::Regex;
use std::default::Default;

let giterator = Giterator::default().regex(Regex::new(r"some-pattern-.+").unwrap());
for direntry in giterator {
    println!("{}", direntry.path().display());
}
```

Check the [`Giterator`][] documentation for more ways to filter results.
*/

use regex::Regex;
use walkdir::{DirEntry, WalkDir};

use std::{
    default::Default,
    iter,
    path::{Path, PathBuf},
};

#[derive(Debug)]
struct GiteratorOptions {
    root: PathBuf,
    follow_links: bool,
    max_depth: usize,
    regex: Regex,
    match_full_path: bool,
}

/// A builder to define how to search for Git directories.
#[derive(Debug)]
pub struct Giterator {
    options: GiteratorOptions,
}

impl Giterator {
    /// Sets the apex root directory to begin searching. The directory itself
    /// will not be considered for inclusion in the final results; only
    /// directories within `root`.
    pub fn root<P: AsRef<Path>>(mut self, root: P) -> Self {
        self.options.root = root.as_ref().to_path_buf();
        self
    }

    /// Sets wether or not to follow symbolic links in the directory tree.
    pub fn follow_links(mut self, follow: bool) -> Self {
        self.options.follow_links = follow;
        self
    }

    /// Sets the maximum number of directories from `root` to search. Directories
    /// more than `depth` directories deeper than `root` will not be found.
    pub fn max_depth(mut self, depth: usize) -> Self {
        self.options.max_depth = depth;
        self
    }

    /// Sets the regular expression that must match directory paths.
    pub fn regex(mut self, regex: Regex) -> Self {
        self.options.regex = regex;
        self
    }

    /// Sets wether or not to match `regex` against the full
    /// [canonicalized][canonical] directory path.
    ///
    /// [canonical]: std::path::Path::canonicalize
    pub fn match_full_path(mut self, match_full_path: bool) -> Self {
        self.options.match_full_path = match_full_path;
        self
    }
}

impl Default for Giterator {
    /// Creates a default Giterator with the following settings:
    ///   - search the current working directory
    ///   - do not follow symbolic links
    ///   - search [`usize::MAX`][] directories deep
    ///   - match any directory path
    fn default() -> Self {
        Giterator {
            options: GiteratorOptions {
                root: Path::new(".").to_path_buf(),
                follow_links: false,
                max_depth: usize::MAX,
                regex: Regex::new(".*").unwrap(),
                match_full_path: false,
            },
        }
    }
}

impl iter::IntoIterator for Giterator {
    type Item = DirEntry;
    type IntoIter = IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        IntoIter {
            walker: WalkDir::new(self.options.root).into_iter(),
            regex: self.options.regex,
            full_path: self.options.match_full_path,
        }
    }
}

#[derive(Debug)]
/// An iterator for recursively finding Git directories.
pub struct IntoIter {
    walker: ::walkdir::IntoIter,
    regex: Regex,
    full_path: bool,
}

impl iter::Iterator for IntoIter {
    type Item = DirEntry;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let entry = match self.walker.next() {
                None => return None, // no more directories to walk
                Some(Err(_err)) => {
                    // TODO provide a debug log message
                    self.walker.skip_current_dir();
                    continue;
                }
                Some(Ok(entry)) => entry,
            };
            if is_git_dir(&entry) {
                self.walker.skip_current_dir();
                if is_matching_dir(&entry, &self.regex, self.full_path) {
                    return Some(entry);
                }
            }
        }
    }
}

/// Returns true if the directory represented by `entry` is a Git directory root.
pub fn is_git_dir(entry: &DirEntry) -> bool {
    entry.file_type().is_dir() && entry.path().join(".git").is_dir()
}

/// Returns true if the directory represented by `entry` matches the given
/// regular express. If `full_path` is true, the regex matches against the
/// [canonicalized][canonical] path of `entry`.
///
/// [canonical]: std::path::Path::canonicalize
fn is_matching_dir(entry: &DirEntry, regex: &Regex, full_path: bool) -> bool {
    let canonical_path = entry
        .path()
        .canonicalize()
        .expect("failed to canonicalize the directory path");
    let pathname: &str = if full_path {
        canonical_path.to_str().unwrap()
    } else {
        entry.file_name().to_str().unwrap()
    };
    regex.is_match(pathname)
}