use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::error::RepoError;
#[cfg(unix)]
fn device_id(path: &Path) -> Option<u64> {
use std::os::unix::fs::MetadataExt;
Some(fs::metadata(path).ok()?.dev())
}
#[cfg(windows)]
fn device_id(_path: &Path) -> Option<u64> {
Some(0)
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Server {
pub alias: String,
pub url: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Project {
pub name: String,
pub git_server_alias: String,
pub git_path: String,
pub path: String,
}
impl Project {
pub fn is_root(&self) -> bool {
self.path.is_empty() || self.path == "."
}
}
const CURRENT_VERSION: u32 = 1;
fn validate_version(version: u32, file: &str) -> Result<(), RepoError> {
if version > CURRENT_VERSION {
return Err(RepoError::Config(format!(
"{file} has version {version}, but this application only supports up to version {CURRENT_VERSION}"
)));
}
Ok(())
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LocalConfig {
pub version: u32,
pub servers: Vec<Server>,
#[serde(default)]
pub autocommit: bool,
#[serde(default)]
pub autoignore: bool,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ProjectsConfig {
pub version: u32,
pub projects: Vec<Project>,
}
fn load_json<T: serde::de::DeserializeOwned>(json_str: &str) -> Result<T, RepoError> {
serde_json::from_str(json_str).map_err(RepoError::Json)
}
pub struct ConfigManager {
pub project_root: PathBuf,
pub local_config: Option<LocalConfig>,
pub projects_config: Option<ProjectsConfig>,
}
impl ConfigManager {
const LOCAL_CONFIG: &'static str = ".repo.json";
const PROJECTS_CONFIG: &'static str = "projects.json";
pub fn find_root(start: &Path) -> Option<PathBuf> {
let start = start.canonicalize().ok()?;
let start_dev = device_id(&start)?;
let mut path = start.clone();
loop {
if path.join(Self::LOCAL_CONFIG).is_file() {
return Some(path);
}
let parent = path.parent()?;
let parent_dev = device_id(parent)?;
if parent_dev != start_dev {
return None;
}
if parent == path {
return None;
}
path = parent.to_path_buf();
}
}
pub fn new() -> Self {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let project_root = Self::find_root(&cwd).unwrap_or(cwd);
ConfigManager {
project_root,
local_config: None,
projects_config: None,
}
}
pub fn local_config_exists(&self) -> bool {
self.project_root.join(Self::LOCAL_CONFIG).is_file()
}
pub fn projects_config_exists(&self) -> bool {
self.project_root.join(Self::PROJECTS_CONFIG).is_file()
}
pub fn read_local_config(&mut self) -> Result<(), RepoError> {
let path = self.project_root.join(Self::LOCAL_CONFIG);
debug!("Loading local config from {}", path.display());
let contents = fs::read_to_string(&path)?;
let config: LocalConfig = load_json(&contents)?;
validate_version(config.version, Self::LOCAL_CONFIG)?;
self.local_config = Some(config);
Ok(())
}
pub fn read_projects_config(&mut self) -> Result<(), RepoError> {
let path = self.project_root.join(Self::PROJECTS_CONFIG);
debug!("Loading projects config from {}", path.display());
let contents = fs::read_to_string(&path)?;
let config: ProjectsConfig = load_json(&contents)?;
validate_version(config.version, Self::PROJECTS_CONFIG)?;
self.projects_config = Some(config);
Ok(())
}
pub fn create_local_config(&self) -> Result<(), RepoError> {
let path = self.project_root.join(Self::LOCAL_CONFIG);
let default = LocalConfig { version: CURRENT_VERSION, servers: vec![], autocommit: false, autoignore: false };
fs::write(&path, serde_json::to_string_pretty(&default)?)?;
Ok(())
}
pub fn create_projects_config(&self) -> Result<(), RepoError> {
let path = self.project_root.join(Self::PROJECTS_CONFIG);
let default = ProjectsConfig { version: CURRENT_VERSION, projects: vec![] };
fs::write(&path, serde_json::to_string_pretty(&default)?)?;
Ok(())
}
pub fn save_local_config(&self) -> Result<(), RepoError> {
let path = self.project_root.join(Self::LOCAL_CONFIG);
let lc = self
.local_config
.as_ref()
.ok_or_else(|| RepoError::Config("Local config not loaded".into()))?;
let json = serde_json::to_string_pretty(lc)?;
fs::write(&path, json)?;
Ok(())
}
pub fn save_projects_config(&self) -> Result<(), RepoError> {
let path = self.project_root.join(Self::PROJECTS_CONFIG);
let pc = self
.projects_config
.as_ref()
.ok_or_else(|| RepoError::Config("Projects config not loaded".into()))?;
let json = serde_json::to_string_pretty(pc)?;
fs::write(&path, json)?;
Ok(())
}
pub fn get_server_url(&self, alias: &str) -> Result<String, RepoError> {
let lc = self
.local_config
.as_ref()
.ok_or_else(|| RepoError::Config("Local config not loaded".into()))?;
lc.servers
.iter()
.find(|s| s.alias == alias)
.map(|s| s.url.clone())
.ok_or_else(|| RepoError::UnknownAlias(alias.to_string()))
}
pub fn get_git_url(&self, project: &Project) -> Result<String, RepoError> {
let server = self.get_server_url(&project.git_server_alias)?;
Ok(format!("{}{}", server, project.git_path))
}
pub fn project_dir(&self, project: &Project) -> PathBuf {
self.project_root.join(&project.path)
}
}