gitall/
lib.rs

1/*!
2Crate `gitall` provides an simple API for recursively finding all Git
3directories below a given directory.
4
5# Example
6
7The following code prints the directory paths of each Git directory located
8beneath the current directory:
9```
10use gitall::Giterator;
11use std::default::Default;
12
13let giterator = Giterator::default();
14for direntry in giterator {
15    println!("{}", direntry.path().display());
16}
17```
18
19This snippet only finds Git directories whose name matches a regular expression:
20```
21use gitall::Giterator;
22use regex::Regex;
23use std::default::Default;
24
25let giterator = Giterator::default().regex(Regex::new(r"some-pattern-.+").unwrap());
26for direntry in giterator {
27    println!("{}", direntry.path().display());
28}
29```
30
31Check the [`Giterator`][] documentation for more ways to filter results.
32*/
33
34use regex::Regex;
35use walkdir::{DirEntry, WalkDir};
36
37use std::{
38    default::Default,
39    iter,
40    path::{Path, PathBuf},
41};
42
43#[derive(Debug)]
44struct GiteratorOptions {
45    root: PathBuf,
46    follow_links: bool,
47    max_depth: usize,
48    regex: Regex,
49    match_full_path: bool,
50}
51
52/// A builder to define how to search for Git directories.
53#[derive(Debug)]
54pub struct Giterator {
55    options: GiteratorOptions,
56}
57
58impl Giterator {
59    /// Sets the apex root directory to begin searching. The directory itself
60    /// will not be considered for inclusion in the final results; only
61    /// directories within `root`.
62    pub fn root<P: AsRef<Path>>(mut self, root: P) -> Self {
63        self.options.root = root.as_ref().to_path_buf();
64        self
65    }
66
67    /// Sets wether or not to follow symbolic links in the directory tree.
68    pub fn follow_links(mut self, follow: bool) -> Self {
69        self.options.follow_links = follow;
70        self
71    }
72
73    /// Sets the maximum number of directories from `root` to search. Directories
74    /// more than `depth` directories deeper than `root` will not be found.
75    pub fn max_depth(mut self, depth: usize) -> Self {
76        self.options.max_depth = depth;
77        self
78    }
79
80    /// Sets the regular expression that must match directory paths.
81    pub fn regex(mut self, regex: Regex) -> Self {
82        self.options.regex = regex;
83        self
84    }
85
86    /// Sets wether or not to match `regex` against the full
87    /// [canonicalized][canonical] directory path.
88    ///
89    /// [canonical]: std::path::Path::canonicalize
90    pub fn match_full_path(mut self, match_full_path: bool) -> Self {
91        self.options.match_full_path = match_full_path;
92        self
93    }
94}
95
96impl Default for Giterator {
97    /// Creates a default Giterator with the following settings:
98    ///   - search the current working directory
99    ///   - do not follow symbolic links
100    ///   - search [`usize::MAX`][] directories deep
101    ///   - match any directory path
102    fn default() -> Self {
103        Giterator {
104            options: GiteratorOptions {
105                root: Path::new(".").to_path_buf(),
106                follow_links: false,
107                max_depth: usize::MAX,
108                regex: Regex::new(".*").unwrap(),
109                match_full_path: false,
110            },
111        }
112    }
113}
114
115impl iter::IntoIterator for Giterator {
116    type Item = DirEntry;
117    type IntoIter = IntoIter;
118
119    fn into_iter(self) -> Self::IntoIter {
120        IntoIter {
121            walker: WalkDir::new(self.options.root).into_iter(),
122            regex: self.options.regex,
123            full_path: self.options.match_full_path,
124        }
125    }
126}
127
128#[derive(Debug)]
129/// An iterator for recursively finding Git directories.
130pub struct IntoIter {
131    walker: ::walkdir::IntoIter,
132    regex: Regex,
133    full_path: bool,
134}
135
136impl iter::Iterator for IntoIter {
137    type Item = DirEntry;
138
139    fn next(&mut self) -> Option<Self::Item> {
140        loop {
141            let entry = match self.walker.next() {
142                None => return None, // no more directories to walk
143                Some(Err(_err)) => {
144                    // TODO provide a debug log message
145                    self.walker.skip_current_dir();
146                    continue;
147                }
148                Some(Ok(entry)) => entry,
149            };
150            if is_git_dir(&entry) {
151                self.walker.skip_current_dir();
152                if is_matching_dir(&entry, &self.regex, self.full_path) {
153                    return Some(entry);
154                }
155            }
156        }
157    }
158}
159
160/// Returns true if the directory represented by `entry` is a Git directory root.
161pub fn is_git_dir(entry: &DirEntry) -> bool {
162    entry.file_type().is_dir() && entry.path().join(".git").is_dir()
163}
164
165/// Returns true if the directory represented by `entry` matches the given
166/// regular express. If `full_path` is true, the regex matches against the
167/// [canonicalized][canonical] path of `entry`.
168///
169/// [canonical]: std::path::Path::canonicalize
170fn is_matching_dir(entry: &DirEntry, regex: &Regex, full_path: bool) -> bool {
171    let canonical_path = entry
172        .path()
173        .canonicalize()
174        .expect("failed to canonicalize the directory path");
175    let pathname: &str = if full_path {
176        canonical_path.to_str().unwrap()
177    } else {
178        entry.file_name().to_str().unwrap()
179    };
180    regex.is_match(pathname)
181}