xvc_config/
configuration.rs

1//! This module defines the core Xvc configuration structures and logic.
2//! It includes `XvcConfiguration` for complete settings, `XvcOptionalConfiguration` for partial overrides,
3//! and functions for merging configurations from various sources like default values, files, and environment variables.
4
5use std::collections::HashMap;
6use std::path::Path;
7
8use crate::Result;
9use derive_more::Display;
10use serde::{Deserialize, Serialize};
11use xvc_walker::AbsolutePath;
12
13/// Core configuration for Xvc.
14#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
15#[display("CoreConfig(xvc_repo_version: {xvc_repo_version}, verbosity: {verbosity})")]
16#[serde(deny_unknown_fields)]
17pub struct CoreConfig {
18    /// The Xvc repository version.
19    pub xvc_repo_version: u8,
20    /// The verbosity level for logging.
21    pub verbosity: String,
22}
23
24/// Git integration configuration for Xvc.
25#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
26#[display("GitConfig(use_git: {use_git}, command: {command}, auto_commit: {auto_commit}, auto_stage: {auto_stage})")]
27#[serde(deny_unknown_fields)]
28pub struct GitConfig {
29    /// Whether to use Git for version control.
30    pub use_git: bool,
31    /// The command to execute Git.
32    pub command: String,
33    /// Whether to automatically commit changes in the .xvc directory.
34    pub auto_commit: bool,
35    /// Whether to automatically stage changes in the .xvc directory.
36    pub auto_stage: bool,
37}
38
39/// Cache configuration for Xvc.
40#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
41#[display("CacheConfig(algorithm: {algorithm})")]
42#[serde(deny_unknown_fields)]
43pub struct CacheConfig {
44    /// The hashing algorithm used for caching.
45    pub algorithm: String,
46}
47
48/// Configuration for file tracking operations.
49#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
50#[display("FileTrackConfig(no_commit: {no_commit}, force: {force}, text_or_binary: {text_or_binary}, no_parallel: {no_parallel}, include_git_files: {include_git_files})")]
51#[serde(deny_unknown_fields)]
52pub struct FileTrackConfig {
53    /// Whether to skip committing changes after tracking.
54    pub no_commit: bool,
55    /// Whether to force tracking even if files are already tracked.
56    pub force: bool,
57    /// How to treat files: "auto", "text", or "binary".
58    pub text_or_binary: String,
59    /// Whether to disable parallel operations during tracking.
60    pub no_parallel: bool,
61    /// Whether to include Git-tracked files in Xvc tracking operations.
62    pub include_git_files: bool,
63}
64
65/// Configuration for file listing operations.
66#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
67#[display("FileListConfig(format: {format}, sort: {sort}, show_dot_files: {show_dot_files}, no_summary: {no_summary}, recursive: {recursive}, include_git_files: {include_git_files})")]
68#[serde(deny_unknown_fields)]
69pub struct FileListConfig {
70    /// The format string for displaying file list entries.
71    pub format: String,
72    /// The sorting order for the file list.
73    pub sort: String,
74    /// Whether to show dot files (e.g., .gitignore).
75    pub show_dot_files: bool,
76    /// Whether to suppress the summary row in the file list.
77    pub no_summary: bool,
78    /// Whether to list files recursively by default.
79    pub recursive: bool,
80    /// Whether to include Git-tracked files in Xvc listing operations.
81    pub include_git_files: bool,
82}
83
84/// Configuration for file carry-in operations.
85#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
86#[display("FileCarryInConfig(force: {force}, no_parallel: {no_parallel})")]
87#[serde(deny_unknown_fields)]
88pub struct FileCarryInConfig {
89    /// Whether to force carry-in even if files are already present in cache.
90    pub force: bool,
91    /// Whether to disable parallel operations during carry-in.
92    pub no_parallel: bool,
93}
94
95/// Configuration for file recheck operations.
96#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
97#[display("FileRecheckConfig(method: {method})")]
98#[serde(deny_unknown_fields)]
99pub struct FileRecheckConfig {
100    /// The method used for rechecking files (e.g., "copy", "hardlink", "symlink", "reflink").
101    pub method: String,
102}
103
104/// Comprehensive file-related configuration for Xvc.
105#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
106#[display("FileConfig(track: {track}, list: {list}, carry_in: {carry_in}, recheck: {recheck})")]
107#[serde(deny_unknown_fields)]
108pub struct FileConfig {
109    /// Configuration for `xvc file track`.
110    pub track: FileTrackConfig,
111    /// Configuration for `xvc file list`.
112    pub list: FileListConfig,
113    /// Configuration for `xvc file carry-in`.
114    #[serde(rename = "carry-in")]
115    pub carry_in: FileCarryInConfig,
116    /// Configuration for `xvc file recheck`.
117    pub recheck: FileRecheckConfig,
118}
119
120/// Configuration for pipeline operations.
121#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
122#[display("PipelineConfig(current_pipeline: {current_pipeline}, default: {default}, default_params_file: {default_params_file}, process_pool_size: {process_pool_size})")]
123#[serde(deny_unknown_fields)]
124pub struct PipelineConfig {
125    /// The name of the currently active pipeline.
126    pub current_pipeline: String,
127    /// The name of the default pipeline.
128    pub default: String,
129    /// The default parameters file name for pipelines.
130    pub default_params_file: String,
131    /// The number of command processes to run concurrently in a pipeline.
132    pub process_pool_size: u32,
133}
134
135/// Configuration for checking ignored files.
136#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
137#[display("CheckIgnoreConfig(details: {details})")]
138#[serde(deny_unknown_fields)]
139pub struct CheckIgnoreConfig {
140    /// Whether to show detailed information when checking ignored files.
141    pub details: bool,
142}
143
144/// The top-level Xvc configuration structure.
145#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize)]
146#[display("XvcConfiguration(core: {core}, git: {git}, cache: {cache}, file: {file}, pipeline: {pipeline}, check_ignore: {check_ignore})")]
147#[serde(deny_unknown_fields)]
148pub struct XvcConfiguration {
149    /// Core Xvc settings.
150    pub core: CoreConfig,
151    /// Git integration settings.
152    pub git: GitConfig,
153    /// Cache settings.
154    pub cache: CacheConfig,
155    /// File-related operation settings.
156    pub file: FileConfig,
157    /// Pipeline execution settings.
158    pub pipeline: PipelineConfig,
159    /// Check ignore settings.
160    #[serde(rename = "check-ignore")]
161    pub check_ignore: CheckIgnoreConfig,
162}
163
164/// Optional core configuration for Xvc, used for partial updates.
165#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
166#[display("OptionalCoreConfig(xvc_repo_version: {xvc_repo_version:?}, verbosity: {verbosity:?})")]
167#[serde(deny_unknown_fields)]
168pub struct OptionalCoreConfig {
169    /// Optional Xvc repository version.
170    pub xvc_repo_version: Option<u8>,
171    /// Optional verbosity level for logging.
172    pub verbosity: Option<String>,
173    /// Optional GUID for the repository.
174    /// This is a legacy field and should be migrated to .xvc/guid file.
175    pub guid: Option<String>,
176}
177
178/// Optional Git integration configuration for Xvc, used for partial updates.
179#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
180#[display("OptionalGitConfig(use_git: {use_git:?}, command: {command:?}, auto_commit: {auto_commit:?}, auto_stage: {auto_stage:?})")]
181#[serde(deny_unknown_fields)]
182pub struct OptionalGitConfig {
183    /// Optional setting for whether to use Git for version control.
184    pub use_git: Option<bool>,
185    /// Optional Git command to execute.
186    pub command: Option<String>,
187    /// Optional setting for whether to automatically commit changes.
188    pub auto_commit: Option<bool>,
189    /// Optional setting for whether to automatically stage changes.
190    pub auto_stage: Option<bool>,
191}
192
193/// Optional cache configuration for Xvc, used for partial updates.
194#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
195#[display("OptionalCacheConfig(algorithm: {algorithm:?})")]
196#[serde(deny_unknown_fields)]
197pub struct OptionalCacheConfig {
198    /// Optional hashing algorithm used for caching.
199    pub algorithm: Option<String>,
200}
201
202/// Optional configuration for file tracking operations, used for partial updates.
203#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
204#[display("OptionalFileTrackConfig(no_commit: {no_commit:?}, force: {force:?}, text_or_binary: {text_or_binary:?}, no_parallel: {no_parallel:?}, include_git_files: {include_git_files:?})")]
205#[serde(deny_unknown_fields)]
206pub struct OptionalFileTrackConfig {
207    /// Optional setting for whether to skip committing changes after tracking.
208    pub no_commit: Option<bool>,
209    /// Optional setting for whether to force tracking.
210    pub force: Option<bool>,
211    /// Optional setting for how to treat files: "auto", "text", or "binary".
212    pub text_or_binary: Option<String>,
213    /// Optional setting for whether to disable parallel operations during tracking.
214    pub no_parallel: Option<bool>,
215    /// Optional setting for whether to include Git-tracked files.
216    pub include_git_files: Option<bool>,
217}
218
219/// Optional configuration for file listing operations, used for partial updates.
220#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
221#[display("OptionalFileListConfig(format: {format:?}, sort: {sort:?}, show_dot_files: {show_dot_files:?}, no_summary: {no_summary:?}, recursive: {recursive:?}, include_git_files: {include_git_files:?})")]
222#[serde(deny_unknown_fields)]
223pub struct OptionalFileListConfig {
224    /// Optional format string for displaying file list entries.
225    pub format: Option<String>,
226    /// Optional sorting order for the file list.
227    pub sort: Option<String>,
228    /// Optional setting for whether to show dot files.
229    pub show_dot_files: Option<bool>,
230    /// Optional setting for whether to suppress the summary row.
231    pub no_summary: Option<bool>,
232    /// Optional setting for whether to list files recursively.
233    pub recursive: Option<bool>,
234    /// Optional setting for whether to include Git-tracked files in Xvc listing operations.
235    pub include_git_files: Option<bool>,
236}
237
238/// Optional configuration for file carry-in operations, used for partial updates.
239#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
240#[display("OptionalFileCarryInConfig(force: {force:?}, no_parallel: {no_parallel:?})")]
241#[serde(deny_unknown_fields)]
242pub struct OptionalFileCarryInConfig {
243    /// Optional setting for whether to force carry-in.
244    pub force: Option<bool>,
245    /// Optional setting for whether to disable parallel operations during carry-in.
246    pub no_parallel: Option<bool>,
247}
248
249/// Optional configuration for file recheck operations, used for partial updates.
250#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
251#[display("OptionalFileRecheckConfig(method: {method:?})")]
252#[serde(deny_unknown_fields)]
253pub struct OptionalFileRecheckConfig {
254    /// Optional recheck method for Xvc.
255    pub method: Option<String>,
256}
257
258/// Optional comprehensive file-related configuration for Xvc, used for partial updates.
259#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
260#[display("OptionalFileConfig(track: {track:?}, list: {list:?}, carry_in: {carry_in:?}, recheck: {recheck:?})")]
261#[serde(deny_unknown_fields)]
262pub struct OptionalFileConfig {
263    /// Optional configuration for `xvc file track`.
264    pub track: Option<OptionalFileTrackConfig>,
265    /// Optional configuration for `xvc file list`.
266    pub list: Option<OptionalFileListConfig>,
267    /// Optional configuration for `xvc file carry-in`.
268    #[serde(rename = "carry-in")]
269    pub carry_in: Option<OptionalFileCarryInConfig>,
270    /// Optional configuration for `xvc file recheck`.
271    pub recheck: Option<OptionalFileRecheckConfig>,
272}
273
274/// Optional configuration for pipeline operations, used for partial updates.
275#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
276#[display("OptionalPipelineConfig(current_pipeline: {current_pipeline:?}, default: {default:?}, default_params_file: {default_params_file:?}, process_pool_size: {process_pool_size:?})")]
277#[serde(deny_unknown_fields)]
278pub struct OptionalPipelineConfig {
279    /// Optional name of the currently active pipeline.
280    pub current_pipeline: Option<String>,
281    /// Optional name of the default pipeline.
282    pub default: Option<String>,
283    /// Optional default parameters file name for pipelines.
284    pub default_params_file: Option<String>,
285    /// Optional number of command processes to run concurrently.
286    pub process_pool_size: Option<u32>,
287}
288
289/// Optional configuration for checking ignored files, used for partial updates.
290#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
291#[display("OptionalCheckIgnoreConfig(details: {details:?})")]
292#[serde(deny_unknown_fields)]
293pub struct OptionalCheckIgnoreConfig {
294    /// Optional setting for whether to show detailed information when checking ignored files.
295    pub details: Option<bool>,
296}
297
298/// The top-level optional Xvc configuration structure, used for partial updates.
299#[derive(Display, Clone, Debug, Deserialize, PartialEq, Serialize, Default)]
300#[display("XvcOptionalConfiguration(core: {core:?}, git: {git:?}, cache: {cache:?}, file: {file:?}, pipeline: {pipeline:?}, check_ignore: {check_ignore:?})")]
301#[serde(deny_unknown_fields)]
302pub struct XvcOptionalConfiguration {
303    /// Optional core Xvc settings.
304    pub core: Option<OptionalCoreConfig>,
305    /// Optional Git integration settings.
306    pub git: Option<OptionalGitConfig>,
307    /// Optional cache settings.
308    pub cache: Option<OptionalCacheConfig>,
309    /// Optional file-related operation settings.
310    pub file: Option<OptionalFileConfig>,
311    /// Optional pipeline execution settings.
312    pub pipeline: Option<OptionalPipelineConfig>,
313    /// Optional check ignore settings.
314    pub check_ignore: Option<OptionalCheckIgnoreConfig>,
315}
316
317impl XvcOptionalConfiguration {
318    /// Creates an `XvcOptionalConfiguration` by reading and parsing a TOML file.
319    ///
320    /// # Arguments
321    ///
322    /// * `file_path` - The path to the TOML configuration file.
323    ///
324    /// # Returns
325    ///
326    /// A `Result` containing the `XvcOptionalConfiguration` if successful, or an error.
327    pub fn from_file(file_path: &Path) -> Result<Self> {
328        let s =
329            std::fs::read_to_string(&file_path).map_err(|e| crate::Error::IoError { source: e })?;
330        let c: XvcOptionalConfiguration =
331            toml::from_str(&s).map_err(|e| crate::Error::TomlDeserializationError { source: e })?;
332        Ok(c)
333    }
334
335    fn parse_bool(s: &str) -> Option<bool> {
336        match s {
337            "1" | "TRUE" | "True" | "true" => Some(true),
338            "0" | "FALSE" | "False" | "false" => Some(false),
339            _ => None,
340        }
341    }
342
343    /// Creates an `XvcOptionalConfiguration` from a HashMap of string key-value pairs.
344    /// This is typically used to parse environment variables or command-line arguments.
345    ///
346    /// # Arguments
347    ///
348    /// * `prefix` - The prefix to filter keys from the HashMap (e.g., "XVC_").
349    /// * `values` - A reference to the HashMap containing configuration values.
350    ///
351    /// # Returns
352    ///
353    /// A new `XvcOptionalConfiguration` instance.
354    pub fn from_hash_map(prefix: &str, values: &HashMap<String, String>) -> Self {
355        let mut config = Self {
356            core: None,
357            git: None,
358            cache: None,
359            file: None,
360            pipeline: None,
361            check_ignore: None,
362        };
363
364        for (key, value) in values.iter() {
365            if !key.starts_with(prefix) {
366                continue;
367            }
368
369            let key_str = &key[prefix.len()..].to_lowercase();
370            match key_str.as_str() {
371                // core
372                "core.xvc_repo_version" => {
373                    if let Ok(val) = value.parse::<u8>() {
374                        config
375                            .core
376                            .get_or_insert_with(Default::default)
377                            .xvc_repo_version = Some(val);
378                    }
379                }
380                "core.verbosity" => {
381                    config.core.get_or_insert_with(Default::default).verbosity =
382                        Some(value.to_string());
383                }
384                // git
385                "git.use_git" => {
386                    if let Some(val) = Self::parse_bool(value) {
387                        config.git.get_or_insert_with(Default::default).use_git = Some(val);
388                    }
389                }
390                "git.command" => {
391                    config.git.get_or_insert_with(Default::default).command =
392                        Some(value.to_string());
393                }
394                "git.auto_commit" => {
395                    if let Some(val) = Self::parse_bool(value) {
396                        config.git.get_or_insert_with(Default::default).auto_commit = Some(val);
397                    }
398                }
399                "git.auto_stage" => {
400                    if let Some(val) = Self::parse_bool(value) {
401                        config.git.get_or_insert_with(Default::default).auto_stage = Some(val);
402                    }
403                }
404                // cache
405                "cache.algorithm" => {
406                    config.cache.get_or_insert_with(Default::default).algorithm =
407                        Some(value.to_string());
408                }
409                // file.track
410                "file.track.no_commit" => {
411                    if let Some(val) = Self::parse_bool(value) {
412                        config
413                            .file
414                            .get_or_insert_with(Default::default)
415                            .track
416                            .get_or_insert_with(Default::default)
417                            .no_commit = Some(val);
418                    }
419                }
420                "file.track.force" => {
421                    if let Some(val) = Self::parse_bool(value) {
422                        config
423                            .file
424                            .get_or_insert_with(Default::default)
425                            .track
426                            .get_or_insert_with(Default::default)
427                            .force = Some(val);
428                    }
429                }
430                "file.track.text_or_binary" => {
431                    config
432                        .file
433                        .get_or_insert_with(Default::default)
434                        .track
435                        .get_or_insert_with(Default::default)
436                        .text_or_binary = Some(value.to_string());
437                }
438                "file.track.no_parallel" => {
439                    if let Some(val) = Self::parse_bool(value) {
440                        config
441                            .file
442                            .get_or_insert_with(Default::default)
443                            .track
444                            .get_or_insert_with(Default::default)
445                            .no_parallel = Some(val);
446                    }
447                }
448                "file.track.include_git_files" => {
449                    if let Some(val) = Self::parse_bool(value) {
450                        config
451                            .file
452                            .get_or_insert_with(Default::default)
453                            .track
454                            .get_or_insert_with(Default::default)
455                            .include_git_files = Some(val);
456                    }
457                }
458                // file.list
459                "file.list.format" => {
460                    config
461                        .file
462                        .get_or_insert_with(Default::default)
463                        .list
464                        .get_or_insert_with(Default::default)
465                        .format = Some(value.to_string());
466                }
467                "file.list.sort" => {
468                    config
469                        .file
470                        .get_or_insert_with(Default::default)
471                        .list
472                        .get_or_insert_with(Default::default)
473                        .sort = Some(value.to_string());
474                }
475                "file.list.show_dot_files" => {
476                    if let Some(val) = Self::parse_bool(value) {
477                        config
478                            .file
479                            .get_or_insert_with(Default::default)
480                            .list
481                            .get_or_insert_with(Default::default)
482                            .show_dot_files = Some(val);
483                    }
484                }
485                "file.list.no_summary" => {
486                    if let Some(val) = Self::parse_bool(value) {
487                        config
488                            .file
489                            .get_or_insert_with(Default::default)
490                            .list
491                            .get_or_insert_with(Default::default)
492                            .no_summary = Some(val);
493                    }
494                }
495                "file.list.recursive" => {
496                    if let Some(val) = Self::parse_bool(value) {
497                        config
498                            .file
499                            .get_or_insert_with(Default::default)
500                            .list
501                            .get_or_insert_with(Default::default)
502                            .recursive = Some(val);
503                    }
504                }
505                "file.list.include_git_files" => {
506                    if let Some(val) = Self::parse_bool(value) {
507                        config
508                            .file
509                            .get_or_insert_with(Default::default)
510                            .list
511                            .get_or_insert_with(Default::default)
512                            .include_git_files = Some(val);
513                    }
514                }
515                // file.carry_in
516                "file.carry_in.force" => {
517                    if let Some(val) = Self::parse_bool(value) {
518                        config
519                            .file
520                            .get_or_insert_with(Default::default)
521                            .carry_in
522                            .get_or_insert_with(Default::default)
523                            .force = Some(val);
524                    }
525                }
526                "file.carry_in.no_parallel" => {
527                    if let Some(val) = Self::parse_bool(value) {
528                        config
529                            .file
530                            .get_or_insert_with(Default::default)
531                            .carry_in
532                            .get_or_insert_with(Default::default)
533                            .no_parallel = Some(val);
534                    }
535                }
536                // file.recheck
537                "file.recheck.method" => {
538                    config
539                        .file
540                        .get_or_insert_with(Default::default)
541                        .recheck
542                        .get_or_insert_with(Default::default)
543                        .method = Some(value.to_string());
544                }
545                // pipeline
546                "pipeline.current_pipeline" => {
547                    config
548                        .pipeline
549                        .get_or_insert_with(Default::default)
550                        .current_pipeline = Some(value.to_string());
551                }
552                "pipeline.default" => {
553                    config.pipeline.get_or_insert_with(Default::default).default =
554                        Some(value.to_string());
555                }
556                "pipeline.default_params_file" => {
557                    config
558                        .pipeline
559                        .get_or_insert_with(Default::default)
560                        .default_params_file = Some(value.to_string());
561                }
562                "pipeline.process_pool_size" => {
563                    if let Ok(val) = value.parse::<u32>() {
564                        config
565                            .pipeline
566                            .get_or_insert_with(Default::default)
567                            .process_pool_size = Some(val);
568                    }
569                }
570                // check_ignore
571                "check_ignore.details" => {
572                    if let Some(val) = Self::parse_bool(value) {
573                        config
574                            .check_ignore
575                            .get_or_insert_with(Default::default)
576                            .details = Some(val);
577                    }
578                }
579                _ => {} // Ignore unknown keys
580            }
581        }
582        config
583    }
584
585    /// Creates an `XvcOptionalConfiguration` by reading environment variables
586    /// prefixed with "XVC_".
587    ///
588    /// # Returns
589    ///
590    /// A new `XvcOptionalConfiguration` instance populated from environment variables.
591    pub fn from_env() -> Self {
592        let env_vars: HashMap<String, String> = std::env::vars().collect();
593        Self::from_hash_map("XVC_", &env_vars)
594    }
595}
596
597/// How should we initialize the configuration?
598///
599/// It's possible to ignore certain sources by supplying `None` to their values here.
600#[derive(Debug, Clone)]
601pub struct XvcConfigParams {
602    /// The default configuration for the project.
603    /// It should contain all default values as a TOML document.
604    /// Xvc produces this in [xvc_core::default_configuration].
605    pub default_configuration: String,
606    /// The directory where the application runs.
607    /// This can be set by various Options.
608    /// It affects how paths are handled in general.
609    pub current_dir: AbsolutePath,
610    /// Should we include system configuration?
611    /// If `true`, it's read from [SYSTEM_CONFIG_DIRS].
612    pub include_system_config: bool,
613    /// Should the user's (home) config be included.
614    /// If `true`, it's read from [USER_CONFIG_DIRS].
615    pub include_user_config: bool,
616    /// Where should we load the project's (public) configuration?
617    /// It's loaded in [XvcRootInner::new]
618    /// TODO: Add a option to ignore this
619    pub project_config_path: Option<AbsolutePath>,
620    /// Where should we load the project's (private) configuration?
621    /// It's loaded in [XvcRootInner::new]
622    /// TODO: Add a option to ignore this
623    pub local_config_path: Option<AbsolutePath>,
624    /// Should we include configuration from the environment.
625    /// If `true`, look for all variables in the form
626    ///
627    /// `XVC_group.key=value`
628    ///
629    /// from the environment and put them into the configuration.
630    pub include_environment_config: bool,
631    /// Command line configuration
632    pub command_line_config: Option<Vec<String>>,
633}
634
635impl XvcConfigParams {
636    /// Creates a new `XvcConfigParams` instance with default settings.
637    ///
638    /// # Arguments
639    ///
640    /// * `default_configuration` - The default configuration as a TOML string.
641    /// * `current_dir` - The absolute path of the current working directory.
642    ///
643    /// # Returns
644    ///
645    /// A new `XvcConfigParams` instance with default settings.
646    pub fn new(default_configuration: String, current_dir: AbsolutePath) -> Self {
647        Self {
648            default_configuration,
649            current_dir,
650            include_system_config: true,
651            include_user_config: true,
652            project_config_path: None,
653            local_config_path: None,
654            include_environment_config: true,
655            command_line_config: None,
656        }
657    }
658
659    /// Updates the `include_system_config` value.
660    ///
661    /// # Arguments
662    ///
663    /// * `include_system_config` - Whether to include system configuration.
664    ///
665    /// # Returns
666    ///
667    /// The modified `XvcConfigParams` instance.
668    pub fn include_system_config(mut self, include_system_config: bool) -> Self {
669        self.include_system_config = include_system_config;
670        self
671    }
672
673    /// Updates the `include_user_config` value.
674    ///
675    /// # Arguments
676    ///
677    /// * `include_user_config` - Whether to include user configuration.
678    ///
679    /// # Returns
680    ///
681    /// The modified `XvcConfigParams` instance.
682    pub fn include_user_config(mut self, include_user_config: bool) -> Self {
683        self.include_user_config = include_user_config;
684        self
685    }
686
687    /// Updates the `project_config_path` value.
688    ///
689    /// # Arguments
690    ///
691    /// * `project_config_path` - The optional absolute path to the project's public configuration.
692    ///
693    /// # Returns
694    ///
695    /// The modified `XvcConfigParams` instance.
696    pub fn project_config_path(mut self, project_config_path: Option<AbsolutePath>) -> Self {
697        self.project_config_path = project_config_path;
698        self
699    }
700
701    /// Updates the `local_config_path` value.
702    ///
703    /// # Arguments
704    ///
705    /// * `local_config_path` - The optional absolute path to the project's private configuration.
706    ///
707    /// # Returns
708    ///
709    /// The modified `XvcConfigParams` instance.
710    pub fn local_config_path(mut self, local_config_path: Option<AbsolutePath>) -> Self {
711        self.local_config_path = local_config_path;
712        self
713    }
714
715    /// Whether to include environment variables in the configuration.
716    ///
717    /// # Arguments
718    ///
719    /// * `include_environment_config` - Whether to include environment variables.
720    ///
721    /// # Returns
722    ///
723    /// The modified `XvcConfigParams` instance.
724    pub fn include_environment_config(mut self, include_environment_config: bool) -> Self {
725        self.include_environment_config = include_environment_config;
726        self
727    }
728
729    /// Sets the command line configuration from key=value definitions.
730    ///
731    /// # Arguments
732    ///
733    /// * `command_line_config` - An optional vector of strings representing command-line configurations.
734    ///
735    /// # Returns
736    ///
737    /// The modified `XvcConfigParams` instance.
738    pub fn command_line_config(mut self, command_line_config: Option<Vec<String>>) -> Self {
739        self.command_line_config = command_line_config;
740        self
741    }
742}
743
744/// Returns the default Xvc configuration.
745///
746/// This configuration serves as the base, which can then be overridden
747/// by system, user, and repository-specific configurations.
748///
749/// # Returns
750///
751/// A `XvcConfiguration` struct populated with default values.
752pub fn default_config() -> XvcConfiguration {
753    XvcConfiguration {
754        core: CoreConfig {
755            xvc_repo_version: 2,
756            verbosity: "error".to_string(),
757        },
758        git: GitConfig {
759            use_git: true,
760            command: "git".to_string(),
761            auto_commit: true,
762            auto_stage: false,
763        },
764        cache: CacheConfig {
765            algorithm: "blake3".to_string(),
766        },
767        file: FileConfig {
768            track: FileTrackConfig {
769                no_commit: false,
770                force: false,
771                text_or_binary: "auto".to_string(),
772                no_parallel: false,
773                include_git_files: false,
774            },
775            list: FileListConfig {
776                format: "{{aft}}{{rrm}} {{asz}} {{ats}} {{rcd8}} {{acd8}} {{name}}".to_string(),
777                sort: "name-desc".to_string(),
778                show_dot_files: false,
779                no_summary: false,
780                recursive: false,
781                include_git_files: false,
782            },
783            carry_in: FileCarryInConfig {
784                force: false,
785                no_parallel: false,
786            },
787            recheck: FileRecheckConfig {
788                method: "copy".to_string(),
789            },
790        },
791        pipeline: PipelineConfig {
792            current_pipeline: "default".to_string(),
793            default: "default".to_string(),
794            default_params_file: "params.yaml".to_string(),
795            process_pool_size: 4,
796        },
797        check_ignore: CheckIgnoreConfig { details: false },
798    }
799}
800
801/// Merges an optional configuration into a base Xvc configuration.
802///
803/// This function applies values from `opt_config` to `config`, overwriting
804/// any existing values in `config` if they are present in `opt_config`.
805///
806/// # Arguments
807///
808/// * `config` - A reference to the base `XvcConfiguration`.
809/// * `opt_config` - A reference to the optional `XvcOptionalConfiguration` to merge.
810///
811/// # Returns
812///
813/// A new `XvcConfiguration` with merged values.
814pub fn merge_configs(
815    config: &XvcConfiguration,
816    opt_config: &XvcOptionalConfiguration,
817) -> XvcConfiguration {
818    let core = CoreConfig {
819        xvc_repo_version: opt_config
820            .core
821            .clone()
822            .and_then(|c| c.xvc_repo_version)
823            .unwrap_or(config.core.xvc_repo_version),
824        verbosity: opt_config
825            .core
826            .clone()
827            .and_then(|c| c.verbosity)
828            .unwrap_or(config.core.verbosity.clone()),
829    };
830
831    let git = GitConfig {
832        use_git: opt_config
833            .git
834            .clone()
835            .and_then(|g| g.use_git)
836            .unwrap_or(config.git.use_git),
837        command: opt_config
838            .git
839            .clone()
840            .and_then(|g| g.command)
841            .unwrap_or(config.git.command.clone()),
842        auto_commit: opt_config
843            .git
844            .clone()
845            .and_then(|g| g.auto_commit)
846            .unwrap_or(config.git.auto_commit),
847        auto_stage: opt_config
848            .git
849            .clone()
850            .and_then(|g| g.auto_stage)
851            .unwrap_or(config.git.auto_stage),
852    };
853
854    let cache = CacheConfig {
855        algorithm: opt_config
856            .cache
857            .clone()
858            .and_then(|g| g.algorithm)
859            .unwrap_or(config.cache.algorithm.clone()),
860    };
861
862    let opt_track = opt_config.file.clone().and_then(|f| f.track);
863    let track = FileTrackConfig {
864        no_commit: opt_track
865            .clone()
866            .and_then(|t| t.no_commit)
867            .unwrap_or(config.file.track.no_commit),
868        force: opt_track
869            .clone()
870            .and_then(|t| t.force)
871            .unwrap_or(config.file.track.force),
872        text_or_binary: opt_track
873            .clone()
874            .and_then(|t| t.text_or_binary)
875            .unwrap_or(config.file.track.text_or_binary.clone()),
876        no_parallel: opt_track
877            .clone()
878            .and_then(|t| t.no_parallel)
879            .unwrap_or(config.file.track.no_parallel),
880        include_git_files: opt_track
881            .and_then(|t| t.include_git_files)
882            .unwrap_or(config.file.track.include_git_files),
883    };
884
885    let opt_list = opt_config.file.clone().and_then(|f| f.list);
886    let list = FileListConfig {
887        format: opt_list
888            .clone()
889            .and_then(|l| l.format)
890            .unwrap_or(config.file.list.format.clone()),
891        sort: opt_list
892            .clone()
893            .and_then(|l| l.sort)
894            .unwrap_or(config.file.list.sort.clone()),
895        show_dot_files: opt_list
896            .clone()
897            .and_then(|l| l.show_dot_files)
898            .unwrap_or(config.file.list.show_dot_files),
899        no_summary: opt_list
900            .clone()
901            .and_then(|l| l.no_summary)
902            .unwrap_or(config.file.list.no_summary),
903        recursive: opt_list
904            .clone()
905            .and_then(|l| l.recursive)
906            .unwrap_or(config.file.list.recursive),
907        include_git_files: opt_list
908            .and_then(|l| l.include_git_files)
909            .unwrap_or(config.file.list.include_git_files),
910    };
911
912    let opt_carry_in = opt_config.file.clone().and_then(|f| f.carry_in);
913    let carry_in = FileCarryInConfig {
914        force: opt_carry_in
915            .clone()
916            .and_then(|c| c.force)
917            .unwrap_or(config.file.carry_in.force),
918        no_parallel: opt_carry_in
919            .and_then(|c| c.no_parallel)
920            .unwrap_or(config.file.carry_in.no_parallel),
921    };
922
923    let opt_recheck = opt_config.file.clone().and_then(|f| f.recheck);
924    let recheck = FileRecheckConfig {
925        method: opt_recheck
926            .and_then(|r| r.method)
927            .unwrap_or(config.file.recheck.method.clone()),
928    };
929
930    let file = FileConfig {
931        track,
932        list,
933        carry_in,
934        recheck,
935    };
936
937    let pipeline = PipelineConfig {
938        current_pipeline: opt_config
939            .pipeline
940            .clone()
941            .and_then(|p| p.current_pipeline)
942            .unwrap_or(config.pipeline.current_pipeline.clone()),
943        default: opt_config
944            .pipeline
945            .clone()
946            .and_then(|p| p.default)
947            .unwrap_or(config.pipeline.default.clone()),
948        default_params_file: opt_config
949            .pipeline
950            .clone()
951            .and_then(|p| p.default_params_file)
952            .unwrap_or(config.pipeline.default_params_file.clone()),
953        process_pool_size: opt_config
954            .pipeline
955            .clone()
956            .and_then(|p| p.process_pool_size)
957            .unwrap_or(config.pipeline.process_pool_size),
958    };
959
960    let check_ignore = CheckIgnoreConfig {
961        details: opt_config
962            .check_ignore
963            .clone()
964            .and_then(|c| c.details)
965            .unwrap_or(config.check_ignore.details),
966    };
967
968    XvcConfiguration {
969        core,
970        git,
971        cache,
972        file,
973        pipeline,
974        check_ignore,
975    }
976}
977
978/// Initializes the Xvc configuration file from the given [XvcConfiguration]
979///
980/// Please use [merge_configs] before this to get [config] from possibly
981/// user supplied [XvcOptionalConfiguration]. See [init_xvc_root] function for a use of this.
982///
983/// # Arguments
984///
985/// * `config` - The `XvcConfiguration` to convert to a TOML file.
986///
987/// # Returns
988///
989/// A `Result` containing a TOML-formatted string of the merged configuration if successful, or an error.
990pub fn initial_xvc_configuration_file(config: &XvcConfiguration) -> Result<String> {
991    Ok(format!(
992        r##"
993[core]
994xvc_repo_version = {xvc_repo_version}
995# Default verbosity level.
996# One of "error", "warn", "info"
997verbosity = "{verbosity}"
998
999[git]
1000# Automate git operations.
1001# Turning this off leads Xvc to behave as if it's not in a Git repository.
1002# Not recommended unless you're really not using Git
1003use_git = {use_git}
1004# Command to run Git process.
1005# You can set this to an absolute path to specify an executable
1006# If set to a non-absolute path, the executable will be searched in $PATH.
1007command = "{git_command}"
1008
1009# Commit changes in .xvc/ directory after commands.
1010# You can set this to false if you want to commit manually.
1011auto_commit = {auto_commit}
1012
1013# Stage changes in .xvc/ directory without committing.
1014# auto_commit implies auto_stage.
1015# If you want to commit manually but don't want to stage after individual Xvc commands, you can set this to true.
1016auto_stage = {auto_stage}
1017
1018[cache]
1019# The hash algorithm used for the cache.
1020# It may take blake3, blake2, sha2 or sha3 as values.
1021# All algorithms are selected to produce 256-bit hashes, so sha2 means SHA2-256, blake2 means BLAKE2s, etc.
1022# The cache path is produced by prepending algorithm name to the cache.
1023# Blake3 files are in .xvc/b3/, while sha2 files are in .xvc/s2/ etc.
1024algorithm = "{cache_algorithm}"
1025
1026[file]
1027
1028[file.track]
1029
1030# Don't move file content to cache after xvc file track
1031no_commit = {file_track_no_commit}
1032# Force to track files even if they are already tracked.
1033force = {file_track_force}
1034
1035# Xvc calculates file content digest differently for text and binary files.
1036# This option controls whether to treat files as text or binary.
1037# It may take auto, text or binary as values.
1038# Auto check each file individually and treat it as text if it's text.
1039text_or_binary = "{file_track_text_or_binary}"
1040
1041# Don't use parallelism in track operations.
1042# Note that some of the operations are implemented in parallel by default, and this option affects some heavier operations.
1043no_parallel = {file_track_no_parallel}
1044
1045# Track files that are tracked by Git. 
1046include_git_files = {file_track_include_git_files}
1047
1048[file.list]
1049
1050# Format for `xvc file list` rows. You can reorder or remove columns.
1051# The following are the keys for each row:
1052# - {{acd64}}:  actual content digest. All 64 digits from the workspace file's content.
1053# - {{acd8}}:  actual content digest. First 8 digits the file content digest.
1054# - {{aft}}:  actual file type. Whether the entry is a file (F), directory (D),
1055#   symlink (S), hardlink (H) or reflink (R).
1056# - {{asz}}:  actual size. The size of the workspace file in bytes. It uses MB,
1057#   GB and TB to represent sizes larger than 1MB.
1058# - {{ats}}:  actual timestamp. The timestamp of the workspace file.
1059# - {{cst}}:  cache status. One of "=", ">", "<", "X", or "?" to show
1060#   whether the file timestamp is the same as the cached timestamp, newer,
1061#   older, not cached or not tracked.
1062# - {{name}}: The name of the file or directory.
1063# - {{rcd64}}:  recorded content digest. All 64 digits.
1064# - {{rcd8}}:  recorded content digest. First 8 digits.
1065# - {{rrm}}:  recorded recheck method. Whether the entry is linked to the workspace
1066#   as a copy (C), symlink (S), hardlink (H) or reflink (R).
1067# - {{rsz}}:  recorded size. The size of the cached content in bytes. It uses
1068#   MB, GB and TB to represent sizes larger than 1MB.
1069# - {{rts}}:  recorded timestamp. The timestamp of the cached content.
1070#
1071# There are no escape sequences in the format string.
1072# If you want to add a tab, type it to the string.
1073# If you want to add a literal double curly brace, open an issue.
1074format = "{file_list_format}"
1075
1076# Default sort order for `xvc file list`.
1077# Valid values are
1078# none, name-asc, name-desc, size-asc, size-desc, ts-asc, ts-desc.
1079sort = "{file_list_sort}"
1080
1081# Show dot files like .gitignore
1082show_dot_files = {file_list_show_dot_files}
1083
1084# Do not show a summary for as the final row for `xvc file list`.
1085no_summary = {file_list_no_summary}
1086
1087# List files recursively always.
1088recursive = {file_list_recursive}
1089
1090# List files tracked by Git. 
1091include_git_files = {file_list_include_git_files}
1092
1093[file.carry-in]
1094# Carry-in the files to cache always, even if they are already present.
1095force = {file_carry_in_force}
1096
1097# Don't use parallel move/copy in carry-in
1098no_parallel = {file_carry_in_no_parallel}
1099
1100[file.recheck]
1101# The recheck method for Xvc. It may take copy, hardlink, symlink, reflink as values.
1102# The default is copy to make sure the options is portable.
1103# Copy duplicates the file content, while hardlink, symlink and reflink only create a new path to the file.
1104# Note that hardlink and symlink are read-only as they link the files in cache.
1105method = "{file_recheck_method}"
1106
1107[pipeline]
1108# Name of the current pipeline to run
1109current_pipeline = "{pipeline_current_pipeline}"
1110# Name of the default pipeline
1111default = "{pipeline_default}"
1112# Name of the default params file name
1113default_params_file = "{pipeline_default_params_file}"
1114# Number of command processes to run concurrently
1115process_pool_size = {pipeline_process_pool_size}
1116 
1117[check_ignore]
1118# Show details by default
1119details = {check_ignore_details}
1120
1121"##,
1122        xvc_repo_version = config.core.xvc_repo_version,
1123        verbosity = config.core.verbosity,
1124        use_git = config.git.use_git,
1125        git_command = config.git.command,
1126        auto_commit = config.git.auto_commit,
1127        auto_stage = config.git.auto_stage,
1128        cache_algorithm = config.cache.algorithm,
1129        file_track_no_commit = config.file.track.no_commit,
1130        file_track_force = config.file.track.force,
1131        file_track_text_or_binary = config.file.track.text_or_binary,
1132        file_track_no_parallel = config.file.track.no_parallel,
1133        file_track_include_git_files = config.file.track.include_git_files,
1134        file_list_format = config.file.list.format,
1135        file_list_sort = config.file.list.sort,
1136        file_list_show_dot_files = config.file.list.show_dot_files,
1137        file_list_no_summary = config.file.list.no_summary,
1138        file_list_recursive = config.file.list.recursive,
1139        file_list_include_git_files = config.file.list.include_git_files,
1140        file_carry_in_force = config.file.carry_in.force,
1141        file_carry_in_no_parallel = config.file.carry_in.no_parallel,
1142        file_recheck_method = config.file.recheck.method,
1143        pipeline_current_pipeline = config.pipeline.current_pipeline,
1144        pipeline_default = config.pipeline.default,
1145        pipeline_default_params_file = config.pipeline.default_params_file,
1146        pipeline_process_pool_size = config.pipeline.process_pool_size,
1147        check_ignore_details = config.check_ignore.details,
1148    ))
1149}
1150
1151/// Returns a blank `XvcOptionalConfiguration` with all fields set to `None`.
1152///
1153/// This is useful as a starting point when no optional configuration overrides are provided.
1154///
1155/// # Returns
1156///
1157/// A new `XvcOptionalConfiguration` instance with no values set.
1158pub fn blank_optional_config() -> XvcOptionalConfiguration {
1159    XvcOptionalConfiguration {
1160        core: None,
1161        git: None,
1162        cache: None,
1163        file: None,
1164        pipeline: None,
1165        check_ignore: None,
1166    }
1167}
1168
1169impl XvcConfiguration {
1170    /// Creates an `XvcConfiguration` by reading and parsing a TOML file.
1171    ///
1172    /// # Arguments
1173    ///
1174    /// * `file_path` - The path to the TOML configuration file.
1175    ///
1176    /// # Returns
1177    ///
1178    /// A `Result` containing the `XvcConfiguration` if successful, or an error.
1179    pub fn from_file(file_path: &Path) -> Result<Self> {
1180        let s =
1181            std::fs::read_to_string(&file_path).map_err(|e| crate::Error::IoError { source: e })?;
1182        let c: XvcConfiguration =
1183            toml::from_str(&s).map_err(|e| crate::Error::TomlDeserializationError { source: e })?;
1184        Ok(c)
1185    }
1186
1187    /// Merges an optional configuration into the current configuration.
1188    ///
1189    /// # Arguments
1190    ///
1191    /// * `opt_config` - A reference to the `XvcOptionalConfiguration` to merge.
1192    ///
1193    /// # Returns
1194    ///
1195    /// A new `XvcConfiguration` with the merged values.
1196    pub fn merge_with_optional(&self, opt_config: &XvcOptionalConfiguration) -> Self {
1197        merge_configs(self, opt_config)
1198    }
1199}