pub mod git; pub mod refs; use std::collections::hash_map; use std::ops::Deref; use std::path::Path; use std::{fmt, io}; use thiserror::Error; use crypto::{PublicKey, Signer, Unverified, Verified}; pub use git::{ProjectError, VerifyError}; pub use radicle_git_ext::Oid; use crate::collections::HashMap; use crate::git::ext as git_ext; use crate::git::{Qualified, RefError, RefString}; use crate::identity; use crate::identity::{Id, IdError}; use crate::storage::refs::Refs; use self::refs::SignedRefs; pub type BranchName = git::RefString; pub type Inventory = Vec; /// Describes one or more namespaces. #[derive(Default, Debug)] pub enum Namespaces { /// All namespaces. #[default] All, /// A single namespace, by public key. One(PublicKey), } impl From for Namespaces { fn from(pk: PublicKey) -> Self { Self::One(pk) } } /// Storage error. #[derive(Error, Debug)] pub enum Error { #[error("invalid git reference")] InvalidRef, #[error("git reference error: {0}")] Ref(#[from] RefError), #[error(transparent)] Refs(#[from] refs::Error), #[error("git: {0}")] Git(#[from] git2::Error), #[error("id: {0}")] Id(#[from] IdError), #[error("i/o: {0}")] Io(#[from] io::Error), } /// Fetch error. #[derive(Error, Debug)] #[allow(clippy::large_enum_variant)] pub enum FetchError { #[error("git: {0}")] Git(#[from] git2::Error), #[error("i/o: {0}")] Io(#[from] io::Error), #[error(transparent)] Refs(#[from] refs::Error), #[error("verify: {0}")] Verify(#[from] git::VerifyError), #[error(transparent)] Storage(#[from] Error), // TODO: This should wrap a more specific error. #[error("repository head: {0}")] SetHead(#[from] ProjectError), } pub type RemoteId = PublicKey; /// An update to a reference. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RefUpdate { Updated { name: RefString, old: Oid, new: Oid }, Created { name: RefString, oid: Oid }, Deleted { name: RefString, oid: Oid }, Skipped { name: RefString, oid: Oid }, } impl RefUpdate { pub fn from(name: RefString, old: impl Into, new: impl Into) -> Self { let old = old.into(); let new = new.into(); if old.is_zero() { Self::Created { name, oid: new } } else if new.is_zero() { Self::Deleted { name, oid: old } } else if old != new { Self::Updated { name, old, new } } else { Self::Skipped { name, oid: old } } } } impl fmt::Display for RefUpdate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Updated { name, old, new } => { write!(f, "~ {:.7}..{:.7} {}", old, new, name) } Self::Created { name, oid } => { write!(f, "* 0000000..{:.7} {}", oid, name) } Self::Deleted { name, oid } => { write!(f, "- {:.7}..0000000 {}", oid, name) } Self::Skipped { name, oid } => { write!(f, "= {:.7}..{:.7} {}", oid, oid, name) } } } } /// Project remotes. Tracks the git state of a project. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Remotes(HashMap>); impl FromIterator<(RemoteId, Remote)> for Remotes { fn from_iter)>>(iter: T) -> Self { Self(iter.into_iter().collect()) } } impl Deref for Remotes { type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.0 } } impl Remotes { pub fn new(remotes: HashMap>) -> Self { Self(remotes) } } impl Remotes { pub fn unverified(self) -> Remotes { Remotes( self.into_iter() .map(|(id, r)| (id, r.unverified())) .collect(), ) } } impl Default for Remotes { fn default() -> Self { Self(HashMap::default()) } } impl IntoIterator for Remotes { type Item = (RemoteId, Remote); type IntoIter = hash_map::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl From> for HashMap { fn from(other: Remotes) -> Self { let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into()); for (k, v) in other.into_iter() { remotes.insert(k, v.refs.into()); } remotes } } /// A project remote. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Remote { /// ID of remote. pub id: PublicKey, /// Git references published under this remote, and their hashes. pub refs: SignedRefs, /// Whether this remote is a delegate for the project. pub delegate: bool, } impl Remote { // TODO(finto): This function seems out of place in the API for a couple of reasons: // * The SignedRefs aren't guaranteed to be by the `id` // * I could write `Remote::::new(id, refs) and because of the above, it's a LIE pub fn new(id: PublicKey, refs: impl Into>) -> Self { Self { id, refs: refs.into(), delegate: false, } } } impl Remote { pub fn verified(self) -> Result, crypto::Error> { let refs = self.refs.verified(&self.id)?; Ok(Remote { id: self.id, refs, delegate: self.delegate, }) } } impl Remote { pub fn unverified(self) -> Remote { Remote { id: self.id, refs: self.refs.unverified(), delegate: self.delegate, } } } pub trait ReadStorage { fn path(&self) -> &Path; fn get( &self, remote: &RemoteId, proj: Id, ) -> Result>, ProjectError>; fn inventory(&self) -> Result; } pub trait WriteStorage: ReadStorage { type Repository: WriteRepository; fn repository(&self, proj: Id) -> Result; } pub trait ReadRepository { /// Returns `true` if there are no references in the repository. fn is_empty(&self) -> Result; /// The [`Path`] to the git repository. fn path(&self) -> &Path; fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result, git_ext::Error>; /// Get the head of this repository. /// /// Returns the reference pointed to by `HEAD` if it is set. Otherwise, computes the canonical /// head using [`ReadRepository::canonical_head`]. /// /// Returns the [`Oid`] as well as the qualified reference name. fn head(&self) -> Result<(Qualified, Oid), ProjectError>; /// Compute the canonical head of this repository. /// /// Ignores any existing `HEAD` reference. /// /// Returns the [`Oid`] as well as the qualified reference name. fn canonical_head(&self) -> Result<(Qualified, Oid), ProjectError>; /// Get the `reference` for the given `remote`. /// /// Returns `None` is the reference did not exist. fn reference( &self, remote: &RemoteId, reference: &Qualified, ) -> Result; /// Get the [`git2::Commit`] found using its `oid`. /// /// Returns `None` if the commit did not exist. fn commit(&self, oid: Oid) -> Result; fn revwalk(&self, head: Oid) -> Result; fn reference_oid( &self, remote: &RemoteId, reference: &Qualified, ) -> Result; fn references(&self, remote: &RemoteId) -> Result; fn remote(&self, remote: &RemoteId) -> Result, refs::Error>; fn remotes(&self) -> Result, refs::Error>; /// Return the project associated with this repository. fn project(&self) -> Result, Error>; fn project_identity(&self) -> Result<(Oid, identity::Doc), ProjectError>; } pub trait WriteRepository: ReadRepository { fn fetch( &mut self, node: &RemoteId, namespaces: impl Into, ) -> Result, FetchError>; fn set_head(&self) -> Result; fn sign_refs(&self, signer: &G) -> Result, Error>; fn raw(&self) -> &git2::Repository; } impl ReadStorage for T where T: Deref, S: ReadStorage + 'static, { fn path(&self) -> &Path { self.deref().path() } fn inventory(&self) -> Result { self.deref().inventory() } fn get( &self, remote: &RemoteId, proj: Id, ) -> Result>, ProjectError> { self.deref().get(remote, proj) } } impl WriteStorage for T where T: Deref, S: WriteStorage + 'static, { type Repository = S::Repository; fn repository(&self, proj: Id) -> Result { self.deref().repository(proj) } } #[cfg(test)] mod tests { #[test] fn test_storage() {} }