use std::env;
use std::fs::{File, create_dir_all, remove_file};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use directories::{ProjectDirs, UserDirs};
use walkdir::{DirEntry, WalkDir};
use crate::repo::Repo;
const QUALIFIER: &str = "";
const ORGANIZATION: &str = "peap";
const APPLICATION: &str = "git-global";
const CACHE_FILE: &str = "repos.txt";
const DEFAULT_CMD: &str = "status";
const DEFAULT_FOLLOW_SYMLINKS: bool = true;
const DEFAULT_SAME_FILESYSTEM: bool = cfg!(any(unix, windows));
const DEFAULT_VERBOSE: bool = false;
const DEFAULT_SHOW_UNTRACKED: bool = true;
const SETTING_BASEDIR: &str = "global.basedir";
const SETTING_FOLLOW_SYMLINKS: &str = "global.follow-symlinks";
const SETTING_SAME_FILESYSTEM: &str = "global.same-filesystem";
const SETTING_IGNORE: &str = "global.ignore";
const SETTING_DEFAULT_CMD: &str = "global.default-cmd";
const SETTING_SHOW_UNTRACKED: &str = "global.show-untracked";
const SETTING_VERBOSE: &str = "global.verbose";
pub struct Config {
pub basedir: PathBuf,
pub follow_symlinks: bool,
pub same_filesystem: bool,
pub ignored_patterns: Vec<String>,
pub default_cmd: String,
pub verbose: bool,
pub show_untracked: bool,
pub cache_file: Option<PathBuf>,
pub manpage_file: Option<PathBuf>,
}
impl Default for Config {
fn default() -> Self {
Config::new()
}
}
impl Config {
pub fn new() -> Self {
let homedir = UserDirs::new()
.expect("Could not determine home directory.")
.home_dir()
.to_path_buf();
let cache_file =
ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
.map(|project_dirs| project_dirs.cache_dir().join(CACHE_FILE));
let manpage_file = match env::consts::OS {
"linux" => Some(PathBuf::from("/usr/share/man/man1/git-global.1")),
"macos" => Some(PathBuf::from("/usr/share/man/man1/git-global.1")),
"windows" => env::var("MSYSTEM").ok().and_then(|val| {
(val == "MINGW64").then(|| {
PathBuf::from("/mingw64/share/doc/git-doc/git-global.html")
})
}),
_ => None,
};
match ::git2::Config::open_default() {
Ok(cfg) => Config {
basedir: cfg.get_path(SETTING_BASEDIR).unwrap_or(homedir),
follow_symlinks: cfg
.get_bool(SETTING_FOLLOW_SYMLINKS)
.unwrap_or(DEFAULT_FOLLOW_SYMLINKS),
same_filesystem: cfg
.get_bool(SETTING_SAME_FILESYSTEM)
.unwrap_or(DEFAULT_SAME_FILESYSTEM),
ignored_patterns: cfg
.get_string(SETTING_IGNORE)
.unwrap_or_default()
.split(',')
.map(|p| p.trim().to_string())
.collect(),
default_cmd: cfg
.get_string(SETTING_DEFAULT_CMD)
.unwrap_or_else(|_| String::from(DEFAULT_CMD)),
verbose: cfg
.get_bool(SETTING_VERBOSE)
.unwrap_or(DEFAULT_VERBOSE),
show_untracked: cfg
.get_bool(SETTING_SHOW_UNTRACKED)
.unwrap_or(DEFAULT_SHOW_UNTRACKED),
cache_file,
manpage_file,
},
Err(_) => {
Config {
basedir: homedir,
follow_symlinks: DEFAULT_FOLLOW_SYMLINKS,
same_filesystem: DEFAULT_SAME_FILESYSTEM,
ignored_patterns: vec![],
default_cmd: String::from(DEFAULT_CMD),
verbose: DEFAULT_VERBOSE,
show_untracked: DEFAULT_SHOW_UNTRACKED,
cache_file,
manpage_file,
}
}
}
}
pub fn get_repos(&mut self) -> Vec<Repo> {
if !self.has_cache() {
let repos = self.find_repos();
self.cache_repos(&repos);
}
self.get_cached_repos()
}
pub fn clear_cache(&mut self) {
if self.has_cache()
&& let Some(file) = &self.cache_file
{
remove_file(file).expect("Failed to delete cache file.");
}
}
fn filter(&self, entry: &DirEntry) -> bool {
if let Some(entry_path) = entry.path().to_str() {
self.ignored_patterns
.iter()
.filter(|p| !p.is_empty())
.all(|pattern| !entry_path.contains(pattern))
} else {
false
}
}
fn find_repos(&self) -> Vec<Repo> {
let mut repos = Vec::new();
println!(
"Scanning for git repos under {}; this may take a while...",
self.basedir.display()
);
let mut n_dirs = 0;
let walker = WalkDir::new(&self.basedir)
.follow_links(self.follow_symlinks)
.same_file_system(self.same_filesystem);
for entry in walker
.into_iter()
.filter_entry(|e| self.filter(e))
.flatten()
{
if entry.file_type().is_dir() {
n_dirs += 1;
if entry.file_name() == ".git" {
let parent_path = entry
.path()
.parent()
.expect("Could not determine parent.");
if git2::Repository::open(parent_path).is_ok()
&& let Some(path) = parent_path.to_str()
{
repos.push(Repo::new(path.to_string()));
}
}
if self.verbose
&& let Some(size) = termsize::get()
{
let prefix = format!(
"\r... found {} repos; scanning directory #{}: ",
repos.len(),
n_dirs
);
let width = size.cols as usize - prefix.len() - 1;
let mut cur_path =
String::from(entry.path().to_str().unwrap());
let byte_width = match cur_path.char_indices().nth(width) {
None => &cur_path,
Some((idx, _)) => &cur_path[..idx],
}
.len();
cur_path.truncate(byte_width);
print!("{}{:<width$}", prefix, cur_path);
}
}
}
if self.verbose {
println!();
}
repos.sort_by_key(|r| r.path());
repos
}
fn has_cache(&self) -> bool {
self.cache_file.as_ref().is_some_and(|f| f.exists())
}
fn cache_repos(&self, repos: &[Repo]) {
if let Some(file) = &self.cache_file {
if !file.exists()
&& let Some(parent) = &file.parent()
{
create_dir_all(parent)
.expect("Could not create cache directory.")
}
let mut f =
File::create(file).expect("Could not create cache file.");
for repo in repos.iter() {
match writeln!(f, "{}", repo.path()) {
Ok(_) => (),
Err(e) => panic!("Problem writing cache file: {}", e),
}
}
}
}
fn get_cached_repos(&self) -> Vec<Repo> {
let mut repos = Vec::new();
if let Some(file) = &self.cache_file
&& file.exists()
{
let f = File::open(file).expect("Could not open cache file.");
let reader = BufReader::new(f);
for repo_path in reader.lines().map_while(Result::ok) {
if !Path::new(&repo_path).exists() {
continue;
}
repos.push(Repo::new(repo_path))
}
}
repos
}
pub fn add_ignore_pattern(pattern: &str) -> Result<(), String> {
let mut cfg = git2::Config::open_default()
.map_err(|e| format!("Could not open git config: {}", e))?;
let current = cfg.get_string(SETTING_IGNORE).unwrap_or_default();
let patterns: Vec<&str> = current
.split(',')
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.collect();
if patterns.contains(&pattern) {
return Err(format!("'{}' is already in global.ignore", pattern));
}
let new_value = if current.is_empty() {
pattern.to_string()
} else {
format!("{},{}", current, pattern)
};
cfg.set_str(SETTING_IGNORE, &new_value)
.map_err(|e| format!("Could not update git config: {}", e))?;
Ok(())
}
}