Skip to main content

git_atomic/config/
mod.rs

1pub mod git_provider;
2pub mod layered;
3pub mod source;
4pub mod types;
5
6pub use layered::{ResolvedConfig, load_layered_config};
7pub use source::{ConfigSource, Sourced};
8pub use types::{Component, Config, Settings, UnmatchedPolicy};
9
10use crate::core::ConfigError;
11use std::path::Path;
12
13/// Load configuration from `.atomic.toml` (or a custom path).
14pub fn load_config(path: &Path) -> Result<Config, ConfigError> {
15    if !path.exists() {
16        return Err(ConfigError::NotFound {
17            path: path.to_path_buf(),
18        });
19    }
20
21    let content = std::fs::read_to_string(path)?;
22    let config: Config = toml::from_str(&content).map_err(|e| ConfigError::Invalid {
23        reason: e.to_string(),
24    })?;
25
26    validate_config(&config)?;
27    Ok(config)
28}
29
30/// Validate that all glob patterns compile and component names are unique.
31fn validate_config(config: &Config) -> Result<(), ConfigError> {
32    // Check component name uniqueness
33    let mut seen = std::collections::HashSet::new();
34    for component in &config.components {
35        if !seen.insert(&component.name) {
36            return Err(ConfigError::Invalid {
37                reason: format!("duplicate component name: {:?}", component.name),
38            });
39        }
40        for pattern in &component.globs {
41            globset::Glob::new(pattern).map_err(|e| ConfigError::InvalidGlob {
42                component: component.name.clone(),
43                pattern: pattern.clone(),
44                reason: e.to_string(),
45            })?;
46        }
47    }
48    Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use std::io::Write;
55
56    fn write_toml(dir: &std::path::Path, content: &str) -> std::path::PathBuf {
57        let path = dir.join(".atomic.toml");
58        let mut f = std::fs::File::create(&path).unwrap();
59        f.write_all(content.as_bytes()).unwrap();
60        path
61    }
62
63    #[test]
64    fn load_valid_config() {
65        let dir = tempfile::tempdir().unwrap();
66        let path = write_toml(
67            dir.path(),
68            r#"
69[settings]
70base_branch = "develop"
71
72[[components]]
73name = "frontend"
74globs = ["src/ui/**"]
75
76[[components]]
77name = "backend"
78globs = ["src/api/**", "src/db/**"]
79commit_type = "fix"
80"#,
81        );
82
83        let cfg = load_config(&path).unwrap();
84        assert_eq!(cfg.settings.base_branch, "develop");
85        assert_eq!(cfg.settings.unmatched_files, UnmatchedPolicy::Error);
86        assert_eq!(cfg.components.len(), 2);
87
88        // Verify document order is preserved.
89        assert_eq!(cfg.components[0].name, "frontend");
90        assert_eq!(cfg.components[1].name, "backend");
91    }
92
93    #[test]
94    fn defaults_applied() {
95        let dir = tempfile::tempdir().unwrap();
96        let path = write_toml(
97            dir.path(),
98            r#"
99[[components]]
100name = "app"
101globs = ["**"]
102"#,
103        );
104
105        let cfg = load_config(&path).unwrap();
106        assert_eq!(cfg.settings.base_branch, "main");
107        assert_eq!(cfg.settings.branch_template, "atomic/{component}");
108        assert_eq!(cfg.settings.unmatched_files, UnmatchedPolicy::Error);
109    }
110
111    #[test]
112    fn invalid_glob_rejected() {
113        let dir = tempfile::tempdir().unwrap();
114        let path = write_toml(
115            dir.path(),
116            r#"
117[[components]]
118name = "bad"
119globs = ["[invalid"]
120"#,
121        );
122
123        let err = load_config(&path).unwrap_err();
124        assert!(matches!(err, ConfigError::InvalidGlob { .. }));
125    }
126
127    #[test]
128    fn missing_config_file() {
129        let err = load_config(Path::new("/nonexistent/.atomic.toml")).unwrap_err();
130        assert!(matches!(err, ConfigError::NotFound { .. }));
131    }
132
133    #[test]
134    fn duplicate_component_names_rejected() {
135        let dir = tempfile::tempdir().unwrap();
136        let path = write_toml(
137            dir.path(),
138            r#"
139[[components]]
140name = "app"
141globs = ["src/**"]
142
143[[components]]
144name = "app"
145globs = ["lib/**"]
146"#,
147        );
148
149        let err = load_config(&path).unwrap_err();
150        assert!(matches!(err, ConfigError::Invalid { .. }));
151    }
152}