pub mod credentials;
pub mod hidden;
pub mod types;
pub mod updates;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use types::{Config, JiraConfig, ResolvedTeam, TeamConfig, TeamJiraOverride, TeamRef};
pub struct LoadedConfig {
pub config: Config,
pub teams: Vec<ResolvedTeam>,
pub load_errors: Vec<String>,
}
pub fn load() -> Result<LoadedConfig> {
let user_path = user_config_path()?;
let config: Config = if user_path.exists() {
load_file(&user_path)?
} else {
Config::default()
};
let mut teams = Vec::new();
let mut load_errors = Vec::new();
for team_ref in &config.teams {
match load_team_config(team_ref) {
Ok(team_config) => {
let jira = resolve_team_jira(&config.jira, &team_config);
teams.push(ResolvedTeam {
id: team_ref.id.clone(),
path: team_ref.path.clone(),
config: team_config,
jira,
});
}
Err(e) => {
load_errors.push(format!("team '{}': {e:#}", team_ref.id));
}
}
}
Ok(LoadedConfig {
config,
teams,
load_errors,
})
}
pub fn user_config_path() -> Result<PathBuf> {
Ok(dirs::config_dir()
.context("Cannot determine config directory")?
.join("do-next")
.join("config.json5"))
}
fn load_file<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
json5::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path.display()))
}
fn load_team_config(team_ref: &TeamRef) -> Result<TeamConfig> {
let dir = expand_tilde(&team_ref.path);
let file_name = team_ref.file.as_deref().unwrap_or("do-next.json5");
let path = dir.join(file_name);
load_file(&path).with_context(|| format!("Failed to load team '{}' config", team_ref.id))
}
fn resolve_team_jira(default: &JiraConfig, team: &TeamConfig) -> JiraConfig {
let Some(ref overlay) = team.jira else {
return default.clone();
};
let mut jira = default.clone();
apply_team_jira_override(&mut jira, overlay);
jira
}
pub fn apply_team_jira_override(base: &mut JiraConfig, overlay: &TeamJiraOverride) {
if let Some(ref v) = overlay.base_url {
base.base_url.clone_from(v);
}
if let Some(ref v) = overlay.default_project {
base.default_project.clone_from(v);
}
if overlay.email.is_some() {
base.email.clone_from(&overlay.email);
}
if overlay.credential_command.is_some() {
base.credential_command
.clone_from(&overlay.credential_command);
}
if overlay.credential_store.is_some() {
base.credential_store.clone_from(&overlay.credential_store);
}
if overlay.credential_key.is_some() {
base.credential_key.clone_from(&overlay.credential_key);
}
if overlay.auth_method.is_some() {
base.auth_method.clone_from(&overlay.auth_method);
}
if overlay.oauth_client_id.is_some() {
base.oauth_client_id.clone_from(&overlay.oauth_client_id);
}
if overlay.oauth_client_secret.is_some() {
base.oauth_client_secret
.clone_from(&overlay.oauth_client_secret);
}
}
pub fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
{
return home.join(rest);
}
PathBuf::from(path)
}