1mod parse;
9mod schema;
10pub mod wtconfig;
11
12use std::path::{Path, PathBuf};
13
14use directories::ProjectDirs;
15
16use crate::cx::Env;
17use crate::error::{Error, Result};
18
19pub use parse::parse_layer;
20pub use schema::{Config, ConfigLayer, SubmoduleInit};
21pub use wtconfig::WtMeta;
22
23pub fn global_config_path(env: &Env) -> Option<PathBuf> {
27 if let Some(xdg) = env.get("XDG_CONFIG_HOME").filter(|s| !s.is_empty()) {
28 return Some(PathBuf::from(xdg).join("wt").join("config.toml"));
29 }
30 ProjectDirs::from("", "", "wt").map(|dirs| dirs.config_dir().join("config.toml"))
31}
32
33pub fn repo_config_path(repo_root: &Path) -> PathBuf {
35 repo_root.join(".wt.toml")
36}
37
38fn read_if_exists(path: &Path) -> Result<Option<String>> {
40 match std::fs::read_to_string(path) {
41 Ok(text) => Ok(Some(text)),
42 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
43 Err(e) => Err(Error::Config {
44 file: path.display().to_string(),
45 key: String::new(),
46 reason: format!("cannot read: {e}"),
47 }),
48 }
49}
50
51pub fn load(repo_root: Option<&Path>, env: &Env) -> Result<Config> {
55 let mut config = Config::default();
56 if let Some(global) = global_config_path(env)
57 && let Some(text) = read_if_exists(&global)?
58 {
59 config.apply(parse_layer(&text, &global.display().to_string())?);
60 }
61 if let Some(root) = repo_root {
62 let per_repo = repo_config_path(root);
63 if let Some(text) = read_if_exists(&per_repo)? {
64 config.apply(parse_layer(&text, &per_repo.display().to_string())?);
65 }
66 }
67 Ok(config)
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use std::collections::HashMap;
74
75 fn env(pairs: &[(&str, &str)]) -> Env {
76 Env::from_map(
77 pairs
78 .iter()
79 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
80 .collect::<HashMap<_, _>>(),
81 )
82 }
83
84 #[test]
85 fn global_path_uses_xdg_when_set() {
86 let e = env(&[("XDG_CONFIG_HOME", "/cfg")]);
87 assert_eq!(
88 global_config_path(&e),
89 Some(PathBuf::from("/cfg/wt/config.toml"))
90 );
91 }
92
93 #[test]
94 fn global_path_falls_back_to_platform_dir() {
95 assert!(global_config_path(&env(&[])).is_some());
97 }
98
99 #[test]
100 fn load_without_files_yields_defaults() {
101 let dir = tempfile::tempdir().unwrap();
102 let e = env(&[("XDG_CONFIG_HOME", dir.path().to_str().unwrap())]);
103 let config = load(Some(dir.path()), &e).unwrap();
104 assert_eq!(config, Config::default());
105 }
106
107 #[test]
108 fn per_repo_overrides_global() {
109 let global_dir = tempfile::tempdir().unwrap();
110 let repo_dir = tempfile::tempdir().unwrap();
111 let global_wt = global_dir.path().join("wt");
113 std::fs::create_dir_all(&global_wt).unwrap();
114 std::fs::write(
115 global_wt.join("config.toml"),
116 "pr.default_remote = \"global-remote\"\n[ui]\nmouse = false\n",
117 )
118 .unwrap();
119 std::fs::write(
121 repo_config_path(repo_dir.path()),
122 "[pr]\ndefault_remote = \"repo-remote\"\n",
123 )
124 .unwrap();
125
126 let e = env(&[("XDG_CONFIG_HOME", global_dir.path().to_str().unwrap())]);
127 let config = load(Some(repo_dir.path()), &e).unwrap();
128 assert_eq!(config.pr_default_remote, "repo-remote"); assert!(!config.ui_mouse); }
131
132 #[test]
133 fn load_propagates_validation_errors() {
134 let repo_dir = tempfile::tempdir().unwrap();
135 std::fs::write(repo_config_path(repo_dir.path()), "bogus_key = 1\n").unwrap();
136 let e = env(&[("XDG_CONFIG_HOME", "/nonexistent-xyz")]);
137 let err = load(Some(repo_dir.path()), &e).unwrap_err();
138 assert!(matches!(err, Error::Config { .. }));
139 }
140
141 #[test]
142 fn load_without_repo_uses_global_only() {
143 let global_dir = tempfile::tempdir().unwrap();
144 let global_wt = global_dir.path().join("wt");
145 std::fs::create_dir_all(&global_wt).unwrap();
146 std::fs::write(global_wt.join("config.toml"), "default_base = \"trunk\"\n").unwrap();
147 let e = env(&[("XDG_CONFIG_HOME", global_dir.path().to_str().unwrap())]);
148 let config = load(None, &e).unwrap();
149 assert_eq!(config.default_base.as_deref(), Some("trunk"));
150 }
151}