use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use super::record::{Project, derive_name_from_url};
use super::store::{ProjectStore, ProjectStoreError};
use crate::core::trusty_tools_config::ProjectConfig;
use crate::session_manager::record::SessionRecord;
#[derive(Debug, Clone)]
pub struct ProjectRegistry {
store: Arc<RwLock<ProjectStore>>,
}
impl ProjectRegistry {
pub async fn load(data_dir: &Path) -> Result<Self, ProjectStoreError> {
let store = ProjectStore::load(data_dir).await?;
Ok(Self {
store: Arc::new(RwLock::new(store)),
})
}
pub async fn register(&self, project: Project) -> Result<(), ProjectStoreError> {
let mut guard = self.store.write().await;
guard.upsert(project).await
}
pub async fn get(&self, name: &str) -> Result<Project, ProjectStoreError> {
let mut guard = self.store.write().await;
guard.get(name).await
}
pub async fn list(&self) -> Result<Vec<Project>, ProjectStoreError> {
let mut guard = self.store.write().await;
guard.all().await
}
pub async fn update_with<F>(&self, name: &str, f: F) -> Result<Project, ProjectStoreError>
where
F: FnOnce(Project) -> Project + Send + 'static,
{
let mut guard = self.store.write().await;
guard.update_with(name, f).await
}
pub async fn seed_from_config(&self, config_projects: &[ProjectConfig]) {
for entry in config_projects {
let project = Project {
name: entry.name.clone(),
repo_url: entry.repo_url.clone(),
default_branch: entry
.default_branch
.clone()
.unwrap_or_else(|| "main".to_string()),
stack_hint: entry.stack_hint.clone(),
tags: entry.tags.clone().unwrap_or_default(),
description: entry.description.clone(),
gh_user: entry.gh_user.clone(),
gh_account: entry.gh_account.clone(),
github: entry.github.clone(),
commit_name: entry.commit_name.clone(),
commit_email: entry.commit_email.clone(),
worktree: entry.worktree,
};
let name = project.name.clone();
if let Err(e) = self.register(project).await {
warn!(name = %name, "seed_from_config: failed to register project: {e}");
} else {
debug!(name = %name, "seed_from_config: registered project");
}
}
}
pub async fn auto_register_from_sessions(&self, records: &[SessionRecord]) {
let existing: std::collections::HashSet<String> = match self.list().await {
Ok(projects) => projects.into_iter().map(|p| p.name).collect(),
Err(e) => {
warn!("auto_register: failed to list existing projects: {e}");
return;
}
};
for record in records {
self.register_implicit_from_record(record, &existing, "auto_register")
.await;
}
}
pub async fn register_from_session(&self, record: &SessionRecord) {
let existing: std::collections::HashSet<String> = match self.list().await {
Ok(projects) => projects.into_iter().map(|p| p.name).collect(),
Err(e) => {
warn!("register_from_session: failed to list existing projects: {e}");
return;
}
};
self.register_implicit_from_record(record, &existing, "register_from_session")
.await;
}
async fn register_implicit_from_record(
&self,
record: &SessionRecord,
existing: &std::collections::HashSet<String>,
log_prefix: &str,
) {
let Some(repo_url) = &record.repo_url else {
return;
};
let shown = crate::core::remote_url_redact::redact_stored_url(repo_url);
let Some(name) = derive_name_from_url(repo_url) else {
warn!(
repo_url = %shown,
"{log_prefix}: could not derive project name from URL; skipping"
);
return;
};
if existing.contains(&name) {
debug!(name = %name, "{log_prefix}: project already registered; skipping");
return;
}
let default_branch = record.branch.clone().unwrap_or_else(|| "main".to_string());
let project = Project {
name: name.clone(),
repo_url: repo_url.clone(),
default_branch,
stack_hint: None,
tags: vec![],
description: None,
gh_user: None,
gh_account: None,
github: None,
commit_name: None,
commit_email: None,
worktree: None,
};
if let Err(e) = self.register(project).await {
warn!(name = %name, "{log_prefix}: failed to register: {e}");
} else {
info!(name = %name, repo_url = %shown, "{log_prefix}: registered project from session history");
}
}
}
#[cfg(test)]
#[path = "registry_tests.rs"]
mod tests;