use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default = "default_model")]
pub model: String,
#[serde(default = "default_temperature")]
pub temperature: f32,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
pub system_prompt: Option<String>,
pub api_key: Option<String>,
pub base_url: Option<String>,
#[serde(default)]
pub plugins: PluginConfig,
#[serde(default)]
pub exclude_patterns: Vec<String>,
#[serde(default)]
pub paths: HashMap<String, PathConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathConfig {
#[serde(default)]
pub focus: Vec<String>,
#[serde(default)]
pub ignore_patterns: Vec<String>,
#[serde(default)]
pub extra_context: Vec<String>,
pub system_prompt: Option<String>,
#[serde(default)]
pub severity_overrides: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginConfig {
#[serde(default = "default_true")]
pub eslint: bool,
#[serde(default = "default_true")]
pub semgrep: bool,
#[serde(default = "default_true")]
pub duplicate_filter: bool,
}
impl Default for PathConfig {
fn default() -> Self {
Self {
focus: Vec::new(),
ignore_patterns: Vec::new(),
extra_context: Vec::new(),
system_prompt: None,
severity_overrides: HashMap::new(),
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
model: default_model(),
temperature: default_temperature(),
max_tokens: default_max_tokens(),
system_prompt: None,
api_key: None,
base_url: None,
plugins: PluginConfig::default(),
exclude_patterns: Vec::new(),
paths: HashMap::new(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = PathBuf::from(".diffscope.yml");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let config: Config = serde_yaml::from_str(&content)?;
return Ok(config);
}
let alt_config_path = PathBuf::from(".diffscope.yaml");
if alt_config_path.exists() {
let content = std::fs::read_to_string(&alt_config_path)?;
let config: Config = serde_yaml::from_str(&content)?;
return Ok(config);
}
if let Some(home_dir) = dirs::home_dir() {
let home_config = home_dir.join(".diffscope.yml");
if home_config.exists() {
let content = std::fs::read_to_string(&home_config)?;
let config: Config = serde_yaml::from_str(&content)?;
return Ok(config);
}
}
Ok(Config::default())
}
pub fn merge_with_cli(&mut self, cli_model: Option<String>, cli_prompt: Option<String>) {
if let Some(model) = cli_model {
self.model = model;
}
if let Some(prompt) = cli_prompt {
self.system_prompt = Some(prompt);
}
}
pub fn get_path_config(&self, file_path: &PathBuf) -> Option<&PathConfig> {
let file_path_str = file_path.to_string_lossy();
let mut best_match: Option<(&String, &PathConfig)> = None;
for (pattern, config) in &self.paths {
if self.path_matches(&file_path_str, pattern) {
if best_match.is_none() || pattern.len() > best_match.unwrap().0.len() {
best_match = Some((pattern, config));
}
}
}
best_match.map(|(_, config)| config)
}
pub fn should_exclude(&self, file_path: &PathBuf) -> bool {
let file_path_str = file_path.to_string_lossy();
for pattern in &self.exclude_patterns {
if self.path_matches(&file_path_str, pattern) {
return true;
}
}
if let Some(path_config) = self.get_path_config(file_path) {
for pattern in &path_config.ignore_patterns {
if self.path_matches(&file_path_str, pattern) {
return true;
}
}
}
false
}
fn path_matches(&self, path: &str, pattern: &str) -> bool {
if pattern.contains('*') {
if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
glob_pattern.matches(path)
} else {
false
}
} else {
path.starts_with(pattern)
}
}
}
fn default_model() -> String {
"gpt-4o".to_string()
}
fn default_temperature() -> f32 {
0.2
}
fn default_max_tokens() -> usize {
4000
}
fn default_true() -> bool {
true
}