1use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::environment::Environment;
7use crate::error::{ConfigError, Error, Result};
8
9use super::{
10 global_config_paths_pub, resolve_config_override_path, system_config_path, ConfigFile,
11 ConfigScope, ConfigSet, IncludeContext,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ConfigLocation {
17 Local,
19 Global,
21 System,
23 Worktree,
25 File(PathBuf),
27}
28
29impl ConfigLocation {
30 #[must_use]
32 pub fn file_scope(&self) -> ConfigScope {
33 match self {
34 Self::Local => ConfigScope::Local,
35 Self::File(_) => ConfigScope::Local,
36 Self::Global => ConfigScope::Global,
37 Self::System => ConfigScope::System,
38 Self::Worktree => ConfigScope::Worktree,
39 }
40 }
41
42 pub fn write_path(&self, env: &Environment, git_dir: Option<&Path>) -> Result<PathBuf> {
51 match self {
52 Self::Global => global_config_write_path(env),
53 Self::Local => {
54 let gd = git_dir.ok_or_else(|| {
55 Error::Config(ConfigError::Other("not in a git repository".into()))
56 })?;
57 Ok(crate::repo::local_config_write_path(gd))
58 }
59 Self::File(path) => {
60 let s = path.to_string_lossy();
61 Ok(resolve_config_override_path(env, s.as_ref()))
62 }
63 Self::System => Ok(system_config_path(env)),
64 Self::Worktree => {
65 let gd = git_dir.ok_or_else(|| {
66 Error::Config(ConfigError::Other("not in a git repository".into()))
67 })?;
68 Ok(gd.join("config.worktree"))
69 }
70 }
71 }
72
73 pub fn load_single(&self, env: &Environment, git_dir: Option<&Path>) -> Result<ConfigSet> {
79 let mut set = ConfigSet::new();
80 let ctx = IncludeContext {
81 git_dir: git_dir.map(Path::to_path_buf),
82 command_line_relative_include_is_error: false,
83 cwd: env.cwd.clone(),
84 pwd: env.pwd.clone(),
85 env: std::sync::Arc::new(env.clone()),
86 };
87
88 match self {
89 Self::Global => {
90 for path in global_config_paths_pub(env) {
91 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Global)? {
92 set.merge_file_with_includes(&f, true, &ctx)?;
93 }
94 }
95 }
96 Self::Local => {
97 let gd = git_dir.ok_or_else(|| {
98 Error::Config(ConfigError::Other("not in a git repository".into()))
99 })?;
100 let local_path = crate::repo::local_config_write_path(gd);
101 if let Some(f) = ConfigFile::from_path(&local_path, ConfigScope::Local)? {
102 set.merge_file_with_includes(&f, true, &ctx)?;
103 }
104 }
105 Self::System => {
106 let system_path = system_config_path(env);
107 if let Some(f) = ConfigFile::from_path(&system_path, ConfigScope::System)? {
108 set.merge_file_with_includes(&f, true, &ctx)?;
109 }
110 }
111 Self::Worktree => {
112 let gd = git_dir.ok_or_else(|| {
113 Error::Config(ConfigError::Other("not in a git repository".into()))
114 })?;
115 let common = crate::repo::common_git_dir_for_config(gd);
116 if !crate::repo::worktree_config_enabled(&common) {
117 return Ok(set);
118 }
119 let wt_path = gd.join("config.worktree");
120 if let Some(f) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree)? {
121 set.merge_file_with_includes(&f, true, &ctx)?;
122 }
123 }
124 Self::File(path) => {
125 let s = path.to_string_lossy();
126 let path = resolve_config_override_path(env, s.as_ref());
127 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Command)? {
128 set.merge_file_with_includes(&f, true, &ctx)?;
129 }
130 }
131 }
132
133 Ok(set)
134 }
135}
136
137pub fn global_config_write_path(env: &Environment) -> Result<PathBuf> {
142 let paths = global_config_paths_pub(env);
143 if paths.len() == 1 {
144 return paths.into_iter().next().ok_or_else(|| {
145 Error::Config(ConfigError::Other(
146 "could not determine global config path".into(),
147 ))
148 });
149 }
150 let dotgitconfig = paths.last().cloned();
151 if dotgitconfig.as_ref().is_some_and(|p| p.exists()) {
152 return dotgitconfig.ok_or_else(|| {
153 Error::Config(ConfigError::Other(
154 "could not determine global config path".into(),
155 ))
156 });
157 }
158 if let Some(xdg) = paths.first() {
159 if xdg.exists() {
160 return Ok(xdg.clone());
161 }
162 }
163 dotgitconfig.ok_or_else(|| {
164 Error::Config(ConfigError::Other(
165 "could not determine global config path".into(),
166 ))
167 })
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct ConfigWritePreview {
173 pub path: PathBuf,
175 pub summary: String,
177}
178
179fn ensure_worktree_write_allowed(location: &ConfigLocation, git_dir: Option<&Path>) -> Result<()> {
180 if !matches!(location, ConfigLocation::Worktree) {
181 return Ok(());
182 }
183 let Some(git_dir) = git_dir else {
184 return Err(Error::Config(ConfigError::Other(
185 "not in a git repository".into(),
186 )));
187 };
188 let common = crate::repo::common_git_dir_for_config(git_dir);
189 if !crate::repo::worktree_config_enabled(&common) {
190 return Err(Error::Config(ConfigError::WorktreeConfigNotEnabled));
191 }
192 Ok(())
193}
194
195fn open_location_config(
196 location: &ConfigLocation,
197 env: &Environment,
198 git_dir: Option<&Path>,
199) -> Result<(PathBuf, ConfigFile)> {
200 ensure_worktree_write_allowed(location, git_dir)?;
201 let path = location.write_path(env, git_dir)?;
202 let scope = location.file_scope();
203 let content = fs::read_to_string(&path).unwrap_or_default();
204 let cfg = ConfigFile::parse(&path, &content, scope)?;
205 Ok((path, cfg))
206}
207
208fn persist_config(
209 path: &Path,
210 cfg: &mut ConfigFile,
211 dry_run: bool,
212 summary: String,
213) -> Result<Option<ConfigWritePreview>> {
214 if dry_run {
215 return Ok(Some(ConfigWritePreview {
216 path: path.to_path_buf(),
217 summary,
218 }));
219 }
220 cfg.write()?;
221 Ok(None)
222}
223
224pub fn set_config_value(
226 location: ConfigLocation,
227 env: &Environment,
228 git_dir: Option<&Path>,
229 key: &str,
230 value: &str,
231 dry_run: bool,
232) -> Result<Option<ConfigWritePreview>> {
233 let (path, mut cfg) = open_location_config(&location, env, git_dir)?;
234 cfg.set(key, value)?;
235 persist_config(&path, &mut cfg, dry_run, format!("set {key}={value}"))
236}
237
238pub fn add_config_value(
240 location: ConfigLocation,
241 env: &Environment,
242 git_dir: Option<&Path>,
243 key: &str,
244 value: &str,
245 dry_run: bool,
246) -> Result<Option<ConfigWritePreview>> {
247 let (path, mut cfg) = open_location_config(&location, env, git_dir)?;
248 cfg.add_value(key, value)?;
249 persist_config(&path, &mut cfg, dry_run, format!("add {key}={value}"))
250}
251
252pub fn unset_config_value(
258 location: ConfigLocation,
259 env: &Environment,
260 git_dir: Option<&Path>,
261 key: &str,
262 dry_run: bool,
263) -> Result<Option<ConfigWritePreview>> {
264 let (path, mut cfg) = open_location_config(&location, env, git_dir)?;
265 if !path.exists() {
266 return Err(Error::Config(ConfigError::KeyNotSet {
267 key: key.to_owned(),
268 }));
269 }
270 let removed = cfg.unset_last(key)?;
271 if removed == 0 {
272 return Err(Error::Config(ConfigError::KeyNotSet {
273 key: key.to_owned(),
274 }));
275 }
276 persist_config(&path, &mut cfg, dry_run, format!("unset {key}"))
277}
278
279pub fn unset_all_config_values(
281 location: ConfigLocation,
282 env: &Environment,
283 git_dir: Option<&Path>,
284 key: &str,
285 dry_run: bool,
286) -> Result<(usize, Option<ConfigWritePreview>)> {
287 let (path, mut cfg) = open_location_config(&location, env, git_dir)?;
288 let removed = cfg.unset_matching(key, None, false)?;
289 if removed == 0 {
290 return Err(Error::Config(ConfigError::KeyNotSet {
291 key: key.to_owned(),
292 }));
293 }
294 let preview = persist_config(
295 &path,
296 &mut cfg,
297 dry_run,
298 format!("unset-all {key} ({removed} values)"),
299 )?;
300 Ok((removed, preview))
301}
302
303pub fn rename_config_section(
305 location: ConfigLocation,
306 env: &Environment,
307 git_dir: Option<&Path>,
308 old_name: &str,
309 new_name: &str,
310 dry_run: bool,
311) -> Result<Option<ConfigWritePreview>> {
312 let (path, mut cfg) = open_location_config(&location, env, git_dir)?;
313 if !cfg.rename_section(old_name, new_name)? {
314 return Err(Error::Config(ConfigError::SectionNotFound {
315 section: old_name.to_owned(),
316 }));
317 }
318 persist_config(
319 &path,
320 &mut cfg,
321 dry_run,
322 format!("rename-section {old_name} -> {new_name}"),
323 )
324}
325
326pub fn remove_config_section(
328 location: ConfigLocation,
329 env: &Environment,
330 git_dir: Option<&Path>,
331 section: &str,
332 dry_run: bool,
333) -> Result<Option<ConfigWritePreview>> {
334 let (path, mut cfg) = open_location_config(&location, env, git_dir)?;
335 if !cfg.remove_section(section)? {
336 return Err(Error::Config(ConfigError::SectionNotFound {
337 section: section.to_owned(),
338 }));
339 }
340 persist_config(
341 &path,
342 &mut cfg,
343 dry_run,
344 format!("remove-section {section}"),
345 )
346}