use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize)]
pub struct WorktreeConfig {
#[serde(rename = "copy-patterns", default)]
pub copy_patterns: CopyPatterns,
#[serde(rename = "symlink-patterns", default)]
pub symlink_patterns: SymlinkPatterns,
#[serde(rename = "on-create", default)]
pub on_create: OnCreate,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct CopyPatterns {
#[serde(default)]
pub include: Option<Vec<String>>,
#[serde(default)]
pub exclude: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct SymlinkPatterns {
#[serde(default)]
pub include: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct OnCreate {
#[serde(default)]
pub commands: Option<Vec<String>>,
}
impl Default for WorktreeConfig {
fn default() -> Self {
Self {
copy_patterns: CopyPatterns {
include: Some(Self::default_include_patterns()),
exclude: Some(Self::default_exclude_patterns()),
},
symlink_patterns: SymlinkPatterns { include: None },
on_create: OnCreate { commands: None },
}
}
}
impl WorktreeConfig {
fn default_include_patterns() -> Vec<String> {
vec![
".env*".to_string(),
".vscode/".to_string(),
"*.local.json".to_string(),
"config/local/*".to_string(),
]
}
fn default_exclude_patterns() -> Vec<String> {
vec![
"node_modules/".to_string(),
"target/".to_string(),
".git/".to_string(),
"*.log".to_string(),
"*.tmp".to_string(),
]
}
pub fn load_from_repo(repo_path: &Path) -> Result<Self> {
let config_path = repo_path.join(".worktree-config.toml");
if !config_path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
if content.trim().is_empty() {
return Ok(Self::default());
}
match toml::from_str::<WorktreeConfig>(&content) {
Ok(config) => Ok(config.merged_with_defaults()),
Err(e) => {
eprintln!("Warning: Invalid TOML syntax in .worktree-config.toml:");
eprintln!(" {}", e);
eprintln!(" Using default configuration. Please fix the syntax and try again.");
Ok(Self::default())
}
}
}
#[must_use]
pub fn merged_with_defaults(self) -> Self {
let mut merged_includes = Self::default_include_patterns();
let mut merged_excludes = Self::default_exclude_patterns();
if let Some(user_includes) = self.copy_patterns.include {
for pattern in user_includes {
if !merged_includes.contains(&pattern) {
merged_includes.push(pattern);
}
}
}
if let Some(user_excludes) = self.copy_patterns.exclude {
for pattern in user_excludes {
if !merged_excludes.contains(&pattern) {
merged_excludes.push(pattern);
}
}
}
Self {
copy_patterns: CopyPatterns {
include: Some(merged_includes),
exclude: Some(merged_excludes),
},
symlink_patterns: self.symlink_patterns,
on_create: self.on_create,
}
}
}