mirror of
https://seed.radicle.dev/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git
synced 2026-11-15 15:50:38 +08:00
214 lines
5.6 KiB
Rust
214 lines
5.6 KiB
Rust
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<refspec::PatternString> =
|
|
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<String, Remote<Unverified>>)>;
|
|
|
|
/// 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<BranchName, Oid>;
|
|
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<V>(HashMap<RemoteId, Remote<V>>);
|
|
|
|
impl Remotes<Unverified> {
|
|
pub fn new(remotes: HashMap<RemoteId, Remote<Unverified>>) -> Self {
|
|
Self(remotes)
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::from_over_into)]
|
|
impl Into<HashMap<String, Remote<Unverified>>> for Remotes<Unverified> {
|
|
fn into(self) -> HashMap<String, Remote<Unverified>> {
|
|
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<V> {
|
|
/// Git references published under this remote, and their hashes.
|
|
refs: HashMap<RefName, Oid>,
|
|
/// 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<V>,
|
|
}
|
|
|
|
impl Remote<Unverified> {
|
|
pub fn new(refs: HashMap<RefName, Oid>) -> Self {
|
|
Self {
|
|
refs,
|
|
delegate: false,
|
|
verified: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait ReadStorage {
|
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
|
|
fn inventory(&self) -> Result<Inventory, Error>;
|
|
}
|
|
|
|
pub trait WriteStorage {
|
|
fn repository(&mut self) -> &mut git2::Repository;
|
|
}
|
|
|
|
impl<T, S> ReadStorage for T
|
|
where
|
|
T: Deref<Target = S>,
|
|
S: ReadStorage,
|
|
{
|
|
fn inventory(&self) -> Result<Inventory, Error> {
|
|
self.deref().inventory()
|
|
}
|
|
|
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
|
self.deref().get(proj)
|
|
}
|
|
}
|
|
|
|
impl<T, S> WriteStorage for T
|
|
where
|
|
T: DerefMut<Target = S>,
|
|
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<Option<Remotes<Unverified>>, Error> {
|
|
todo!()
|
|
}
|
|
|
|
fn inventory(&self) -> Result<Inventory, Error> {
|
|
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<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
|
|
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<identity::Doc>,
|
|
) -> 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() {}
|
|
}
|