use crate::{StorageError, StorageLayout, WorktreeId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RegistryEntry {
pub worktree_id: WorktreeId,
pub root_path: String,
pub last_accessed_at: String,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProjectRegistry {
pub entries: HashMap<String, RegistryEntry>,
}
impl ProjectRegistry {
pub fn load(layout: &StorageLayout) -> Result<Self, StorageError> {
let path = layout.global_registry();
if !path.exists() {
return Ok(Self::default());
}
let data = std::fs::read_to_string(&path)?;
let reg: Self = serde_json::from_str(&data)?;
Ok(reg)
}
pub fn save(&self, layout: &StorageLayout) -> Result<(), StorageError> {
let path = layout.global_registry();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)?;
std::fs::write(&path, json)?;
Ok(())
}
pub fn upsert(&mut self, worktree: &WorktreeId, root_path: &Path) {
let key = worktree.key();
let _ = self.entries.insert(
key,
RegistryEntry {
worktree_id: worktree.clone(),
root_path: root_path.display().to_string(),
last_accessed_at: chrono::Utc::now().to_rfc3339(),
tags: Vec::new(),
},
);
}
pub fn touch(&mut self, worktree: &WorktreeId) {
if let Some(entry) = self.entries.get_mut(&worktree.key()) {
entry.last_accessed_at = chrono::Utc::now().to_rfc3339();
}
}
pub fn remove(&mut self, worktree: &WorktreeId) {
let _ = self.entries.remove(&worktree.key());
}
pub fn add_tag(&mut self, worktree: &WorktreeId, tag: impl Into<String>) {
let tag = tag.into();
if let Some(entry) = self.entries.get_mut(&worktree.key())
&& !entry.tags.contains(&tag)
{
entry.tags.push(tag);
}
}
#[must_use]
pub fn get(&self, worktree: &WorktreeId) -> Option<&RegistryEntry> {
self.entries.get(&worktree.key())
}
#[must_use]
pub fn list_recent(&self) -> Vec<&RegistryEntry> {
let mut entries: Vec<_> = self.entries.values().collect();
entries.sort_by(|a, b| b.last_accessed_at.cmp(&a.last_accessed_at));
entries
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::identity::RepoId;
use tempfile::tempdir;
fn dummy_worktree(name: &str) -> WorktreeId {
WorktreeId::from_parts(
RepoId::from_string(format!("repo-{name}")),
format!("{name}hash000000"),
format!("{name}@main"),
)
}
fn test_layout() -> (StorageLayout, tempfile::TempDir) {
let dir = tempdir().expect("tempdir");
let layout = StorageLayout::with_root(dir.path(), "synwire");
(layout, dir)
}
#[test]
fn empty_registry_when_absent() {
let (layout, _dir) = test_layout();
let reg = ProjectRegistry::load(&layout).expect("load");
assert!(reg.entries.is_empty());
}
#[test]
fn upsert_and_round_trip() {
let (layout, _dir) = test_layout();
let wid = dummy_worktree("a");
let root = std::path::PathBuf::from("/tmp/my-repo");
let mut reg = ProjectRegistry::load(&layout).expect("load");
reg.upsert(&wid, &root);
reg.save(&layout).expect("save");
let reg2 = ProjectRegistry::load(&layout).expect("reload");
assert!(reg2.get(&wid).is_some());
assert_eq!(reg2.get(&wid).expect("entry").root_path, "/tmp/my-repo");
}
#[test]
fn list_recent_orders_by_timestamp() {
let (_layout, _dir) = test_layout();
let wa = dummy_worktree("a");
let wb = dummy_worktree("b");
let root = std::path::PathBuf::from("/tmp");
let mut reg = ProjectRegistry::default();
reg.upsert(&wa, &root);
std::thread::sleep(std::time::Duration::from_millis(5));
reg.upsert(&wb, &root);
let recent = reg.list_recent();
assert_eq!(recent[0].worktree_id.key(), wb.key());
}
#[test]
fn tag_added_and_present() {
let wid = dummy_worktree("tagged");
let root = std::path::PathBuf::from("/tmp");
let mut reg = ProjectRegistry::default();
reg.upsert(&wid, &root);
reg.add_tag(&wid, "important");
assert!(
reg.get(&wid)
.expect("entry")
.tags
.contains(&"important".to_owned())
);
}
#[test]
fn remove_entry() {
let wid = dummy_worktree("rm");
let root = std::path::PathBuf::from("/tmp");
let mut reg = ProjectRegistry::default();
reg.upsert(&wid, &root);
reg.remove(&wid);
assert!(reg.get(&wid).is_none());
}
}