use config::ConfigError;
use fs2::FileExt;
use crate::path::format_path_for_display;
use super::UserConfig;
use super::path;
use super::sections::{CommitConfig, CommitGenerationConfig};
pub(crate) fn acquire_config_lock(
config_path: &std::path::Path,
) -> Result<std::fs::File, ConfigError> {
let lock_path = config_path.with_extension("toml.lock");
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ConfigError::Message(format!("Failed to create config directory: {e}")))?;
}
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.map_err(|e| ConfigError::Message(format!("Failed to open lock file: {e}")))?;
file.lock_exclusive()
.map_err(|e| ConfigError::Message(format!("Failed to acquire config lock: {e}")))?;
Ok(file)
}
impl UserConfig {
pub(super) fn with_locked_mutation<F>(
&mut self,
config_path: Option<&std::path::Path>,
mutate: F,
) -> Result<(), ConfigError>
where
F: FnOnce(&mut Self) -> bool,
{
let path = match config_path {
Some(p) => p.to_path_buf(),
None => path::config_path().ok_or_else(|| {
ConfigError::Message(
"Cannot determine config directory. Set $HOME or $XDG_CONFIG_HOME".to_string(),
)
})?,
};
let _lock = acquire_config_lock(&path)?;
self.reload_projects_from(config_path)?;
if mutate(self) {
self.save_impl(config_path)?;
}
Ok(())
}
fn reload_projects_from(
&mut self,
config_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
let path = match config_path {
Some(p) => Some(p.to_path_buf()),
None => path::config_path(),
};
let Some(path) = path else {
return Ok(()); };
if !path.exists() {
return Ok(()); }
let content = std::fs::read_to_string(&path).map_err(|e| {
ConfigError::Message(format!(
"Failed to read config file {}: {}",
format_path_for_display(&path),
e
))
})?;
let migrated = crate::config::deprecation::migrate_content(&content);
let disk_config: UserConfig = toml::from_str(&migrated).map_err(|e| {
ConfigError::Message(format!(
"Failed to parse config file {}: {}",
format_path_for_display(&path),
e
))
})?;
self.projects = disk_config.projects;
Ok(())
}
pub fn set_skip_shell_integration_prompt(
&mut self,
config_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
self.with_locked_mutation(config_path, |config| {
if config.skip_shell_integration_prompt {
return false;
}
config.skip_shell_integration_prompt = true;
true
})
}
pub fn set_skip_commit_generation_prompt(
&mut self,
config_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
self.with_locked_mutation(config_path, |config| {
if config.skip_commit_generation_prompt {
return false;
}
config.skip_commit_generation_prompt = true;
true
})
}
pub fn set_project_worktree_path(
&mut self,
project: &str,
worktree_path: String,
config_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
self.with_locked_mutation(config_path, |config| {
let entry = config.projects.entry(project.to_string()).or_default();
if entry.overrides.worktree_path.as_ref() == Some(&worktree_path) {
return false;
}
entry.overrides.worktree_path = Some(worktree_path);
true
})
}
pub fn set_commit_generation_command(
&mut self,
command: String,
config_path: Option<&std::path::Path>,
) -> Result<(), ConfigError> {
self.with_locked_mutation(config_path, |config| {
let commit_config = config
.configs
.commit
.get_or_insert_with(CommitConfig::default);
let gen_config = commit_config
.generation
.get_or_insert_with(CommitGenerationConfig::default);
gen_config.command = Some(command.clone());
true
})
}
}