use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{GwmError, Result};
use super::merger::merge_configs;
use super::types::Config;
const GLOBAL_CONFIG_PATHS: &[&str] = &[".config/gwm/config.toml", ".gwmrc"];
const PROJECT_CONFIG_DIRS: &[&str] = &[".gwm", "gwm"];
#[derive(Debug)]
pub struct ConfigWithSource {
pub config: Config,
pub global_config_path: Option<PathBuf>,
pub project_config_path: Option<PathBuf>,
pub has_project_hooks: bool,
pub repo_root: Option<PathBuf>,
}
fn find_global_config() -> Option<PathBuf> {
let home = dirs::home_dir()?;
for relative_path in GLOBAL_CONFIG_PATHS {
let path = home.join(relative_path);
if path.exists() {
return Some(path);
}
}
None
}
fn find_project_config(repo_root: &Path) -> Option<PathBuf> {
for dir in PROJECT_CONFIG_DIRS {
let path = repo_root.join(dir).join("config.toml");
if path.exists() {
return Some(path);
}
}
None
}
pub fn find_repo_root() -> Option<PathBuf> {
let current = std::env::current_dir().ok()?;
find_repo_root_from(¤t)
}
pub fn find_repo_root_from(start: &Path) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
if current.join(".git").exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
fn load_config_from_file(path: &Path) -> Result<Config> {
let content = fs::read_to_string(path)
.map_err(|e| GwmError::config(format!("failed to read config file {:?}: {}", path, e)))?;
toml::from_str(&content)
.map_err(|e| GwmError::config(format!("failed to parse config file {:?}: {}", path, e)))
}
pub fn load_global_config() -> (Config, Option<PathBuf>) {
match find_global_config() {
Some(path) => match load_config_from_file(&path) {
Ok(config) => (config, Some(path)),
Err(e) => {
eprintln!(
"Warning: Failed to parse global config file {:?}: {}",
path, e
);
eprintln!("Using default configuration instead.");
(Config::default(), None)
}
},
None => (Config::default(), None),
}
}
pub fn load_config_with_source() -> ConfigWithSource {
let (global_config, global_path) = load_global_config();
let repo_root = find_repo_root();
let (project_config, project_path, has_project_hooks) = match &repo_root {
Some(root) => match find_project_config(root) {
Some(path) => match load_config_from_file(&path) {
Ok(config) => {
let has_hooks = config
.hooks
.as_ref()
.and_then(|h| h.post_create.as_ref())
.map(|h| h.enabled && !h.commands.is_empty())
.unwrap_or(false);
(Some(config), Some(path), has_hooks)
}
Err(e) => {
eprintln!(
"Warning: Failed to parse project config file {:?}: {}",
path, e
);
eprintln!("Project-specific settings will be ignored.");
(None, None, false)
}
},
None => (None, None, false),
},
None => (None, None, false),
};
let merged_config = match project_config {
Some(project) => merge_configs(&global_config, &project),
None => global_config,
};
ConfigWithSource {
config: merged_config,
global_config_path: global_path,
project_config_path: project_path,
has_project_hooks,
repo_root,
}
}
impl ConfigWithSource {
pub fn build_hook_context(
&self,
worktree_path: &Path,
branch_name: &str,
) -> crate::hooks::HookContext {
let repo_root = self.repo_root.clone().unwrap_or_else(|| {
eprintln!(
"\x1b[33m⚠ Warning: Could not determine repository root, using current directory for hooks\x1b[0m"
);
PathBuf::from(".")
});
let repo_name = self
.repo_root
.as_ref()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| {
"unknown".to_string()
});
crate::hooks::HookContext {
worktree_path: worktree_path.to_path_buf(),
branch_name: branch_name.to_string(),
repo_root,
repo_name,
}
}
pub fn get_post_create_commands(&self) -> Vec<String> {
self.config
.post_create_commands()
.map(|c| c.to_vec())
.unwrap_or_default()
}
}
pub fn load_config() -> Config {
load_config_with_source().config
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_find_repo_root() {
let temp = TempDir::new().unwrap();
let repo_root = temp.path();
fs::create_dir(repo_root.join(".git")).unwrap();
let subdir = repo_root.join("src").join("deep");
fs::create_dir_all(&subdir).unwrap();
let found = find_repo_root_from(&subdir);
assert_eq!(found, Some(repo_root.to_path_buf()));
let found = find_repo_root_from(repo_root);
assert_eq!(found, Some(repo_root.to_path_buf()));
}
#[test]
fn test_load_config_from_file() {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let content = r#"
worktree_base_path = "/custom/path"
main_branches = ["main", "develop"]
clean_branch = "auto"
"#;
fs::write(&config_path, content).unwrap();
let config = load_config_from_file(&config_path).unwrap();
assert_eq!(config.worktree_base_path, "/custom/path");
assert_eq!(config.main_branches, vec!["main", "develop"]);
assert_eq!(
config.clean_branch,
super::super::types::CleanBranchMode::Auto
);
}
#[test]
fn test_load_default_config() {
let config = Config::default();
assert_eq!(config.worktree_base_path, "~/git-worktrees");
}
#[test]
fn test_build_hook_context() {
let temp = TempDir::new().unwrap();
let repo_root = temp.path().join("my-repo");
fs::create_dir(&repo_root).unwrap();
let config_source = ConfigWithSource {
config: Config::default(),
global_config_path: None,
project_config_path: None,
has_project_hooks: false,
repo_root: Some(repo_root.clone()),
};
let worktree_path = temp.path().join("worktree");
let context = config_source.build_hook_context(&worktree_path, "feature/test");
assert_eq!(context.worktree_path, worktree_path);
assert_eq!(context.branch_name, "feature/test");
assert_eq!(context.repo_root, repo_root);
assert_eq!(context.repo_name, "my-repo");
}
#[test]
fn test_build_hook_context_without_repo_root() {
let config_source = ConfigWithSource {
config: Config::default(),
global_config_path: None,
project_config_path: None,
has_project_hooks: false,
repo_root: None,
};
let worktree_path = Path::new("/tmp/worktree");
let context = config_source.build_hook_context(worktree_path, "main");
assert_eq!(context.worktree_path, worktree_path);
assert_eq!(context.branch_name, "main");
assert_eq!(context.repo_root, PathBuf::from("."));
assert_eq!(context.repo_name, "unknown");
}
#[test]
fn test_get_post_create_commands_empty() {
let config_source = ConfigWithSource {
config: Config::default(),
global_config_path: None,
project_config_path: None,
has_project_hooks: false,
repo_root: None,
};
let commands = config_source.get_post_create_commands();
assert!(commands.is_empty());
}
}