use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use super::Config;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConfigFormat {
Toml,
Json,
}
impl ConfigFormat {
pub fn from_path(path: &Path) -> Self {
match path.extension().and_then(|s| s.to_str()) {
Some("toml") => ConfigFormat::Toml,
Some("json") => ConfigFormat::Json,
_ => ConfigFormat::Toml, }
}
pub fn parse(&self, contents: &str) -> Result<Config> {
match self {
ConfigFormat::Toml => toml::from_str(contents).context("Failed to parse TOML config"),
ConfigFormat::Json => {
serde_json::from_str(contents).context("Failed to parse JSON config")
}
}
}
pub fn serialize(&self, config: &Config) -> Result<String> {
match self {
ConfigFormat::Toml => {
toml::to_string_pretty(config).context("Failed to serialize to TOML")
}
ConfigFormat::Json => {
serde_json::to_string_pretty(config).context("Failed to serialize to JSON")
}
}
}
}
#[derive(Debug)]
pub struct ConfigLocations {
pub repo: Option<PathBuf>,
pub global: PathBuf,
}
impl ConfigLocations {
pub fn get() -> Result<Self> {
let global = if let Ok(config_home) = std::env::var("RCO_CONFIG_HOME") {
PathBuf::from(config_home).join("config.toml")
} else {
let home = dirs::home_dir().context("Could not find home directory")?;
home.join(".config").join("rustycommit").join("config.toml")
};
let ignore_repo_config = std::env::var("RCO_IGNORE_REPO_CONFIG")
.ok()
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let repo = if ignore_repo_config {
None
} else if let Ok(repo) = git2::Repository::open_from_env() {
let workdir = repo
.workdir()
.context("Could not get repository working directory")?;
let possible_configs = [
workdir.join(".rustycommit.toml"),
workdir.join(".rustycommit.json"),
workdir.join(".rco.toml"),
workdir.join(".rco.json"),
];
possible_configs.into_iter().find(|p| p.exists())
} else {
None
};
Ok(ConfigLocations { repo, global })
}
pub fn load_merged() -> Result<Config> {
let locations = Self::get()?;
let mut config = Config::default();
if locations.global.exists() {
if let Ok(contents) = fs::read_to_string(&locations.global) {
let format = ConfigFormat::from_path(&locations.global);
match format.parse(&contents) {
Ok(global_config) => config.merge(global_config),
Err(e) => tracing::warn!(
"Failed to parse global config at {}: {}",
locations.global.display(),
e
),
}
}
}
if let Some(repo_path) = &locations.repo {
if let Ok(contents) = fs::read_to_string(repo_path) {
let format = ConfigFormat::from_path(repo_path);
match format.parse(&contents) {
Ok(repo_config) => config.merge(repo_config),
Err(e) => tracing::warn!(
"Failed to parse repo config at {}: {}",
repo_path.display(),
e
),
}
}
}
config.load_from_environment();
if config.api_key.is_none() {
if let Ok(Some(key)) = crate::config::secure_storage::get_secret("RCO_API_KEY") {
config.api_key = Some(key);
}
}
if config.api_key.is_none() {
if let Some(_token) = crate::auth::token_storage::get_access_token()
.ok()
.flatten()
{
}
}
Ok(config)
}
pub fn save(config: &Config, location: ConfigLocation) -> Result<()> {
let locations = Self::get()?;
let (path, format) = match location {
ConfigLocation::Global => {
if let Some(parent) = locations.global.parent() {
fs::create_dir_all(parent)?;
}
(locations.global, ConfigFormat::Toml)
}
ConfigLocation::Repo => {
let path = locations.repo.unwrap_or_else(|| {
if let Ok(repo) = git2::Repository::open_from_env() {
if let Some(workdir) = repo.workdir() {
return workdir.join(".rustycommit.toml");
}
}
PathBuf::from(".rustycommit.toml")
});
let format = ConfigFormat::from_path(&path);
(path, format)
}
};
let contents = format.serialize(config)?;
fs::write(&path, contents).context("Failed to write config file")?;
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub enum ConfigLocation {
Global,
#[allow(dead_code)]
Repo,
}