use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::Path; use std::{fmt, fs, io}; use git_ref_format::refspec; use once_cell::sync::Lazy; use radicle_git_ext as git_ext; use serde::{Deserialize, Serialize}; use thiserror::Error; pub use radicle_git_ext::Oid; use crate::collections::HashMap; use crate::identity; use crate::identity::{IdError, ProjId, UserId}; pub static RAD_ID_GLOB: Lazy = Lazy::new(|| refspec::pattern!("refs/namespaces/*/refs/rad/id")); pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml")); pub type BranchName = String; pub type Inventory = Vec<(ProjId, HashMap>)>; /// Storage error. #[derive(Error, Debug)] pub enum Error { #[error("invalid git reference")] InvalidRef, #[error("git: {0}")] Git(#[from] git2::Error), #[error("id: {0}")] ProjId(#[from] IdError), #[error("i/o: {0}")] Io(#[from] io::Error), #[error("doc: {0}")] Doc(#[from] identity::DocError), #[error("invalid repository head")] InvalidHead, } pub type Refs = HashMap; pub type RemoteId = UserId; pub type RefName = String; /// Verified (used as type witness). #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Verified; /// Unverified (used as type witness). #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Unverified; /// Project remotes. Tracks the git state of a project. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Remotes(HashMap>); impl Remotes { pub fn new(remotes: HashMap>) -> Self { Self(remotes) } } #[allow(clippy::from_over_into)] impl Into>> for Remotes { fn into(self) -> HashMap> { let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into()); for (k, v) in self.0 { remotes.insert(k.to_string(), v); } remotes } } /// A project remote. #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] pub struct Remote { /// Git references published under this remote, and their hashes. refs: HashMap, /// Whether this remote is of a project delegate. delegate: bool, /// Whether the remote is verified or not, ie. whether its signed refs were checked. #[serde(skip)] verified: PhantomData, } impl Remote { pub fn new(refs: HashMap) -> Self { Self { refs, delegate: false, verified: PhantomData, } } } pub trait ReadStorage { fn get(&self, proj: &ProjId) -> Result>, Error>; fn inventory(&self) -> Result; } pub trait WriteStorage { fn repository(&mut self) -> &mut git2::Repository; } impl ReadStorage for T where T: Deref, S: ReadStorage, { fn inventory(&self) -> Result { self.deref().inventory() } fn get(&self, proj: &ProjId) -> Result>, Error> { self.deref().get(proj) } } impl WriteStorage for T where T: DerefMut, S: WriteStorage + 'static, { fn repository(&mut self) -> &mut git2::Repository { self.deref_mut().repository() } } pub struct Storage { backend: git2::Repository, } impl fmt::Debug for Storage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Storage(..)") } } impl ReadStorage for Storage { fn get(&self, _id: &ProjId) -> Result>, Error> { todo!() } fn inventory(&self) -> Result { let glob: String = RAD_ID_GLOB.clone().into(); let refs = self.backend.references_glob(glob.as_str())?; for r in refs { let r = r?; let name = r.name().ok_or(Error::InvalidRef)?; let _id = ProjId::from_ref(name)?; todo!(); } Ok(vec![]) } } impl WriteStorage for Storage { fn repository(&mut self) -> &mut git2::Repository { &mut self.backend } } impl Storage { pub fn open>(path: P) -> Result { let path = path.as_ref(); let backend = match git2::Repository::open_bare(path) { Err(e) if git_ext::is_not_found_err(&e) => { let backend = git2::Repository::init_opts( path, git2::RepositoryInitOptions::new() .bare(true) .no_reinit(true) .external_template(false), )?; Ok(backend) } Ok(repo) => Ok(repo), Err(e) => Err(e), }?; Ok(Self { backend }) } pub fn create( &self, repo: &git2::Repository, identity: impl Into, ) -> Result<(ProjId, git2::Reference), Error> { let doc = identity.into(); let file = fs::OpenOptions::new() .create_new(true) .write(true) .open(*IDENTITY_PATH)?; let id = doc.write(file)?; let ref_name = RAD_ID_GLOB.replace('*', &id.encode()); let oid = repo.head()?.target().ok_or(Error::InvalidHead)?; let reference = self.backend.reference(&ref_name, oid, false, "")?; // TODO: Push project to monorepo. Ok((id, reference)) } } #[cfg(test)] mod tests { #[test] fn test_storage() {} }