use std::path::{Path, PathBuf};
use eyre::{Result, WrapErr};
use indexmap::IndexMap;
use serde::Deserialize;
use crate::config::Settings;
use crate::config::config_file::mise_toml::MiseToml;
use crate::file::display_path;
#[derive(Debug, Default, Clone, Deserialize)]
pub(crate) struct HistoryTomlConfig {
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub reload: IndexMap<String, String>,
#[serde(default)]
pub origin: Option<OriginTomlConfig>,
#[serde(default)]
pub encryption: Option<FileEncryptionConfig>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub(crate) struct FileEncryptionConfig {
#[serde(default)]
pub recipients: Vec<String>,
}
pub(crate) fn file_recipients() -> Result<Vec<String>> {
let mut recipients = Vec::new();
for (path, layer) in layers()? {
if let Some(encryption) = layer.encryption {
if !crate::config::config_file::is_trusted(&path) {
eyre::bail!(
"trust the configuration before using its encryption recipients: {}",
display_path(&path)
);
}
recipients = encryption.recipients;
if recipients.is_empty() {
eyre::bail!(
"[history.encryption].recipients must not be empty; configure recipients before capturing encrypted files"
);
}
}
}
Ok(recipients)
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub(crate) struct OriginTomlConfig {
pub url: String,
#[serde(default = "default_branch")]
pub branch: String,
}
impl OriginTomlConfig {
pub(crate) fn plain(url: String, branch: String) -> Self {
Self { url, branch }
}
}
fn default_branch() -> String {
"main".to_string()
}
pub(crate) fn describe_command() -> Result<Option<String>> {
if let Some(command) = std::env::var_os("MISE_HISTORY_DESCRIBE_COMMAND") {
let command = command
.into_string()
.map_err(|_| eyre::eyre!("MISE_HISTORY_DESCRIBE_COMMAND contains invalid Unicode"))?;
return Ok(nonempty_command(command));
}
let mut found = None;
for path in config_files() {
let settings = match Settings::parse_settings_file(&path) {
Ok(settings) => settings,
Err(err) => {
warn!(
"history: cannot read description command from {}: {err}",
display_path(&path)
);
continue;
}
};
if let Some(command) = settings.history.describe_command {
if !crate::config::config_file::is_trusted(&path) {
warn!(
"history: ignoring settings.history.describe_command in untrusted {}",
display_path(&path)
);
continue;
}
found = Some(command);
}
}
Ok(found.and_then(nonempty_command))
}
fn nonempty_command(command: String) -> Option<String> {
let command = command.trim().to_string();
(!command.is_empty()).then_some(command)
}
pub(crate) fn origin() -> Result<Option<(PathBuf, OriginTomlConfig)>> {
let mut found = None;
for (path, layer) in layers()? {
if let Some(origin) = layer.origin {
super::sync::network::validate_url(&origin.url)?;
found = Some((path, origin));
}
}
Ok(found)
}
pub(crate) fn layers() -> Result<Vec<(PathBuf, HistoryTomlConfig)>> {
let mut layers = vec![];
for path in config_files() {
if let Some(history) = read_layer(&path)? {
layers.push((path, history));
}
}
Ok(layers)
}
fn config_files() -> impl Iterator<Item = PathBuf> {
crate::config::system_config_files()
.into_iter()
.chain(crate::config::global_config_files())
.filter(|path| path.is_file())
.filter(|path| path.extension().is_some_and(|ext| ext == "toml"))
}
fn read_layer(path: &Path) -> Result<Option<HistoryTomlConfig>> {
let toml = MiseToml::from_file(path)
.wrap_err_with(|| format!("cannot read history configuration: {}", display_path(path)))?;
Ok(toml.history_config())
}
pub(crate) fn reload_commands() -> Result<IndexMap<String, String>> {
let mut commands = IndexMap::new();
for (path, layer) in layers()? {
if !crate::config::config_file::is_trusted(&path) {
if !layer.reload.is_empty() {
warn!(
"history: ignoring [history.reload] in untrusted {}",
display_path(&path)
);
}
continue;
}
for (glob, command) in &layer.reload {
commands.insert(glob.clone(), command.clone());
}
}
Ok(commands)
}
pub(crate) fn exclude_globs() -> Result<Vec<String>> {
let mut globs: Vec<String> = vec![];
for (_, layer) in layers()? {
globs.extend(layer.exclude.iter().cloned());
}
Ok(globs)
}
pub(crate) fn exclusion_sources(pattern: &str) -> Vec<PathBuf> {
let Ok(layers) = layers() else {
return vec![];
};
layers
.into_iter()
.filter(|(_, layer)| layer.exclude.iter().any(|glob| glob == pattern))
.map(|(path, _)| path)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn malformed_encryption_is_rejected_and_layer_errors_propagate() {
let error = toml::from_str::<HistoryTomlConfig>("[encryption]\nrecipents = ['typo']\n")
.unwrap_err();
assert!(error.to_string().contains("recipents"));
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("config.toml");
std::fs::write(&path, "[history.encryption]\nrecipents = ['typo']\n").unwrap();
let error = read_layer(&path).unwrap_err();
assert!(format!("{error:#}").contains("config.toml"));
}
}