pub mod author;
pub mod global;
pub mod hook;
pub mod local;
pub mod remote;
pub mod template;
use author::Author;
use global::Global;
use hook::Hooks;
use local::Local;
use remote::RemoteConfig;
use template::Template;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use dialoguer::theme;
use figment::Figment;
use figment::providers::{Format, Toml};
use log::{info, warn};
use serde_derive::{Deserialize, Serialize};
pub const DEFAULT_CONFIG: &str = include_str!("defaults.toml");
pub const REPOSITORY_CONFIG_FILE: &str = "config";
pub const GLOBAL_CONFIG_FILE: &str = ".pijulconfig";
pub const CONFIG_DIR: &str = "pijul";
pub const CONFIG_FILE: &str = "config.toml";
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Shared {
pub unrecord_changes: Option<usize>,
pub reset_overwrites_changes: Option<Choice>,
pub colors: Option<Choice>,
pub pager: Option<Choice>,
pub template: Option<Template>,
#[serde(default)]
pub hooks: Hooks,
}
#[derive(Debug, Default, Deserialize)]
pub struct Config {
#[serde(skip)]
global_config: Option<Global>,
#[serde(skip)]
local_config: Option<Local>,
#[serde(default)]
pub author: Author,
pub ignore_kinds: HashMap<String, Vec<String>>,
pub default_remote: Option<String>,
pub extra_dependencies: Vec<String>,
pub remotes: Vec<RemoteConfig>,
pub unrecord_changes: Option<usize>,
pub reset_overwrites_changes: Choice,
pub colors: Choice,
pub pager: Choice,
pub template: Option<Template>,
#[serde(default)]
pub hooks: hook::Hooks,
}
impl Config {
pub fn load(
repository_path: Option<&Path>,
config_overrides: Vec<(String, String)>,
) -> Result<Self, anyhow::Error> {
let global_config = match Global::config_file() {
Some(global_config_path) => match Global::read_contents(&global_config_path) {
Ok(contents) => Some((global_config_path, contents)),
Err(error) => {
warn!("Unable to read global config file: {error:#?}");
None
}
},
None => {
warn!("Unable to find global configuration path");
None
}
};
let local_config = match repository_path {
Some(repository_path) => match Local::read_contents(&repository_path) {
Ok(contents) => Some((repository_path.to_path_buf(), contents)),
Err(error) => {
warn!("Unable to read global config file: {error:#?}");
None
}
},
None => {
info!(
"Skipping local configuration path - repository path was not supplied by caller"
);
None
}
};
Self::load_with(global_config, local_config, config_overrides)
}
pub fn load_with(
global_config_file: Option<(PathBuf, String)>,
local_config_file: Option<(PathBuf, String)>,
config_overrides: Vec<(String, String)>,
) -> Result<Self, anyhow::Error> {
let mut layers = Figment::new();
layers = layers.merge(Toml::string(DEFAULT_CONFIG));
let global_config = match global_config_file {
Some((path, contents)) => {
let global_config = Global::parse_contents(&path, &contents)?;
layers = layers.merge(Toml::string(&contents));
Some(global_config)
}
None => None,
};
let local_config = match local_config_file {
Some((path, contents)) => {
let global_config = Local::parse_contents(&path, &contents)?;
layers = layers.merge(Toml::string(&contents));
Some(global_config)
}
None => None,
};
for (key, value) in config_overrides {
layers = layers.join((key, value));
}
let mut config: Self = layers.extract()?;
assert!(config.global_config.is_none());
assert!(config.local_config.is_none());
config.global_config = global_config;
config.local_config = local_config;
Ok(config)
}
pub fn global(&self) -> Option<Global> {
self.global_config.clone()
}
pub fn local(&self) -> Option<Local> {
self.local_config.clone()
}
pub fn dot_ignore_contents(&self, ignore_kind: Option<&str>) -> Result<String, anyhow::Error> {
let default_ignore_lines = self.ignore_kinds.get("default").unwrap();
let extra_ignore_lines = match ignore_kind {
Some(kind) => match self.ignore_kinds.get(kind) {
Some(extra_ignore_lines) => extra_ignore_lines.iter(),
None => {
return Err(anyhow::anyhow!(
"Unable to find specific ignore kind: {kind}"
));
}
},
None => [].iter(),
};
let mut ignore_lines = default_ignore_lines
.iter()
.chain(extra_ignore_lines)
.map(|line| line.as_str())
.collect::<Vec<_>>()
.join("\n");
if !ignore_lines.is_empty() {
ignore_lines.push('\n');
}
Ok(ignore_lines)
}
pub fn theme(&self) -> Box<dyn theme::Theme + Send + Sync> {
match self.colors {
Choice::Auto | Choice::Always => Box::new(theme::ColorfulTheme::default()),
Choice::Never => Box::new(theme::SimpleTheme),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Choice {
#[default]
#[serde(rename = "auto")]
Auto,
#[serde(rename = "always")]
Always,
#[serde(rename = "never")]
Never,
}
pub fn global_config_directory() -> Option<PathBuf> {
std::env::var("PIJUL_CONFIG_DIR")
.ok()
.map(PathBuf::from)
.or_else(|| match dirs_next::config_dir() {
Some(global_config_dir) => Some(global_config_dir.join(CONFIG_DIR)),
None => None,
})
.or_else(|| match dirs_next::home_dir() {
Some(home_dir) => Some(home_dir.join(CONFIG_DIR)),
None => None,
})
}
pub fn parse_config_arg(argument: &str) -> Result<(String, String), anyhow::Error> {
let (key, value) = argument
.split_once('=')
.ok_or(anyhow::anyhow!("Unable to find '=' character"))?;
Ok((key.to_string(), value.to_string()))
}