use std::sync::Arc;
use gunnar_store::git_storage_trait::GitServe;
use crate::control::ControlStore;
use crate::doc::{impl_doc, DocRef, ShardedMap};
use crate::error::{Error, Result};
use crate::model::{check_branch, HashKind, Namespace, Repo, Visibility, DEFAULT_BRANCH};
use crate::sandbox::{check_component, RepoRef};
pub const CATALOG_REF: &str = "refs/gunnar/catalog";
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct CatalogDoc {
#[serde(default)]
namespaces: ShardedMap<Namespace>,
#[serde(default)]
repos: ShardedMap<Repo>,
}
impl_doc!(CatalogDoc { namespaces, repos });
#[derive(Clone, Debug)]
pub struct NewRepo {
pub path: RepoRef,
pub hash_kind: HashKind,
pub default_branch: Option<String>,
pub visibility: Visibility,
pub description: String,
}
impl NewRepo {
pub fn new(path: RepoRef) -> Self {
NewRepo {
path,
hash_kind: HashKind::Sha256,
default_branch: None,
visibility: Visibility::Private,
description: String::new(),
}
}
pub fn hash_kind(mut self, kind: HashKind) -> Self {
self.hash_kind = kind;
self
}
pub fn visibility(mut self, visibility: Visibility) -> Self {
self.visibility = visibility;
self
}
pub fn default_branch(mut self, name: impl Into<String>) -> Self {
self.default_branch = Some(name.into());
self
}
pub fn prospect(&self) -> Result<Repo> {
let branch = match &self.default_branch {
Some(name) => check_branch(name)?,
None => DEFAULT_BRANCH.to_owned(),
};
Ok(Repo {
namespace: self.path.namespace().to_owned(),
name: self.path.name().to_owned(),
hash_kind: self.hash_kind,
default_branch: branch,
visibility: self.visibility,
created: crate::model::now_ms(),
description: self.description.clone(),
encrypted: false,
})
}
}
#[derive(Clone, Debug)]
pub struct RepoCatalog {
doc: DocRef<CatalogDoc>,
}
impl RepoCatalog {
pub fn new(store: Arc<dyn ControlStore>) -> Result<Self> {
Ok(RepoCatalog {
doc: DocRef::new(store, CATALOG_REF)?,
})
}
pub fn ref_name(&self) -> &str {
self.doc.ref_name()
}
pub fn history(&self) -> Result<Vec<crate::control::DocVersion>> {
self.doc.history()
}
pub fn create_namespace(&self, name: &str) -> Result<Namespace> {
for level in name.split('/') {
check_component(name, level)?;
}
if name.split('/').count() >= crate::sandbox::MAX_COMPONENTS {
return Err(Error::InvalidPath {
input: name.escape_debug().to_string(),
reason: crate::sandbox::PathReject::TooManyComponents,
});
}
let namespace = Namespace {
name: name.to_owned(),
explode_budget_bytes: None,
public: false,
events_public: false,
};
self.doc.update(|doc| {
if doc.namespaces.contains_key(name)? {
return Err(Error::NamespaceExists(name.to_owned()));
}
doc.namespaces.insert(name.to_owned(), namespace.clone())?;
Ok(namespace.clone())
})
}
pub fn set_namespace_explode_budget(
&self,
name: &str,
bytes: Option<u64>,
) -> Result<Namespace> {
self.doc.update(|doc| {
let Some(mut ns) = doc.namespaces.get(name)? else {
return Err(Error::NoSuchNamespace(name.to_owned()));
};
ns.explode_budget_bytes = bytes;
doc.namespaces.insert(name.to_owned(), ns.clone())?;
Ok(ns)
})
}
pub fn namespace_explode_budget(&self, name: &str) -> Result<Option<u64>> {
Ok(self
.doc
.load()?
.namespaces
.get(name)?
.and_then(|ns| ns.explode_budget_bytes))
}
pub fn set_namespace_public(&self, name: &str, public: bool) -> Result<Namespace> {
self.update_namespace(name, |ns| ns.public = public)
}
pub fn set_namespace_events_public(&self, name: &str, public: bool) -> Result<Namespace> {
self.update_namespace(name, |ns| ns.events_public = public)
}
fn update_namespace(&self, name: &str, change: impl Fn(&mut Namespace)) -> Result<Namespace> {
self.doc.update(|doc| {
let Some(mut ns) = doc.namespaces.get(name)? else {
return Err(Error::NoSuchNamespace(name.to_owned()));
};
change(&mut ns);
doc.namespaces.insert(name.to_owned(), ns.clone())?;
Ok(ns)
})
}
pub fn version(&self) -> Result<Option<crate::control::BlobId>> {
self.doc.version()
}
pub fn namespaces(&self) -> Result<Vec<Namespace>> {
self.doc.load()?.namespaces.values()
}
pub fn namespace(&self, name: &str) -> Result<Option<Namespace>> {
self.doc.load()?.namespaces.get(name)
}
pub fn delete_namespace(&self, name: &str, delete_repos: bool) -> Result<Vec<Repo>> {
self.doc.update(|doc| {
if !doc.namespaces.contains_key(name)? {
return Err(Error::NoSuchNamespace(name.to_owned()));
}
let doomed: Vec<Repo> = doc
.repos
.values()?
.into_iter()
.filter(|repo| repo.namespace == name)
.collect();
if !doomed.is_empty() && !delete_repos {
return Err(Error::NamespaceNotEmpty {
name: name.to_owned(),
repos: doomed.len(),
});
}
for repo in &doomed {
doc.repos.remove(&repo.key())?;
}
doc.namespaces.remove(name)?;
Ok(doomed)
})
}
pub fn create(&self, spec: NewRepo) -> Result<Repo> {
let repo = spec.prospect()?;
let key = spec.path.key();
self.doc.update(|doc| {
if !doc.namespaces.contains_key(repo.namespace.as_str())? {
return Err(Error::NoSuchNamespace(repo.namespace.clone()));
}
if doc.repos.contains_key(&key)? {
return Err(Error::RepoExists(spec.path.clone()));
}
doc.repos.insert(key.clone(), repo.clone())?;
Ok(repo.clone())
})
}
pub fn open(&self, path: &RepoRef) -> Result<Repo> {
self.get(path)?
.ok_or_else(|| Error::NoSuchRepo(path.clone()))
}
pub fn get(&self, path: &RepoRef) -> Result<Option<Repo>> {
self.doc.load()?.repos.get(&path.key())
}
pub fn get_many(&self, paths: &[RepoRef]) -> Result<Vec<Option<Repo>>> {
let doc = self.doc.load()?;
paths.iter().map(|p| doc.repos.get(&p.key())).collect()
}
pub fn contains(&self, path: &RepoRef) -> Result<bool> {
self.doc.load()?.repos.contains_key(&path.key())
}
pub fn list(&self, namespace: Option<&str>) -> Result<Vec<Repo>> {
let doc = self.doc.load()?;
Ok(doc
.repos
.values()?
.into_iter()
.filter(|repo| namespace.is_none_or(|ns| repo.namespace == ns))
.collect())
}
pub fn delete(&self, path: &RepoRef) -> Result<Repo> {
let key = path.key();
self.doc.update(|doc| {
doc.repos
.remove(&key)?
.ok_or_else(|| Error::NoSuchRepo(path.clone()))
})
}
pub fn set_default_branch(&self, path: &RepoRef, branch: &str) -> Result<Repo> {
let branch = check_branch(branch)?;
let key = path.key();
self.doc.update(|doc| {
let mut repo = doc
.repos
.get(&key)?
.ok_or_else(|| Error::NoSuchRepo(path.clone()))?;
repo.default_branch = branch.clone();
doc.repos.insert(key.clone(), repo.clone())?;
Ok(repo)
})
}
pub fn set_visibility(&self, path: &RepoRef, visibility: Visibility) -> Result<Repo> {
let key = path.key();
self.doc.update(|doc| {
let mut repo = doc
.repos
.get(&key)?
.ok_or_else(|| Error::NoSuchRepo(path.clone()))?;
repo.visibility = visibility;
doc.repos.insert(key.clone(), repo.clone())?;
Ok(repo)
})
}
pub fn set_encrypted(&self, path: &RepoRef, encrypted: bool) -> Result<Repo> {
let key = path.key();
self.doc.update(|doc| {
let mut repo = doc
.repos
.get(&key)?
.ok_or_else(|| Error::NoSuchRepo(path.clone()))?;
repo.encrypted = encrypted;
doc.repos.insert(key.clone(), repo.clone())?;
Ok(repo)
})
}
pub fn counts(&self) -> Result<(usize, usize)> {
let doc = self.doc.load()?;
Ok((doc.namespaces.len(), doc.repos.len()))
}
}
pub fn point_head_at(git: &dyn GitServe, default_branch: &str) -> Result<bool> {
let want = check_branch(default_branch)?;
let current = git
.head()
.map_err(|err| backend(err.context("reading HEAD to see where it points")))?
.and_then(|row| row.symref_target);
if current.as_deref() == Some(want.as_str()) {
return Ok(false);
}
git.set_head_projected(&want)
.map_err(|err| backend(err.context(format!("pointing HEAD at {default_branch}"))))?;
Ok(true)
}
fn backend(err: anyhow::Error) -> Error {
Error::Store(gunnar_store::Error::Backend(err))
}