1pub mod cli;
2mod dotrc;
3
4use crate::{
5 common::{self, util, AbsolutePath, Platform},
6 verbose_println,
7};
8use derive_getters::Getters;
9use derive_more::From;
10use failure::Fail;
11use gethostname::gethostname;
12use globset::Glob;
13use lazy_static::lazy_static;
14use std::{
15 collections::HashSet,
16 ffi::OsStr,
17 path::{Path, PathBuf},
18 str::FromStr,
19};
20use walkdir::WalkDir;
21
22const DEFAULT_DOTFILES_DIR: &str = ".dotfiles";
23lazy_static! {
24 static ref DOTRC_NAMES: [&'static OsStr; 3] = [
25 OsStr::new(".dotrc"),
26 OsStr::new(".dotrc.yml"),
27 OsStr::new(".dotrc.yaml")
28 ];
29}
30
31#[derive(Debug, Getters)]
33pub struct Config {
34 excludes: Vec<AbsolutePath>,
35 tags: Vec<String>,
36 dotfiles_path: AbsolutePath,
37 hostname: String,
38 platform: Platform,
39 command: cli::Command,
40}
41
42#[derive(Debug)]
43enum PartialSource {
44 Cli,
45 Default,
46}
47
48#[derive(Debug)]
52struct PartialConfig {
53 excludes: Vec<PathBuf>,
54 tags: Vec<String>,
55 dotfiles_path: (PathBuf, PartialSource),
56 hostname: (String, PartialSource),
57 platform: (Platform, PartialSource),
58 command: cli::Command,
59}
60
61impl PartialConfig {
62 fn merge(cli: cli::Config, default: DefaultConfig) -> Self {
63 let excludes = util::append_vecs(cli.excludes, default.excludes);
64 let tags = util::append_vecs(cli.tags, default.tags);
65
66 macro_rules! merge_with_source {
67 ($field: ident) => {
68 match cli.$field {
69 Some($field) => ($field, PartialSource::Cli),
70 None => (default.$field, PartialSource::Default),
71 }
72 };
73 }
74 let dotfiles_path = merge_with_source!(dotfiles_path);
75 let hostname = merge_with_source!(hostname);
76 let platform = merge_with_source!(platform);
77
78 let command = cli.command;
79
80 PartialConfig {
81 excludes,
82 tags,
83 dotfiles_path,
84 hostname,
85 platform,
86 command,
87 }
88 }
89
90 fn to_config(&self) -> Result<Config, walkdir::Error> {
91 let dotfiles_path = {
92 let (dotfiles_path, _) = &self.dotfiles_path;
93
94 AbsolutePath::from(dotfiles_path.clone())
95 };
96
97 let excludes = self
98 .excludes
99 .iter()
100 .map(|exclude| expand_glob(exclude, &dotfiles_path))
102 .collect::<Result<Vec<_>, _>>()?
103 .into_iter()
104 .flatten()
105 .map(|exclude| AbsolutePath::from(dotfiles_path.join(exclude)))
107 .collect();
108
109 let tags = self.tags.clone();
110 let hostname = self.hostname.0.clone();
111 let platform = self.platform.0;
112 let command = self.command;
113
114 Ok(Config {
115 excludes,
116 tags,
117 dotfiles_path,
118 hostname,
119 platform,
120 command,
121 })
122 }
123}
124
125struct DefaultConfig {
126 excludes: Vec<PathBuf>,
127 tags: Vec<String>,
128 dotfiles_path: PathBuf,
129 hostname: String,
130 platform: Platform,
131}
132
133impl DefaultConfig {
134 fn get() -> Result<DefaultConfig, Error> {
137 let excludes = vec![];
138 let tags = vec![];
139
140 let dotfiles_path = util::home_dir().join(DEFAULT_DOTFILES_DIR);
141
142 let hostname = gethostname().to_str().ok_or(NoSystemHostname)?.to_owned();
143
144 let platform = util::platform();
145
146 Ok(DefaultConfig {
147 excludes,
148 tags,
149 dotfiles_path,
150 hostname,
151 platform,
152 })
153 }
154}
155
156fn expand_glob(path: &Path, dotfiles_path: &AbsolutePath) -> Result<Vec<PathBuf>, walkdir::Error> {
160 let mut had_glob_output = false;
162 let mut glob_output = || {
163 if !had_glob_output {
164 had_glob_output = true;
165 verbose_println!();
166 }
167 };
168
169 let glob = match path.to_str().map(Glob::new) {
170 Some(Ok(glob)) => glob.compile_matcher(),
171 None | Some(Err(_)) => {
172 glob_output();
173 verbose_println!("Could not glob-expand {}", path.display());
174 return Ok(vec![PathBuf::from(path)]);
175 },
176 };
177
178 let entries: Vec<walkdir::DirEntry> = WalkDir::new(dotfiles_path)
179 .follow_links(true)
180 .into_iter()
181 .collect::<Result<_, _>>()?;
182
183 let expanded_paths: Vec<_> = entries
184 .into_iter()
185 .filter_map(|entry| {
186 let entry_path = entry
187 .path()
188 .strip_prefix(dotfiles_path)
189 .expect("Entry should be in the dotfiles path");
190
191 if glob.is_match(entry_path) {
192 Some(PathBuf::from(entry_path))
193 } else {
194 None
195 }
196 })
197 .collect();
198
199 match &expanded_paths.as_slice() {
201 [expanded_path] if expanded_path == path => (),
202 _ => {
203 glob_output();
204 verbose_println!("Glob-expanded {} to:", path.display());
205 for expanded_path in &expanded_paths {
206 verbose_println!("\t- {}", expanded_path.display())
207 }
208 },
209 }
210
211 Ok(expanded_paths)
212}
213
214fn merge_dotrc(
217 partial_config: PartialConfig,
218 dotrc_config: dotrc::Config,
219) -> Result<Config, Error> {
220 fn merge_hierarchy<T>(partial: (T, PartialSource), dotrc: Option<T>) -> T {
227 match (partial, dotrc) {
228 ((x, PartialSource::Cli), _) => x,
229 (_, Some(x)) => x,
230 ((x, PartialSource::Default), None) => x,
231 }
232 }
233
234 fn expand_tilde(path: String) -> Option<String> {
239 Some(
240 if path.starts_with('~') {
241 path.replacen("~", util::home_dir().to_str()?, 1)
242 } else {
243 path
244 },
245 )
246 }
247
248 let dotfiles_path = AbsolutePath::from(merge_hierarchy(
249 partial_config.dotfiles_path,
250 dotrc_config
251 .dotfiles_path
252 .and_then(expand_tilde)
253 .map(PathBuf::from),
254 ));
255
256 let excludes = {
257 let mut excludes: Vec<AbsolutePath> =
258 util::append_vecs(
260 partial_config.excludes,
261 dotrc_config
264 .excludes
265 .unwrap_or_else(|| vec![])
266 .iter()
267 .map(PathBuf::from)
268 .collect(),
269 )
270 .into_iter()
271 .map(|path| expand_glob(&path, &dotfiles_path))
273 .collect::<Result<Vec<Vec<_>>, _>>()?
275 .into_iter()
277 .flatten()
278 .map(|exclude| AbsolutePath::from(dotfiles_path.join(exclude)))
281 .collect();
282
283 let set: HashSet<_> = excludes.drain(..).collect();
285 excludes.extend(set.into_iter());
286
287 excludes
288 };
289
290 let tags = util::append_vecs(
291 partial_config.tags,
292 dotrc_config.tags.unwrap_or_else(|| vec![]),
293 );
294
295 let hostname = merge_hierarchy(partial_config.hostname, dotrc_config.hostname);
296
297 let platform = match (partial_config.platform, dotrc_config.platform) {
298 ((platform, PartialSource::Cli), _) => platform,
299 (_, Some(platform)) => Platform::from_str(&platform)?,
300 ((platform, PartialSource::Default), None) => platform,
301 };
302
303 let command = partial_config.command;
304
305 Ok(Config {
306 excludes,
307 tags,
308 dotfiles_path,
309 hostname,
310 platform,
311 command,
312 })
313}
314
315fn find_dotrc(partial_config: &PartialConfig) -> Option<AbsolutePath> {
324 let config = partial_config.to_config().ok()?;
325
326 let items = crate::resolver::get(&config).ok()?;
328 for item in items {
329 match item.dest().file_name() {
330 Some(name) if DOTRC_NAMES.contains(&name) => {
331 verbose_println!("Discovered dotrc at {}", item.source());
332 return Some(item.source().clone());
333 },
334 _ => (),
335 }
336 }
337
338 for dotrc_name in DOTRC_NAMES.iter() {
340 let dotrc_path = util::home_dir().join(dotrc_name);
341 if dotrc_path.exists() {
342 return Some(AbsolutePath::from(dotrc_path));
343 }
344 }
345
346 None
347}
348
349pub fn get() -> Result<Config, Error> {
353 let partial_config = PartialConfig::merge(cli::Config::get(), DefaultConfig::get()?);
354 let dotrc_config = dotrc::get(find_dotrc(&partial_config))?;
355 let config = merge_dotrc(partial_config, dotrc_config)?;
356
357 Ok(config)
358}
359
360#[derive(Fail, Debug, From)]
361pub enum Error {
362 #[fail(display = "error reading system hostname")]
363 NoSystemHostname,
364
365 #[fail(display = "error reading file or directory ({})", _0)]
366 WalkdirError(#[fail(cause)] walkdir::Error),
367
368 #[fail(display = "{}", _0)]
369 DotrcError(#[fail(cause)] dotrc::Error),
370
371 #[fail(display = "{}", _0)]
372 InvalidPlatform(#[fail(cause)] common::PlatformParseError),
373}
374use Error::*;