1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Configuration loading logic
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use super::schema::{Config, IntegrationsConfig};
impl Config {
/// Load configuration from a TOML file
///
/// # Errors
/// Returns an error if the file cannot be read or parsed
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
let config: Self = toml::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path.display()))?;
Ok(config)
}
/// Load configuration with fallback (from current working directory)
///
/// This function is provided for backward compatibility and cases where
/// you're not in a git repository. For git repository operations, prefer
/// `load_from_repo_root()` to ensure consistent behavior across worktrees.
///
/// Load priority:
/// 1. Local config (.ofsht.toml in current directory)
/// 2. Global config (~/.config/ofsht/config.toml)
/// 3. Default config
///
/// # Errors
/// Returns an error if configuration files exist but cannot be read or parsed
#[allow(dead_code)]
pub fn load() -> Result<Self> {
Self::load_impl(None)
}
/// Load configuration with fallback (from specified repository root)
///
/// This is the recommended way to load config for git operations. It ensures
/// that .ofsht.toml is always loaded from the main repository root, not from
/// individual worktrees.
///
/// Load priority:
/// 1. Local config (.ofsht.toml in `repo_root` directory)
/// 2. Global config (~/.config/ofsht/config.toml)
/// 3. Default config
///
/// # Arguments
/// * `repo_root` - Path to the main repository root (from `get_main_repo_root()`)
///
/// # Errors
/// Returns an error if configuration files exist but cannot be read or parsed
pub fn load_from_repo_root(repo_root: &Path) -> Result<Self> {
Self::load_impl(Some(repo_root))
}
/// Load integration settings from global config
/// Falls back to default if global config doesn't exist or can't be read
fn load_integration_from_global() -> IntegrationsConfig {
Self::global_config_path()
.and_then(|path| {
if path.exists() {
Self::from_file(&path).ok()
} else {
None
}
})
.map(|config| config.integrations)
.unwrap_or_default()
}
/// Internal implementation for config loading
fn load_impl(repo_root: Option<&Path>) -> Result<Self> {
// Try local config first
let local_config = repo_root.map_or_else(Self::local_config_path, |root| {
Self::local_config_path_from(root)
});
if local_config.exists() {
let mut config = Self::from_file(&local_config)?;
// Integration configuration is only available in global config
// Load integration settings from global config (or defaults if unavailable)
config.integrations = Self::load_integration_from_global();
return Ok(config);
}
// Try global config
if let Some(global_config) = Self::global_config_path() {
if global_config.exists() {
return Self::from_file(&global_config);
}
}
// Return default config
Ok(Self::default())
}
/// Get the local config path from a specific directory
/// Returns the path to .ofsht.toml in the specified directory
#[must_use]
pub fn local_config_path_from(repo_root: &Path) -> PathBuf {
repo_root.join(".ofsht.toml")
}
/// Get the local config path
/// Returns the path to the local config file in the current directory
#[must_use]
pub fn local_config_path() -> PathBuf {
PathBuf::from(".ofsht.toml")
}
/// Get the global config path
/// Respects `XDG_CONFIG_HOME` environment variable on all platforms.
/// Fallback: `$HOME/.config/ofsht/config.toml`
#[must_use]
pub fn global_config_path() -> Option<PathBuf> {
let config_home = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| dirs::home_dir().map(|home| home.join(".config")))?;
Some(config_home.join("ofsht").join("config.toml"))
}
/// Merge this config with another (other takes precedence)
#[must_use]
#[allow(dead_code)]
pub fn merge(&self, other: &Self) -> Self {
Self {
hooks: self.hooks.merge(&other.hooks),
worktree: other.worktree.clone(),
integrations: other.integrations.clone(),
}
}
}