Skip to content

Commit b341a3a

Browse files
committed
refactor(config): migrate to figment for hierarchical config layering
Replace manual TOML loading and env var parsing with figment providers. Config precedence: CLI > ENV (COMMITBEE_*) > user config > project .commitbee.toml > defaults. Provider-specific API key fallback (OPENAI_API_KEY, ANTHROPIC_API_KEY) retained as post-figment step.
1 parent 4d7bf8e commit b341a3a

1 file changed

Lines changed: 39 additions & 44 deletions

File tree

src/config.rs

Lines changed: 39 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// SPDX-License-Identifier: GPL-3.0-only
44

55
use directories::ProjectDirs;
6+
use figment::Figment;
7+
use figment::providers::{Env, Format, Serialized, Toml};
68
use serde::{Deserialize, Serialize};
79
use std::fs;
810
use std::path::PathBuf;
@@ -70,7 +72,7 @@ pub struct Config {
7072
#[serde(default = "default_ollama_host")]
7173
pub ollama_host: String,
7274

73-
#[serde(skip)]
75+
#[serde(default)]
7476
pub api_key: Option<String>,
7577

7678
#[serde(default = "default_max_diff_lines")]
@@ -151,10 +153,43 @@ impl Default for Config {
151153
}
152154

153155
impl Config {
154-
/// Load with priority: CLI > ENV > file > defaults
156+
/// Load with priority: CLI > ENV > user config > project config > defaults
155157
pub fn load(cli: &Cli) -> Result<Self> {
156-
let mut config = Self::load_from_file()?;
157-
config.apply_env();
158+
let mut figment = Figment::new().merge(Serialized::defaults(Config::default()));
159+
160+
// Project-level config (.commitbee.toml in repo root)
161+
if let Ok(cwd) = std::env::current_dir() {
162+
let project_config = cwd.join(".commitbee.toml");
163+
if project_config.exists() {
164+
figment = figment.merge(Toml::file(&project_config));
165+
}
166+
}
167+
168+
// User-level config
169+
if let Some(path) = Self::config_path() {
170+
if path.exists() {
171+
figment = figment.merge(Toml::file(&path));
172+
}
173+
}
174+
175+
// Environment variables (COMMITBEE_MODEL, COMMITBEE_PROVIDER, etc.)
176+
// Use __ separator for nested keys (e.g., COMMITBEE_FORMAT__INCLUDE_BODY)
177+
figment = figment.merge(Env::prefixed("COMMITBEE_").split("__"));
178+
179+
let mut config: Config = figment
180+
.extract()
181+
.map_err(|e| Error::Config(e.to_string()))?;
182+
183+
// Provider-specific API key fallback
184+
if config.api_key.is_none() {
185+
config.api_key = match config.provider {
186+
Provider::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
187+
Provider::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
188+
Provider::Ollama => None,
189+
};
190+
}
191+
192+
// CLI overrides (highest priority)
158193
config.apply_cli(cli);
159194
config.validate()?;
160195
Ok(config)
@@ -168,46 +203,6 @@ impl Config {
168203
Self::config_dir().map(|d| d.join("config.toml"))
169204
}
170205

171-
fn load_from_file() -> Result<Self> {
172-
let Some(path) = Self::config_path() else {
173-
return Ok(Self::default());
174-
};
175-
176-
if !path.exists() {
177-
return Ok(Self::default());
178-
}
179-
180-
let content = fs::read_to_string(&path)?;
181-
toml::from_str(&content).map_err(|e| Error::Config(e.to_string()))
182-
}
183-
184-
fn apply_env(&mut self) {
185-
if let Ok(p) = std::env::var("COMMITBEE_PROVIDER") {
186-
self.provider = match p.to_lowercase().as_str() {
187-
"openai" => Provider::OpenAI,
188-
"anthropic" => Provider::Anthropic,
189-
_ => Provider::Ollama,
190-
};
191-
}
192-
193-
if let Ok(m) = std::env::var("COMMITBEE_MODEL") {
194-
self.model = m;
195-
}
196-
197-
if let Ok(h) = std::env::var("COMMITBEE_OLLAMA_HOST") {
198-
self.ollama_host = h;
199-
}
200-
201-
// API key: COMMITBEE_API_KEY > provider-specific
202-
self.api_key = std::env::var("COMMITBEE_API_KEY")
203-
.or_else(|_| match self.provider {
204-
Provider::OpenAI => std::env::var("OPENAI_API_KEY"),
205-
Provider::Anthropic => std::env::var("ANTHROPIC_API_KEY"),
206-
Provider::Ollama => Err(std::env::VarError::NotPresent),
207-
})
208-
.ok();
209-
}
210-
211206
fn apply_cli(&mut self, cli: &Cli) {
212207
if let Some(ref p) = cli.provider {
213208
self.provider = match p.to_lowercase().as_str() {

0 commit comments

Comments
 (0)