1use std::env;
8use std::fs::{create_dir_all, remove_file, File};
9use std::io::{BufRead, BufReader, Write};
10use std::path::{Path, PathBuf};
11
12use directories::{ProjectDirs, UserDirs};
13use walkdir::{DirEntry, WalkDir};
14
15use crate::repo::Repo;
16
17const QUALIFIER: &str = "";
18const ORGANIZATION: &str = "peap";
19const APPLICATION: &str = "git-global";
20const CACHE_FILE: &str = "repos.txt";
21
22const DEFAULT_CMD: &str = "status";
23const DEFAULT_FOLLOW_SYMLINKS: bool = true;
24const DEFAULT_SAME_FILESYSTEM: bool = cfg!(any(unix, windows));
25const DEFAULT_VERBOSE: bool = false;
26const DEFAULT_SHOW_UNTRACKED: bool = true;
27
28const SETTING_BASEDIR: &str = "global.basedir";
29const SETTING_FOLLOW_SYMLINKS: &str = "global.follow-symlinks";
30const SETTING_SAME_FILESYSTEM: &str = "global.same-filesystem";
31const SETTING_IGNORE: &str = "global.ignore";
32const SETTING_DEFAULT_CMD: &str = "global.default-cmd";
33const SETTING_SHOW_UNTRACKED: &str = "global.show-untracked";
34const SETTING_VERBOSE: &str = "global.verbose";
35
36pub struct Config {
38 pub basedir: PathBuf,
42
43 pub follow_symlinks: bool,
47
48 pub same_filesystem: bool,
53
54 pub ignored_patterns: Vec<String>,
58
59 pub default_cmd: String,
63
64 pub verbose: bool,
68
69 pub show_untracked: bool,
73
74 pub cache_file: Option<PathBuf>,
79
80 pub manpage_file: Option<PathBuf>,
85}
86
87impl Default for Config {
88 fn default() -> Self {
89 Config::new()
90 }
91}
92
93impl Config {
94 pub fn new() -> Self {
97 let homedir = UserDirs::new()
99 .expect("Could not determine home directory.")
100 .home_dir()
101 .to_path_buf();
102 let cache_file =
104 ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
105 .map(|project_dirs| project_dirs.cache_dir().join(CACHE_FILE));
106 let manpage_file = match env::consts::OS {
107 "linux" => Some(PathBuf::from("/usr/share/man/man1/git-global.1")),
109 "macos" => Some(PathBuf::from("/usr/share/man/man1/git-global.1")),
110 "windows" => env::var("MSYSTEM").ok().and_then(|val| {
111 (val == "MINGW64").then(|| {
112 PathBuf::from("/mingw64/share/doc/git-doc/git-global.html")
113 })
114 }),
115 _ => None,
116 };
117 match ::git2::Config::open_default() {
118 Ok(cfg) => Config {
119 basedir: cfg.get_path(SETTING_BASEDIR).unwrap_or(homedir),
120 follow_symlinks: cfg
121 .get_bool(SETTING_FOLLOW_SYMLINKS)
122 .unwrap_or(DEFAULT_FOLLOW_SYMLINKS),
123 same_filesystem: cfg
124 .get_bool(SETTING_SAME_FILESYSTEM)
125 .unwrap_or(DEFAULT_SAME_FILESYSTEM),
126 ignored_patterns: cfg
127 .get_string(SETTING_IGNORE)
128 .unwrap_or_default()
129 .split(',')
130 .map(|p| p.trim().to_string())
131 .collect(),
132 default_cmd: cfg
133 .get_string(SETTING_DEFAULT_CMD)
134 .unwrap_or_else(|_| String::from(DEFAULT_CMD)),
135 verbose: cfg
136 .get_bool(SETTING_VERBOSE)
137 .unwrap_or(DEFAULT_VERBOSE),
138 show_untracked: cfg
139 .get_bool(SETTING_SHOW_UNTRACKED)
140 .unwrap_or(DEFAULT_SHOW_UNTRACKED),
141 cache_file,
142 manpage_file,
143 },
144 Err(_) => {
145 Config {
147 basedir: homedir,
148 follow_symlinks: DEFAULT_FOLLOW_SYMLINKS,
149 same_filesystem: DEFAULT_SAME_FILESYSTEM,
150 ignored_patterns: vec![],
151 default_cmd: String::from(DEFAULT_CMD),
152 verbose: DEFAULT_VERBOSE,
153 show_untracked: DEFAULT_SHOW_UNTRACKED,
154 cache_file,
155 manpage_file,
156 }
157 }
158 }
159 }
160
161 pub fn get_repos(&mut self) -> Vec<Repo> {
163 if !self.has_cache() {
164 let repos = self.find_repos();
165 self.cache_repos(&repos);
166 }
167 self.get_cached_repos()
168 }
169
170 pub fn clear_cache(&mut self) {
173 if self.has_cache() {
174 if let Some(file) = &self.cache_file {
175 remove_file(file).expect("Failed to delete cache file.");
176 }
177 }
178 }
179
180 fn filter(&self, entry: &DirEntry) -> bool {
182 if let Some(entry_path) = entry.path().to_str() {
183 self.ignored_patterns
184 .iter()
185 .filter(|p| p != &"")
186 .all(|pattern| !entry_path.contains(pattern))
187 } else {
188 false
190 }
191 }
192
193 fn find_repos(&self) -> Vec<Repo> {
195 let mut repos = Vec::new();
196 println!(
197 "Scanning for git repos under {}; this may take a while...",
198 self.basedir.display()
199 );
200 let mut n_dirs = 0;
201 let walker = WalkDir::new(&self.basedir)
202 .follow_links(self.follow_symlinks)
203 .same_file_system(self.same_filesystem);
204 for entry in walker
205 .into_iter()
206 .filter_entry(|e| self.filter(e))
207 .flatten()
208 {
209 if entry.file_type().is_dir() {
210 n_dirs += 1;
211 if entry.file_name() == ".git" {
212 let parent_path = entry
213 .path()
214 .parent()
215 .expect("Could not determine parent.");
216 if let Some(path) = parent_path.to_str() {
217 repos.push(Repo::new(path.to_string()));
218 }
219 }
220 if self.verbose {
221 if let Some(size) = termsize::get() {
222 let prefix = format!(
223 "\r... found {} repos; scanning directory #{}: ",
224 repos.len(),
225 n_dirs
226 );
227 let width = size.cols as usize - prefix.len() - 1;
228 let mut cur_path =
229 String::from(entry.path().to_str().unwrap());
230 let byte_width =
231 match cur_path.char_indices().nth(width) {
232 None => &cur_path,
233 Some((idx, _)) => &cur_path[..idx],
234 }
235 .len();
236 cur_path.truncate(byte_width);
237 print!("{}{:<width$}", prefix, cur_path);
238 };
239 }
240 }
241 }
242 if self.verbose {
243 println!();
244 }
245 repos.sort_by_key(|r| r.path());
246 repos
247 }
248
249 fn has_cache(&self) -> bool {
251 self.cache_file.as_ref().is_some_and(|f| f.exists())
252 }
253
254 fn cache_repos(&self, repos: &[Repo]) {
256 if let Some(file) = &self.cache_file {
257 if !file.exists() {
258 if let Some(parent) = &file.parent() {
259 create_dir_all(parent)
260 .expect("Could not create cache directory.")
261 }
262 }
263 let mut f =
264 File::create(file).expect("Could not create cache file.");
265 for repo in repos.iter() {
266 match writeln!(f, "{}", repo.path()) {
267 Ok(_) => (),
268 Err(e) => panic!("Problem writing cache file: {}", e),
269 }
270 }
271 }
272 }
273
274 fn get_cached_repos(&self) -> Vec<Repo> {
276 let mut repos = Vec::new();
277 if let Some(file) = &self.cache_file {
278 if file.exists() {
279 let f = File::open(file).expect("Could not open cache file.");
280 let reader = BufReader::new(f);
281 for repo_path in reader.lines().map_while(Result::ok) {
282 if !Path::new(&repo_path).exists() {
283 continue;
284 }
285 repos.push(Repo::new(repo_path))
286 }
287 }
288 }
289 repos
290 }
291}