pub mod cli;
mod dotrc;
use crate::{
common::{util, AbsolutePath},
verbose_println,
};
use derive_getters::Getters;
use derive_more::From;
use failure::Fail;
use gethostname::gethostname;
use globset::Glob;
use lazy_static::lazy_static;
use std::{
collections::HashSet,
ffi::OsStr,
path::{Path, PathBuf},
};
use walkdir::WalkDir;
const DEFAULT_DOTFILES_DIR: &str = ".dotfiles";
lazy_static! {
static ref DOTRC_NAMES: [&'static OsStr; 3] = [
OsStr::new(".dotrc"),
OsStr::new(".dotrc.yml"),
OsStr::new(".dotrc.yaml")
];
}
#[derive(Debug, Getters)]
pub struct Config {
excludes: Vec<AbsolutePath>,
tags: Vec<String>,
dotfiles_path: AbsolutePath,
hostname: String,
command: cli::Command,
}
#[derive(Debug)]
enum PartialSource {
Cli,
Default,
}
#[derive(Debug)]
struct PartialConfig {
excludes: Vec<PathBuf>,
tags: Vec<String>,
dotfiles_path: (PathBuf, PartialSource),
hostname: (String, PartialSource),
command: cli::Command,
}
impl PartialConfig {
fn merge(cli: cli::Config, default: DefaultConfig) -> Self {
let excludes = util::append_vecs(cli.excludes, default.excludes);
let tags = util::append_vecs(cli.tags, default.tags);
let dotfiles_path = match cli.dotfiles_path {
Some(dotfiles_path) => (dotfiles_path, PartialSource::Cli),
None => (default.dotfiles_path, PartialSource::Default),
};
let hostname = match cli.hostname {
Some(hostname) => (hostname, PartialSource::Cli),
None => (default.hostname, PartialSource::Default),
};
let command = cli.command;
PartialConfig {
excludes,
tags,
dotfiles_path,
hostname,
command,
}
}
fn to_config(&self) -> Result<Config, walkdir::Error> {
let dotfiles_path = {
let (dotfiles_path, _) = &self.dotfiles_path;
AbsolutePath::from(dotfiles_path.clone())
};
let excludes = self
.excludes
.iter()
.map(|exclude| expand_glob(exclude, &dotfiles_path))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.map(|exclude| AbsolutePath::from(exclude.clone()))
.collect();
let tags = self.tags.clone();
let hostname = {
let (hostname, _) = &self.hostname;
hostname.clone()
};
let command = self.command.clone();
Ok(Config {
excludes,
tags,
dotfiles_path,
hostname,
command,
})
}
}
struct DefaultConfig {
excludes: Vec<PathBuf>,
tags: Vec<String>,
dotfiles_path: PathBuf,
hostname: String,
}
impl DefaultConfig {
fn get() -> Result<DefaultConfig, Error> {
let excludes = vec![];
let tags = vec![];
let dotfiles_path = util::home_dir().join(DEFAULT_DOTFILES_DIR);
let hostname = gethostname().to_str().ok_or(NoSystemHostname)?.to_owned();
Ok(DefaultConfig {
excludes,
tags,
dotfiles_path,
hostname,
})
}
}
fn expand_glob(path: &Path, dotfiles_path: &AbsolutePath) -> Result<Vec<PathBuf>, walkdir::Error> {
let mut had_glob_output = false;
let mut glob_output = || {
if !had_glob_output {
had_glob_output = true;
verbose_println!();
}
};
let glob = match path.to_str().map(Glob::new) {
Some(Ok(glob)) => glob.compile_matcher(),
None | Some(Err(_)) => {
glob_output();
verbose_println!("Could not glob-expand {}", path.display());
return Ok(vec![PathBuf::from(path)]);
},
};
let entries: Vec<walkdir::DirEntry> = WalkDir::new(dotfiles_path)
.follow_links(true)
.into_iter()
.collect::<Result<_, _>>()?;
let expanded_paths: Vec<_> = entries
.into_iter()
.filter_map(|entry| {
let entry_path = entry
.path()
.strip_prefix(dotfiles_path)
.expect("Entry should be in the dotfiles path");
if glob.is_match(entry_path) {
Some(PathBuf::from(entry_path))
} else {
None
}
})
.collect();
match &expanded_paths.as_slice() {
[expanded_path] if expanded_path == path => (),
_ => {
glob_output();
verbose_println!("Glob-expanded {} to:", path.display());
for expanded_path in &expanded_paths {
verbose_println!("\t- {}", expanded_path.display())
}
},
}
Ok(expanded_paths)
}
fn merge_dotrc(
partial_config: PartialConfig,
dotrc_config: dotrc::Config,
) -> Result<Config, Error> {
fn merge_hierarchy<T>(partial: (T, PartialSource), dotrc: Option<T>) -> T {
match partial {
(x, PartialSource::Cli) => x,
(x, PartialSource::Default) => dotrc.unwrap_or(x),
}
}
fn expand_tilde(path: String) -> Option<String> {
Some(
if path.starts_with('~') {
path.replacen("~", util::home_dir().to_str()?, 1)
} else {
path
},
)
}
let dotfiles_path = {
let dotrc_dotfiles_path = dotrc_config
.dotfiles_path
.and_then(expand_tilde)
.map(PathBuf::from);
AbsolutePath::from(merge_hierarchy(
partial_config.dotfiles_path,
dotrc_dotfiles_path,
))
};
let excludes = {
let mut excludes: Vec<AbsolutePath> =
util::append_vecs(
partial_config.excludes,
dotrc_config
.excludes
.unwrap_or_else(|| vec![])
.iter()
.map(PathBuf::from)
.collect(),
)
.into_iter()
.map(|path| expand_glob(&path, &dotfiles_path))
.collect::<Result<Vec<Vec<_>>, _>>()?
.into_iter()
.flatten()
.map(|exclude| AbsolutePath::from(dotfiles_path.join(exclude)))
.collect();
let set: HashSet<_> = excludes.drain(..).collect();
excludes.extend(set.into_iter());
excludes
};
let tags = util::append_vecs(
partial_config.tags,
dotrc_config.tags.unwrap_or_else(|| vec![]),
);
let hostname = merge_hierarchy(partial_config.hostname, dotrc_config.hostname);
let command = partial_config.command;
Ok(Config {
excludes,
tags,
dotfiles_path,
hostname,
command,
})
}
fn find_dotrc(partial_config: &PartialConfig) -> Option<AbsolutePath> {
let config = partial_config.to_config().ok()?;
let items = crate::resolver::get(&config).ok()?;
for item in items {
match item.dest().file_name() {
Some(name) if DOTRC_NAMES.contains(&name) => {
verbose_println!("Discovered dotrc at {}", item.source());
return Some(item.source().clone());
},
_ => (),
}
}
for dotrc_name in DOTRC_NAMES.iter() {
let dotrc_path = util::home_dir().join(dotrc_name);
if dotrc_path.exists() {
return Some(AbsolutePath::from(dotrc_path));
}
}
None
}
pub fn get() -> Result<Config, Error> {
let partial_config = PartialConfig::merge(cli::Config::get(), DefaultConfig::get()?);
let dotrc_config = dotrc::get(find_dotrc(&partial_config))?;
let config = merge_dotrc(partial_config, dotrc_config)?;
Ok(config)
}
#[derive(Fail, Debug, From)]
pub enum Error {
#[fail(display = "error reading system hostname")]
NoSystemHostname,
#[fail(display = "error reading file or directory ({})", _0)]
WalkdirError(#[fail(cause)] walkdir::Error),
#[fail(display = "{}", _0)]
DotrcError(#[fail(cause)] dotrc::Error),
}
use self::Error::*;