use std::{
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
pub(crate) const DIR_NAME: &str = ".mnemglobal";
pub(crate) fn default_dir() -> PathBuf {
if let Ok(dir) = std::env::var("MNEM_GLOBAL_DIR") {
return PathBuf::from(dir);
}
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(DIR_NAME)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct RepoEntry {
pub path: PathBuf,
pub added_ts: u64,
pub last_used_ts: u64,
#[serde(default)]
pub default: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct RepoRegistry {
#[serde(default)]
pub repos: Vec<RepoEntry>,
}
impl RepoRegistry {
pub(crate) fn load(global_dir: &Path) -> Result<Self> {
let path = registry_path(global_dir);
if !path.exists() {
return Ok(Self::default());
}
let text =
fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))
}
pub(crate) fn save(&self, global_dir: &Path) -> Result<()> {
let path = registry_path(global_dir);
let text = toml::to_string_pretty(self).context("serialising repos.toml")?;
atomic_write(&path, text.as_bytes())
}
pub(crate) fn register(&mut self, repo_path: &Path, set_default: bool) {
let canon = canonicalize_lax(repo_path);
let now = now_ts();
let already = self.repos.iter().any(|e| e.path == canon);
if already {
for e in self.repos.iter_mut() {
if e.path == canon {
e.last_used_ts = now;
}
if set_default {
e.default = e.path == canon;
}
}
} else {
if set_default {
for e in self.repos.iter_mut() {
e.default = false;
}
}
self.repos.push(RepoEntry {
path: canon,
added_ts: now,
last_used_ts: now,
default: set_default,
label: None,
});
}
}
pub(crate) fn default_repo(&self) -> Option<&RepoEntry> {
self.repos
.iter()
.find(|e| e.default)
.or_else(|| self.repos.iter().max_by_key(|e| e.last_used_ts))
}
pub(crate) fn prune(&mut self) -> Vec<PathBuf> {
let mut removed = Vec::new();
self.repos.retain(|e| {
if e.path.exists() {
true
} else {
removed.push(e.path.clone());
false
}
});
removed
}
}
pub(crate) fn registry_path(global_dir: &Path) -> PathBuf {
global_dir.join("repos.toml")
}
pub(crate) fn bootstrap(global_dir: &Path) -> Result<bool> {
let mnem_dir = global_dir.join(crate::repo::MNEM_DIR);
if mnem_dir.exists() {
return Ok(false);
}
fs::create_dir_all(global_dir).with_context(|| format!("creating {}", global_dir.display()))?;
crate::commands::init::init_mnem_dir(global_dir)
.with_context(|| format!("initialising graph in {}", global_dir.display()))?;
Ok(true)
}
pub(crate) fn register_repo(repo_parent: &Path) {
let global_dir = default_dir();
if !global_dir.exists() {
return;
}
let Ok(mut reg) = RepoRegistry::load(&global_dir) else {
return;
};
reg.register(repo_parent, false);
let _ = reg.save(&global_dir);
}
fn canonicalize_lax(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
let tmp = path.with_extension("toml.tmp");
fs::write(&tmp, data).with_context(|| format!("writing {}", tmp.display()))?;
fs::rename(&tmp, path).with_context(|| format!("renaming to {}", path.display()))?;
Ok(())
}
fn now_ts() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}