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}