1#![warn(missing_docs)]
27#![forbid(unsafe_code)]
28pub mod config_params;
29pub mod configuration;
30pub mod error;
31
32pub use config_params::XvcLoadParams;
33pub use configuration::blank_optional_config;
34pub use configuration::default_config;
35pub use configuration::initial_xvc_configuration_file;
36pub use configuration::XvcConfiguration;
37pub use configuration::XvcOptionalConfiguration;
38
39use directories_next::{BaseDirs, ProjectDirs, UserDirs};
40use lazy_static::lazy_static;
41use serde::{Deserialize, Serialize};
42use std::{
43 collections::HashMap,
44 fmt,
45 path::{Path, PathBuf},
46 str::FromStr,
47};
48use xvc_walker::AbsolutePath;
49
50use strum_macros::{Display as EnumDisplay, EnumString, IntoStaticStr};
51
52use crate::error::{Error, Result};
53use toml::Value as TomlValue;
54
55lazy_static! {
56 pub static ref SYSTEM_CONFIG_DIRS: Option<ProjectDirs> =
59 ProjectDirs::from("com", "emresult", "xvc");
60
61 pub static ref USER_CONFIG_DIRS: Option<BaseDirs> = BaseDirs::new();
64
65 pub static ref USER_DIRS: Option<UserDirs> = UserDirs::new();
68}
69
70#[derive(
72 Debug, Copy, Clone, EnumString, EnumDisplay, IntoStaticStr, Serialize, Deserialize, PartialEq,
73)]
74#[strum(serialize_all = "lowercase")]
75pub enum XvcConfigOptionSource {
76 Default,
78 System,
80 Global,
82 Project,
84 Local,
86 CommandLine,
88 Environment,
90 Runtime,
92}
93
94#[derive(Debug, Copy, Clone)]
96pub struct XvcConfigOption<T> {
97 pub source: XvcConfigOptionSource,
99 pub option: T,
101}
102
103#[derive(Debug, Copy, Clone, EnumString, EnumDisplay, IntoStaticStr)]
105pub enum XvcVerbosity {
106 #[strum(serialize = "quiet", serialize = "0")]
108 Quiet,
109 #[strum(serialize = "default", serialize = "error", serialize = "1")]
111 Default,
112 #[strum(serialize = "warn", serialize = "2")]
114 Warn,
115 #[strum(serialize = "info", serialize = "3")]
117 Info,
118 #[strum(serialize = "debug", serialize = "4")]
120 Debug,
121 #[strum(serialize = "trace", serialize = "5")]
123 Trace,
124}
125
126impl From<u8> for XvcVerbosity {
127 fn from(v: u8) -> Self {
128 match v {
129 0 => Self::Quiet,
130 1 => Self::Default,
131 2 => Self::Warn,
132 3 => Self::Info,
133 4 => Self::Debug,
134 _ => Self::Trace,
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct XvcConfigValue {
142 pub source: XvcConfigOptionSource,
144 pub value: TomlValue,
146}
147
148impl XvcConfigValue {
149 pub fn new(source: XvcConfigOptionSource, value: TomlValue) -> Self {
151 Self { source, value }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct XvcConfigMap {
158 pub source: XvcConfigOptionSource,
160 pub map: HashMap<String, TomlValue>,
162}
163
164#[derive(Debug, Clone)]
170pub struct XvcConfig {
171 pub current_dir: AbsolutePath,
173 system_config: XvcOptionalConfiguration,
175 user_config: XvcOptionalConfiguration,
177 project_config: XvcOptionalConfiguration,
179 local_config: XvcOptionalConfiguration,
181 command_line_config: XvcOptionalConfiguration,
183 environment_config: XvcOptionalConfiguration,
185 runtime_config: XvcOptionalConfiguration,
187 the_config: XvcConfiguration,
189}
190
191impl fmt::Display for XvcConfig {
192 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
193 writeln!(f, "\nCurrent Configuration")?;
194 writeln!(f, "current_dir: {}", self.current_dir)?;
195 writeln!(f, "{}", &self.the_config)?;
196 writeln!(f)
197 }
198}
199
200impl XvcConfig {
201 pub fn new_v2(config_init_params: &XvcLoadParams) -> Result<Self> {
205 let default_conf = default_config();
206
207 let system_config = if config_init_params.include_system_config {
208 Self::system_config_file()
209 .and_then(|path| Self::load_optional_config_from_file(&path))
210 .unwrap_or(blank_optional_config())
211 } else {
212 blank_optional_config()
213 };
214
215 let user_config = if config_init_params.include_user_config {
216 Self::user_config_file()
217 .and_then(|path| Self::load_optional_config_from_file(&path))
218 .unwrap_or(blank_optional_config())
219 } else {
220 blank_optional_config()
221 };
222
223 let mut project_config = if config_init_params.include_project_config {
224 if let Some(ref config_path) = config_init_params.project_config_path {
225 Self::load_optional_config_from_file(config_path)?
226 } else {
227 blank_optional_config()
228 }
229 } else {
230 blank_optional_config()
231 };
232
233 if let Some(ref config_path) = config_init_params.project_config_path {
234 if let Some(ref mut core) = project_config.core {
235 if let Some(guid) = core.guid.take() {
236 Self::migrate_config_to_07(config_path, guid)?;
237 }
238 }
239 }
240
241 let local_config = if config_init_params.include_local_config {
242 if let Some(ref config_path) = config_init_params.local_config_path {
243 Self::load_optional_config_from_file(config_path)?
244 } else {
245 blank_optional_config()
246 }
247 } else {
248 blank_optional_config()
249 };
250
251 let environment_config = if config_init_params.include_environment_config {
252 XvcOptionalConfiguration::from_env()
253 } else {
254 blank_optional_config()
255 };
256
257 let command_line_config =
258 Self::load_command_line_config(&config_init_params.command_line_config)?;
259
260 let runtime_config = blank_optional_config();
261
262 let the_config = default_conf
263 .merge_with_optional(&system_config)
264 .merge_with_optional(&user_config)
265 .merge_with_optional(&project_config)
266 .merge_with_optional(&local_config)
267 .merge_with_optional(&environment_config)
268 .merge_with_optional(&command_line_config)
269 .merge_with_optional(&runtime_config);
270
271 Ok(XvcConfig {
272 current_dir: config_init_params.current_dir.clone(),
273 system_config,
274 user_config,
275 project_config,
276 local_config,
277 command_line_config,
278 environment_config,
279 runtime_config,
280 the_config,
281 })
282 }
283
284 pub fn config(&self) -> &XvcConfiguration {
286 &self.the_config
287 }
288
289 pub fn system_config_file() -> Result<PathBuf> {
292 Ok(SYSTEM_CONFIG_DIRS
293 .to_owned()
294 .ok_or(Error::CannotDetermineSystemConfigurationPath)?
295 .config_dir()
296 .to_path_buf())
297 }
298
299 pub fn user_config_file() -> Result<PathBuf> {
301 Ok(USER_CONFIG_DIRS
302 .to_owned()
303 .ok_or(Error::CannotDetermineUserConfigurationPath)?
304 .config_dir()
305 .join("xvc"))
306 }
307
308 pub fn load_optional_config_from_file(path: &Path) -> Result<XvcOptionalConfiguration> {
310 if path.exists() {
311 let opt_config = XvcOptionalConfiguration::from_file(path)?;
312 Ok(opt_config)
313 } else {
314 Ok(blank_optional_config())
315 }
316 }
317
318 fn migrate_config_to_07(config_path: &Path, guid: String) -> Result<()> {
319 if let Some(xvc_dir) = config_path.parent() {
320 let guid_path = xvc_dir.join("guid");
321 if !guid_path.exists() {
322 std::fs::write(&guid_path, &guid).map_err(|e| Error::IoError { source: e })?;
323 }
324
325 if let Some(project_root) = xvc_dir.parent() {
326 let gitignore_path = project_root.join(".gitignore");
327 if gitignore_path.exists() {
328 let content = std::fs::read_to_string(&gitignore_path)
329 .map_err(|e| Error::IoError { source: e })?;
330 let mut new_content = content.clone();
331 let mut changed = false;
332
333 if !content.contains("!.xvc/guid") {
334 new_content.push_str("\n!.xvc/guid");
335 changed = true;
336 }
337
338 if !content.contains("!.xvc/pipelines/") {
339 new_content.push_str("\n!.xvc/pipelines/");
340 changed = true;
341 }
342
343 if changed {
344 std::fs::write(&gitignore_path, new_content)
345 .map_err(|e| Error::IoError { source: e })?;
346 }
347 }
348 }
349 }
350
351 let content =
352 std::fs::read_to_string(config_path).map_err(|e| Error::IoError { source: e })?;
353 let mut new_lines = Vec::new();
354 for line in content.lines() {
355 let trimmed = line.trim();
356 if !trimmed.starts_with("guid =") && !trimmed.starts_with("guid=") {
357 new_lines.push(line);
358 }
359 }
360 let mut new_content = new_lines.join("\n");
361 if content.ends_with('\n') {
362 new_content.push('\n');
363 }
364 std::fs::write(config_path, new_content).map_err(|e| Error::IoError { source: e })?;
365
366 Ok(())
367 }
368
369 pub fn load_command_line_config(
372 cli_opt_vector: &Option<Vec<String>>,
373 ) -> Result<XvcOptionalConfiguration> {
374 if let Some(cli_opts) = cli_opt_vector {
375 let cli_opts_hm = Self::parse_key_value_vector(cli_opts);
376 Ok(XvcOptionalConfiguration::from_hash_map("", &cli_opts_hm))
377 } else {
378 Ok(XvcOptionalConfiguration::default())
379 }
380 }
381
382 fn parse_key_value_vector(cli_opts: &Vec<String>) -> HashMap<String, String> {
384 cli_opts
385 .into_iter()
386 .map(|str| {
387 let elements: Vec<&str> = str.split('=').collect();
388 let key = elements[0].trim().to_owned();
389 let value = elements[1].trim().to_owned();
390 (key, value)
391 })
392 .collect()
393 }
394
395 pub fn current_dir(&self) -> Result<&AbsolutePath> {
399 let pb = &self.current_dir;
400 Ok(pb)
401 }
402
403 pub fn verbosity(&self) -> XvcVerbosity {
406 let verbosity_str = &self.the_config.core.verbosity;
407 match XvcVerbosity::from_str(verbosity_str) {
408 Ok(v) => v,
409 Err(source) => {
410 Error::StrumError { source }.warn();
411 XvcVerbosity::Default
412 }
413 }
414 }
415
416 pub fn find_value_source(&self, key: &str) -> Option<XvcConfigOptionSource> {
418 let layers = [
419 (XvcConfigOptionSource::Runtime, &self.runtime_config),
420 (
421 XvcConfigOptionSource::CommandLine,
422 &self.command_line_config,
423 ),
424 (XvcConfigOptionSource::Environment, &self.environment_config),
425 (XvcConfigOptionSource::Local, &self.local_config),
426 (XvcConfigOptionSource::Project, &self.project_config),
427 (XvcConfigOptionSource::Global, &self.user_config), (XvcConfigOptionSource::System, &self.system_config),
429 ];
430
431 for (source, config) in &layers {
432 if self.key_exists_in_optional_config(config, key) {
433 return Some(*source);
434 }
435 }
436
437 if self.is_valid_key(key) {
438 Some(XvcConfigOptionSource::Default)
439 } else {
440 None
441 }
442 }
443
444 fn key_exists_in_optional_config(&self, config: &XvcOptionalConfiguration, key: &str) -> bool {
447 let parts: Vec<&str> = key.split('.').collect();
448 match parts.as_slice() {
449 ["core", "xvc_repo_version"] => config
451 .core
452 .as_ref()
453 .is_some_and(|c| c.xvc_repo_version.is_some()),
454 ["core", "verbosity"] => config.core.as_ref().is_some_and(|c| c.verbosity.is_some()),
455 ["git", "use_git"] => config.git.as_ref().is_some_and(|c| c.use_git.is_some()),
457 ["git", "command"] => config.git.as_ref().is_some_and(|c| c.command.is_some()),
458 ["git", "auto_commit"] => config.git.as_ref().is_some_and(|c| c.auto_commit.is_some()),
459 ["git", "auto_stage"] => config.git.as_ref().is_some_and(|c| c.auto_stage.is_some()),
460 ["cache", "algorithm"] => config.cache.as_ref().is_some_and(|c| c.algorithm.is_some()),
462 ["file", "track", "no_commit"] => config
464 .file
465 .as_ref()
466 .and_then(|f| f.track.as_ref())
467 .is_some_and(|t| t.no_commit.is_some()),
468 ["file", "track", "force"] => config
469 .file
470 .as_ref()
471 .and_then(|f| f.track.as_ref())
472 .is_some_and(|t| t.force.is_some()),
473 ["file", "track", "text_or_binary"] => config
474 .file
475 .as_ref()
476 .and_then(|f| f.track.as_ref())
477 .is_some_and(|t| t.text_or_binary.is_some()),
478 ["file", "track", "no_parallel"] => config
479 .file
480 .as_ref()
481 .and_then(|f| f.track.as_ref())
482 .is_some_and(|t| t.no_parallel.is_some()),
483 ["file", "track", "include_git_files"] => config
484 .file
485 .as_ref()
486 .and_then(|f| f.track.as_ref())
487 .is_some_and(|t| t.include_git_files.is_some()),
488 ["file", "list", "format"] => config
490 .file
491 .as_ref()
492 .and_then(|f| f.list.as_ref())
493 .is_some_and(|l| l.format.is_some()),
494 ["file", "list", "sort"] => config
495 .file
496 .as_ref()
497 .and_then(|f| f.list.as_ref())
498 .is_some_and(|l| l.sort.is_some()),
499 ["file", "list", "show_dot_files"] => config
500 .file
501 .as_ref()
502 .and_then(|f| f.list.as_ref())
503 .is_some_and(|l| l.show_dot_files.is_some()),
504 ["file", "list", "no_summary"] => config
505 .file
506 .as_ref()
507 .and_then(|f| f.list.as_ref())
508 .is_some_and(|l| l.no_summary.is_some()),
509 ["file", "list", "recursive"] => config
510 .file
511 .as_ref()
512 .and_then(|f| f.list.as_ref())
513 .is_some_and(|l| l.recursive.is_some()),
514 ["file", "list", "include_git_files"] => config
515 .file
516 .as_ref()
517 .and_then(|f| f.list.as_ref())
518 .is_some_and(|l| l.include_git_files.is_some()),
519 ["file", "carry-in", "force"] => config
521 .file
522 .as_ref()
523 .and_then(|f| f.carry_in.as_ref())
524 .is_some_and(|c| c.force.is_some()),
525 ["file", "carry-in", "no_parallel"] => config
526 .file
527 .as_ref()
528 .and_then(|f| f.carry_in.as_ref())
529 .is_some_and(|c| c.no_parallel.is_some()),
530 ["file", "recheck", "method"] => config
532 .file
533 .as_ref()
534 .and_then(|f| f.recheck.as_ref())
535 .is_some_and(|r| r.method.is_some()),
536 ["pipeline", "current_pipeline"] => config
538 .pipeline
539 .as_ref()
540 .is_some_and(|p| p.current_pipeline.is_some()),
541 ["pipeline", "default"] => config
542 .pipeline
543 .as_ref()
544 .is_some_and(|p| p.default.is_some()),
545 ["pipeline", "default_params_file"] => config
546 .pipeline
547 .as_ref()
548 .is_some_and(|p| p.default_params_file.is_some()),
549 ["pipeline", "process_pool_size"] => config
550 .pipeline
551 .as_ref()
552 .is_some_and(|p| p.process_pool_size.is_some()),
553 ["check-ignore", "details"] => config
555 .check_ignore
556 .as_ref()
557 .is_some_and(|c| c.details.is_some()),
558 _ => false,
559 }
560 }
561
562 fn is_valid_key(&self, key: &str) -> bool {
565 let parts: Vec<&str> = key.split('.').collect();
566 matches!(
567 parts.as_slice(),
568 ["core", "xvc_repo_version"] |
570 ["core", "verbosity"] |
571 ["git", "use_git"] |
573 ["git", "command"] |
574 ["git", "auto_commit"] |
575 ["git", "auto_stage"] |
576 ["cache", "algorithm"] |
578 ["file", "track", "no_commit"] |
580 ["file", "track", "force"] |
581 ["file", "track", "text_or_binary"] |
582 ["file", "track", "no_parallel"] |
583 ["file", "track", "include_git_files"] |
584 ["file", "list", "format"] |
586 ["file", "list", "sort"] |
587 ["file", "list", "show_dot_files"] |
588 ["file", "list", "no_summary"] |
589 ["file", "list", "recursive"] |
590 ["file", "list", "include_git_files"] |
591 ["file", "carry-in", "force"] |
593 ["file", "carry-in", "no_parallel"] |
594 ["file", "recheck", "method"] |
596 ["pipeline", "current_pipeline"] |
598 ["pipeline", "default"] |
599 ["pipeline", "default_params_file"] |
600 ["pipeline", "process_pool_size"] |
601 ["check-ignore", "details"]
603 )
604 }
605}
606
607pub trait FromConfig {
611 fn from_config(conf: &XvcConfiguration) -> Result<Box<Self>>;
616}
617
618pub trait UpdateFromConfig {
623 fn update_from_config(self, conf: &XvcConfiguration) -> Result<Box<Self>>;
637}