use crate::config_error::ConfigError;
use crate::config_parse::parse_config;
use crate::config_schema::CliGitConfig;
use std::io::Read;
use std::path::{Path, PathBuf};
pub const CONFIG_FILE_NAME: &str = "cli-git.config.jsonc";
pub const LEGACY_CONFIG_FILE_NAMES: &[&str] = &["cli-git.config.mjs", "cli-git.config.ts"];
pub const MAX_CONFIG_BYTES: u64 = 1_048_576;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LoadedConfig {
pub config: CliGitConfig,
pub source: Option<PathBuf>,
pub ignored_legacy: Vec<PathBuf>,
}
fn entry_exists(path: &Path) -> Result<bool, ConfigError> {
match std::fs::symlink_metadata(path) {
Ok(_) => return Ok(true),
Err(error) => {
if error.kind() == std::io::ErrorKind::NotFound {
return Ok(false);
}
return Err(ConfigError::new(
format!("Cannot inspect {}: {error}.", path.display()).as_str(),
));
}
}
}
fn migration_required(legacy: &Path, target: &Path) -> ConfigError {
return ConfigError::new(
format!(
"Legacy configuration {} is not executed or read by the native cli-git. \
Translate its settings into {} (data only: policies, hooks, indexLock, landing), \
then remove the legacy file. Plugins, executable paths, command arrays and trust \
settings have no JSONC equivalent because every policy is built in.",
legacy.display(),
target.display()
)
.as_str(),
);
}
pub fn ignored_legacy_notice(legacy: &Path, source: &Path) -> String {
return format!(
"Legacy configuration {} is ignored: {} is authoritative for the native cli-git. \
Remove the legacy file once no TypeScript cli-git reads it.",
legacy.display(),
source.display()
);
}
fn read_config_text(path: &Path) -> Result<String, ConfigError> {
let metadata: std::fs::Metadata = match std::fs::symlink_metadata(path) {
Ok(found) => found,
Err(error) => {
return Err(ConfigError::new(
format!("Cannot inspect {}: {error}.", path.display()).as_str(),
));
}
};
if metadata.file_type().is_symlink() {
return Err(ConfigError::new(
format!(
"Configuration path must not be a symbolic link: {}. Replace it with a regular file.",
path.display()
)
.as_str(),
));
}
if !metadata.is_file() {
return Err(ConfigError::new(
format!(
"Configuration path must be a regular file: {}.",
path.display()
)
.as_str(),
));
}
let file: std::fs::File = match std::fs::File::open(path) {
Ok(opened) => opened,
Err(error) => {
return Err(ConfigError::new(
format!("Cannot open {}: {error}.", path.display()).as_str(),
));
}
};
let mut bytes: Vec<u8> = Vec::<u8>::new();
if let Err(error) = file.take(MAX_CONFIG_BYTES + 1).read_to_end(&mut bytes) {
return Err(ConfigError::new(
format!("Cannot read {}: {error}.", path.display()).as_str(),
));
}
if bytes.len() as u64 > MAX_CONFIG_BYTES {
return Err(ConfigError::new(
format!(
"Configuration file {} is larger than {MAX_CONFIG_BYTES} bytes.",
path.display()
)
.as_str(),
));
}
match String::from_utf8(bytes) {
Ok(text) => return Ok(text),
Err(error) => {
return Err(ConfigError::new(
format!(
"Configuration file {} is not UTF-8 text: {error}.",
path.display()
)
.as_str(),
));
}
}
}
pub fn load_repository_config(repository_root: &Path) -> Result<LoadedConfig, ConfigError> {
let source: PathBuf = repository_root.join(CONFIG_FILE_NAME);
let mut legacy: Vec<PathBuf> = Vec::<PathBuf>::new();
for name in LEGACY_CONFIG_FILE_NAMES {
let candidate: PathBuf = repository_root.join(name);
if entry_exists(candidate.as_path())? {
legacy.push(candidate);
}
}
if !entry_exists(source.as_path())? {
if let Some(first) = legacy.first() {
return Err(migration_required(first.as_path(), source.as_path()));
}
return Ok(LoadedConfig {
config: CliGitConfig::defaults(),
source: None,
ignored_legacy: legacy,
});
}
let text: String = read_config_text(source.as_path())?;
let body: &str = text.strip_prefix('\u{feff}').unwrap_or(text.as_str());
match parse_config(body) {
Ok(config) => {
return Ok(LoadedConfig {
config,
source: Some(source),
ignored_legacy: legacy,
});
}
Err(error) => {
return Err(ConfigError::new(
format!("{}: {}", source.display(), error.message).as_str(),
));
}
}
}
#[cfg(test)]
#[path = "config_file_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "config_repository_tests.rs"]
mod repository_tests;