node: Change storage layout
Projects now have their own repos. Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
parent
5c41af060f
commit
f18afb793c
133
node/src/git.rs
133
node/src/git.rs
|
|
@ -1,85 +1,62 @@
|
||||||
use crate::identity::ProjId;
|
use std::str::FromStr;
|
||||||
use crate::storage::{Error, WriteStorage};
|
|
||||||
|
use crate::collections::HashMap;
|
||||||
|
use crate::identity::UserId;
|
||||||
|
use crate::storage::{Remote, Remotes, Unverified};
|
||||||
|
use git_ref_format as format;
|
||||||
|
|
||||||
/// Default port of the `git` transport protocol.
|
/// Default port of the `git` transport protocol.
|
||||||
pub const PROTOCOL_PORT: u16 = 9418;
|
pub const PROTOCOL_PORT: u16 = 9418;
|
||||||
|
|
||||||
/// Fetch all remotes of a project from the given URL.
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub fn fetch<S: WriteStorage>(proj: &ProjId, url: &str, mut storage: S) -> Result<(), Error> {
|
pub enum RefError {
|
||||||
// TODO: Use `Url` type?
|
#[error("invalid ref name '{0}'")]
|
||||||
// TODO: Have function to fetch specific remotes.
|
InvalidName(format::RefString),
|
||||||
// TODO: Return meaningful info on success.
|
#[error("invalid ref format: {0}")]
|
||||||
//
|
Format(#[from] format::Error),
|
||||||
// Repository layout should look like this:
|
|
||||||
//
|
|
||||||
// /refs/namespaces/<project>
|
|
||||||
// /refs/namespaces/<remote>
|
|
||||||
// /heads
|
|
||||||
// /master
|
|
||||||
// /tags
|
|
||||||
// ...
|
|
||||||
//
|
|
||||||
let repo = storage.repository();
|
|
||||||
let refs: &[&str] = &[&format!(
|
|
||||||
"refs/namespaces/{}/refs/*:refs/namespaces/{}/refs/*",
|
|
||||||
proj, proj
|
|
||||||
)];
|
|
||||||
let mut remote = repo.remote_anonymous(url)?;
|
|
||||||
let mut opts = git2::FetchOptions::default();
|
|
||||||
|
|
||||||
remote.fetch(refs, Some(&mut opts), None)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
mod tests {
|
pub enum ListRefsError {
|
||||||
use super::*;
|
#[error("git error: {0}")]
|
||||||
use crate::hash::Digest;
|
Git(#[from] git2::Error),
|
||||||
use crate::identity::ProjId;
|
#[error("invalid ref: {0}")]
|
||||||
use crate::storage::Storage;
|
InvalidRef(#[from] RefError),
|
||||||
|
}
|
||||||
/// Create an initial empty commit.
|
|
||||||
fn initial_commit(repo: &git2::Repository) -> Result<git2::Oid, Error> {
|
/// List remote refs of a project, given the remote URL.
|
||||||
// First use the config to initialize a commit signature for the user.
|
pub fn list_remotes(url: &str) -> Result<Remotes<Unverified>, ListRefsError> {
|
||||||
let sig = git2::Signature::now("cloudhead", "cloudhead@radicle.xyz")?;
|
let mut remotes = HashMap::default();
|
||||||
// Now let's create an empty tree for this commit.
|
let mut remote = git2::Remote::create_detached(url)?;
|
||||||
let tree_id = repo.index()?.write_tree()?;
|
|
||||||
let tree = repo.find_tree(tree_id)?;
|
remote.connect(git2::Direction::Fetch)?;
|
||||||
let oid = repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
|
|
||||||
|
let refs = remote.list()?;
|
||||||
Ok(oid)
|
for r in refs {
|
||||||
}
|
let (id, refname) = parse_ref::<UserId>(r.name())?;
|
||||||
|
let entry = remotes
|
||||||
#[test]
|
.entry(id.clone())
|
||||||
fn test_fetch() {
|
.or_insert_with(|| Remote::new(id, HashMap::default()));
|
||||||
let path = tempfile::tempdir().unwrap().into_path();
|
|
||||||
let alice = git2::Repository::init_bare(path.join("alice")).unwrap();
|
entry.refs.insert(refname.to_string(), r.oid().into());
|
||||||
let bob = git2::Repository::init_bare(path.join("bob")).unwrap();
|
}
|
||||||
let mut bob_storage = Storage::from(bob);
|
|
||||||
let proj = ProjId::from(Digest::new(&[42]));
|
Ok(Remotes::new(remotes))
|
||||||
let master = format!("refs/namespaces/{}/refs/heads/master", proj);
|
}
|
||||||
let alice_oid = initial_commit(&alice).unwrap();
|
|
||||||
|
/// Parse a ref string.
|
||||||
alice
|
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
|
||||||
.reference(&master, alice_oid, false, "Create master branch")
|
let input = format::RefStr::try_from_str(s)?;
|
||||||
.unwrap();
|
let suffix = input
|
||||||
|
.strip_prefix(format::refname!("refs/namespaces"))
|
||||||
// Have Bob fetch Alice's refs.
|
.ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
|
||||||
fetch(
|
|
||||||
&proj,
|
let mut components = suffix.components();
|
||||||
&format!("file://{}/alice", path.display()),
|
let id = components
|
||||||
&mut bob_storage,
|
.next()
|
||||||
)
|
.ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
|
||||||
.unwrap();
|
let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
|
||||||
|
let refstr = components.collect::<format::RefString>();
|
||||||
let bob_oid = bob_storage
|
|
||||||
.repository()
|
Ok((id, refstr))
|
||||||
.find_reference(&master)
|
|
||||||
.unwrap()
|
|
||||||
.target()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(alice_oid, bob_oid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{fmt, io, ops::Deref, str::FromStr};
|
use std::{ffi::OsString, fmt, io, ops::Deref, str::FromStr};
|
||||||
|
|
||||||
use ed25519_consensus::{VerificationKey, VerificationKeyBytes};
|
use ed25519_consensus::{VerificationKey, VerificationKeyBytes};
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
@ -10,8 +10,6 @@ use crate::hash;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ProjIdError {
|
pub enum ProjIdError {
|
||||||
#[error("invalid ref '{0}'")]
|
|
||||||
InvalidRef(String),
|
|
||||||
#[error("invalid digest: {0}")]
|
#[error("invalid digest: {0}")]
|
||||||
InvalidDigest(#[from] hash::DecodeError),
|
InvalidDigest(#[from] hash::DecodeError),
|
||||||
}
|
}
|
||||||
|
|
@ -35,24 +33,25 @@ impl ProjId {
|
||||||
pub fn encode(&self) -> String {
|
pub fn encode(&self) -> String {
|
||||||
multibase::encode(multibase::Base::Base58Btc, &self.0.as_ref())
|
multibase::encode(multibase::Base::Base58Btc, &self.0.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_ref(s: &str) -> Result<ProjId, ProjIdError> {
|
|
||||||
if let Some(s) = s.split('/').nth(2) {
|
|
||||||
let id = Self::from_str(s)?;
|
|
||||||
return Ok(id);
|
|
||||||
}
|
|
||||||
Err(ProjIdError::InvalidRef(s.to_owned()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ProjId {
|
impl FromStr for ProjId {
|
||||||
type Err = hash::DecodeError;
|
type Err = ProjIdError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(Self(hash::Digest::from_str(s)?))
|
Ok(Self(hash::Digest::from_str(s)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<OsString> for ProjId {
|
||||||
|
type Error = ProjIdError;
|
||||||
|
|
||||||
|
fn try_from(value: OsString) -> Result<Self, Self::Error> {
|
||||||
|
let string = value.to_string_lossy();
|
||||||
|
Self::from_str(&string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<hash::Digest> for ProjId {
|
impl From<hash::Digest> for ProjId {
|
||||||
fn from(digest: hash::Digest) -> Self {
|
fn from(digest: hash::Digest) -> Self {
|
||||||
Self(digest)
|
Self(digest)
|
||||||
|
|
@ -134,8 +133,6 @@ pub enum UserIdError {
|
||||||
InvalidKey(#[from] ed25519_consensus::Error),
|
InvalidKey(#[from] ed25519_consensus::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserId {}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DocError {
|
pub enum DocError {
|
||||||
#[error("toml: {0}")]
|
#[error("toml: {0}")]
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,8 @@ use crate::address_manager::AddressManager;
|
||||||
use crate::clock::RefClock;
|
use crate::clock::RefClock;
|
||||||
use crate::collections::{HashMap, HashSet};
|
use crate::collections::{HashMap, HashSet};
|
||||||
use crate::decoder::Decoder;
|
use crate::decoder::Decoder;
|
||||||
use crate::git;
|
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
use crate::storage;
|
use crate::storage::{self, WriteRepository};
|
||||||
use crate::storage::{Inventory, ReadStorage, Remotes, Unverified, WriteStorage};
|
use crate::storage::{Inventory, ReadStorage, Remotes, Unverified, WriteStorage};
|
||||||
|
|
||||||
/// Network peer identifier.
|
/// Network peer identifier.
|
||||||
|
|
@ -452,7 +451,11 @@ where
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Connect(addr) => self.context.connect(addr),
|
Command::Connect(addr) => self.context.connect(addr),
|
||||||
Command::Fetch(proj, remote) => {
|
Command::Fetch(proj, remote) => {
|
||||||
git::fetch(&proj, &format!("git://{}", remote), &mut self.storage).unwrap();
|
self.storage
|
||||||
|
.repository(&proj)
|
||||||
|
.unwrap()
|
||||||
|
.fetch(&format!("git://{}", remote))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
|
pub mod git;
|
||||||
|
|
||||||
|
use std::collections::hash_map;
|
||||||
|
use std::io;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::{fmt, fs, io};
|
|
||||||
|
|
||||||
use git_ref_format::refspec;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use radicle_git_ext as git_ext;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use radicle_git_ext::Oid;
|
pub use radicle_git_ext::Oid;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
|
use crate::git::RefError;
|
||||||
use crate::identity;
|
use crate::identity;
|
||||||
use crate::identity::{ProjId, ProjIdError, UserId};
|
use crate::identity::{ProjId, ProjIdError, 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 static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml"));
|
||||||
|
|
||||||
pub type BranchName = String;
|
pub type BranchName = String;
|
||||||
|
|
@ -27,6 +27,8 @@ pub type Inventory = Vec<(ProjId, HashMap<String, Remote<Unverified>>)>;
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("invalid git reference")]
|
#[error("invalid git reference")]
|
||||||
InvalidRef,
|
InvalidRef,
|
||||||
|
#[error("git reference error: {0}")]
|
||||||
|
Ref(#[from] RefError),
|
||||||
#[error("git: {0}")]
|
#[error("git: {0}")]
|
||||||
Git(#[from] git2::Error),
|
Git(#[from] git2::Error),
|
||||||
#[error("id: {0}")]
|
#[error("id: {0}")]
|
||||||
|
|
@ -51,7 +53,7 @@ pub struct Verified;
|
||||||
pub struct Unverified;
|
pub struct Unverified;
|
||||||
|
|
||||||
/// Project remotes. Tracks the git state of a project.
|
/// Project remotes. Tracks the git state of a project.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
|
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
|
||||||
|
|
||||||
impl Remotes<Unverified> {
|
impl Remotes<Unverified> {
|
||||||
|
|
@ -60,6 +62,21 @@ impl Remotes<Unverified> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Remotes<Unverified> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(HashMap::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntoIterator for Remotes<V> {
|
||||||
|
type Item = (RemoteId, Remote<V>);
|
||||||
|
type IntoIter = hash_map::IntoIter<RemoteId, Remote<V>>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
#[allow(clippy::from_over_into)]
|
||||||
impl Into<HashMap<String, Remote<Unverified>>> for Remotes<Unverified> {
|
impl Into<HashMap<String, Remote<Unverified>>> for Remotes<Unverified> {
|
||||||
fn into(self) -> HashMap<String, Remote<Unverified>> {
|
fn into(self) -> HashMap<String, Remote<Unverified>> {
|
||||||
|
|
@ -73,20 +90,23 @@ impl Into<HashMap<String, Remote<Unverified>>> for Remotes<Unverified> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A project remote.
|
/// A project remote.
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Remote<V> {
|
pub struct Remote<V> {
|
||||||
|
/// ID of remote.
|
||||||
|
pub id: UserId,
|
||||||
/// Git references published under this remote, and their hashes.
|
/// Git references published under this remote, and their hashes.
|
||||||
refs: HashMap<RefName, Oid>,
|
pub refs: HashMap<RefName, Oid>,
|
||||||
/// Whether this remote is of a project delegate.
|
/// Whether this remote is of a project delegate.
|
||||||
delegate: bool,
|
pub delegate: bool,
|
||||||
/// Whether the remote is verified or not, ie. whether its signed refs were checked.
|
/// Whether the remote is verified or not, ie. whether its signed refs were checked.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
verified: PhantomData<V>,
|
verified: PhantomData<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Remote<Unverified> {
|
impl Remote<Unverified> {
|
||||||
pub fn new(refs: HashMap<RefName, Oid>) -> Self {
|
pub fn new(id: UserId, refs: HashMap<RefName, Oid>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
id,
|
||||||
refs,
|
refs,
|
||||||
delegate: false,
|
delegate: false,
|
||||||
verified: PhantomData,
|
verified: PhantomData,
|
||||||
|
|
@ -100,12 +120,18 @@ pub trait ReadStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WriteStorage {
|
pub trait WriteStorage {
|
||||||
fn repository(&mut self) -> &mut git2::Repository;
|
type Repository: WriteRepository;
|
||||||
fn namespace(
|
|
||||||
&mut self,
|
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error>;
|
||||||
proj: &ProjId,
|
}
|
||||||
user: &UserId,
|
|
||||||
) -> Result<&mut git2::Repository, git2::Error>;
|
pub trait ReadRepository {
|
||||||
|
fn remotes(&self) -> Result<Remotes<Unverified>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WriteRepository {
|
||||||
|
fn fetch(&mut self, url: &str) -> Result<(), git2::Error>;
|
||||||
|
fn namespace(&mut self, user: &UserId) -> Result<&mut git2::Repository, git2::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S> ReadStorage for T
|
impl<T, S> ReadStorage for T
|
||||||
|
|
@ -127,114 +153,10 @@ where
|
||||||
T: DerefMut<Target = S>,
|
T: DerefMut<Target = S>,
|
||||||
S: WriteStorage + 'static,
|
S: WriteStorage + 'static,
|
||||||
{
|
{
|
||||||
fn repository(&mut self) -> &mut git2::Repository {
|
type Repository = S::Repository;
|
||||||
self.deref_mut().repository()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn namespace(
|
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
|
||||||
&mut self,
|
self.deref().repository(proj)
|
||||||
proj: &ProjId,
|
|
||||||
user: &UserId,
|
|
||||||
) -> Result<&mut git2::Repository, git2::Error> {
|
|
||||||
self.deref_mut().namespace(proj, user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Storage {
|
|
||||||
backend: git2::Repository,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<git2::Repository> for Storage {
|
|
||||||
fn from(backend: git2::Repository) -> Self {
|
|
||||||
Self { backend }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
fn namespace(
|
|
||||||
&mut self,
|
|
||||||
proj: &ProjId,
|
|
||||||
user: &UserId,
|
|
||||||
) -> Result<&mut git2::Repository, git2::Error> {
|
|
||||||
let path = self.backend.path();
|
|
||||||
|
|
||||||
self.backend = git2::Repository::open_bare(path)?;
|
|
||||||
self.backend.set_namespace(&format!("{}/{}", proj, user))?;
|
|
||||||
|
|
||||||
Ok(&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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,26 @@
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::{fmt, fs};
|
||||||
use std::{fmt, fs, io};
|
|
||||||
|
|
||||||
use git_ref_format::refspec;
|
use git_ref_format::refspec;
|
||||||
use git_url::Url;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use radicle_git_ext as git_ext;
|
use radicle_git_ext as git_ext;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub use radicle_git_ext::Oid;
|
pub use radicle_git_ext::Oid;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity;
|
use crate::identity;
|
||||||
use crate::identity::{ProjId, ProjIdError, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, Unverified, Verified,
|
Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, Unverified, WriteRepository,
|
||||||
WriteRepository, WriteStorage,
|
WriteStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static RAD_ROOT_GLOB: Lazy<refspec::PatternString> =
|
pub static RAD_ROOT_GLOB: Lazy<refspec::PatternString> =
|
||||||
Lazy::new(|| refspec::pattern!("refs/namespaces/*/refs/rad/root"));
|
Lazy::new(|| refspec::pattern!("refs/namespaces/*/refs/rad/root"));
|
||||||
|
pub static NAMESPACES_GLOB: Lazy<refspec::PatternString> =
|
||||||
|
Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
|
||||||
pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml"));
|
pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml"));
|
||||||
|
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
|
|
@ -48,7 +45,6 @@ impl ReadStorage for Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inventory(&self) -> Result<Inventory, Error> {
|
fn inventory(&self) -> Result<Inventory, Error> {
|
||||||
let glob: String = RAD_ROOT_GLOB.clone().into();
|
|
||||||
let projs = self.projects()?;
|
let projs = self.projects()?;
|
||||||
let mut inv = Vec::new();
|
let mut inv = Vec::new();
|
||||||
|
|
||||||
|
|
@ -57,7 +53,7 @@ impl ReadStorage for Storage {
|
||||||
let remotes = repo
|
let remotes = repo
|
||||||
.remotes()?
|
.remotes()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| (r.id.to_string(), r))
|
.map(|(id, r)| (id.to_string(), r))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
inv.push((proj, remotes));
|
inv.push((proj, remotes));
|
||||||
|
|
@ -157,8 +153,8 @@ impl Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadRepository for Repository {
|
impl ReadRepository for Repository {
|
||||||
fn remotes(&self) -> Result<Vec<Remote<Unverified>>, Error> {
|
fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
|
||||||
let refs = self.backend.references_glob(RAD_ROOT_GLOB.as_str())?;
|
let refs = self.backend.references_glob(NAMESPACES_GLOB.as_str())?;
|
||||||
let mut remotes = HashMap::default();
|
let mut remotes = HashMap::default();
|
||||||
|
|
||||||
for r in refs {
|
for r in refs {
|
||||||
|
|
@ -172,7 +168,7 @@ impl ReadRepository for Repository {
|
||||||
|
|
||||||
entry.refs.insert(refname.to_string(), oid.into());
|
entry.refs.insert(refname.to_string(), oid.into());
|
||||||
}
|
}
|
||||||
Ok(remotes.into_values().collect())
|
Ok(Remotes::new(remotes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +188,7 @@ impl WriteRepository for Repository {
|
||||||
// /tags
|
// /tags
|
||||||
// ...
|
// ...
|
||||||
//
|
//
|
||||||
let refs: &[&str] = &[&format!("refs/namespaces/*:refs/namespaces/*")];
|
let refs: &[&str] = &["refs/namespaces/*:refs/namespaces/*"];
|
||||||
let mut remote = self.backend.remote_anonymous(url)?;
|
let mut remote = self.backend.remote_anonymous(url)?;
|
||||||
let mut opts = git2::FetchOptions::default();
|
let mut opts = git2::FetchOptions::default();
|
||||||
|
|
||||||
|
|
@ -221,38 +217,22 @@ impl From<git2::Repository> for Repository {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::hash::Digest;
|
|
||||||
use crate::identity::ProjId;
|
|
||||||
use crate::storage::{ReadStorage, WriteRepository};
|
use crate::storage::{ReadStorage, WriteRepository};
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
|
|
||||||
/// Create an initial empty commit.
|
|
||||||
fn initial_commit(repo: &git2::Repository) -> Result<git2::Oid, Error> {
|
|
||||||
// First use the config to initialize a commit signature for the user.
|
|
||||||
let sig = git2::Signature::now("cloudhead", "cloudhead@radicle.xyz")?;
|
|
||||||
// Now let's create an empty tree for this commit.
|
|
||||||
let tree_id = repo.index()?.write_tree()?;
|
|
||||||
let tree = repo.find_tree(tree_id)?;
|
|
||||||
let oid = repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
|
|
||||||
|
|
||||||
Ok(oid)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ls_remote() {
|
fn test_list_remotes() {
|
||||||
crate::test::logger::init(log::Level::Debug);
|
|
||||||
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let storage = fixtures::storage(dir.path());
|
let storage = fixtures::storage(dir.path());
|
||||||
let inv = storage.inventory().unwrap();
|
let inv = storage.inventory().unwrap();
|
||||||
let (proj, _) = inv.first().unwrap();
|
let (proj, _) = inv.first().unwrap();
|
||||||
let refs = git::list_refs(&format!(
|
let refs = git::list_remotes(&format!(
|
||||||
"file://{}",
|
"file://{}",
|
||||||
dir.path().join(&proj.to_string()).display(),
|
dir.path().join(&proj.to_string()).display(),
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let remotes = storage.repository(&proj).unwrap().remotes().unwrap();
|
let remotes = storage.repository(proj).unwrap().remotes().unwrap();
|
||||||
|
|
||||||
assert_eq!(refs, remotes);
|
assert_eq!(refs, remotes);
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +247,7 @@ mod tests {
|
||||||
let refname = "refs/heads/master";
|
let refname = "refs/heads/master";
|
||||||
|
|
||||||
// Have Bob fetch Alice's refs.
|
// Have Bob fetch Alice's refs.
|
||||||
bob.repository(&proj)
|
bob.repository(proj)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fetch(&format!(
|
.fetch(&format!(
|
||||||
"file://{}",
|
"file://{}",
|
||||||
|
|
@ -275,14 +255,14 @@ mod tests {
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for (_, remote) in remotes {
|
for remote in remotes.values() {
|
||||||
let alice_oid = alice
|
let alice_oid = alice
|
||||||
.repository(&proj)
|
.repository(proj)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.find_reference(&remote.id, refname)
|
.find_reference(&remote.id, refname)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let bob_oid = bob
|
let bob_oid = bob
|
||||||
.repository(&proj)
|
.repository(proj)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.find_reference(&remote.id, refname)
|
.find_reference(&remote.id, refname)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ impl quickcheck::Arbitrary for storage::Remote<storage::Unverified> {
|
||||||
let mut refs: HashMap<storage::BranchName, storage::Oid> = HashMap::with_hasher(rng.into());
|
let mut refs: HashMap<storage::BranchName, storage::Oid> = HashMap::with_hasher(rng.into());
|
||||||
let mut bytes: [u8; 20] = [0; 20];
|
let mut bytes: [u8; 20] = [0; 20];
|
||||||
let names = &["master", "dev", "feature/1", "feature/2", "feature/3"];
|
let names = &["master", "dev", "feature/1", "feature/2", "feature/3"];
|
||||||
|
let id = UserId::arbitrary(g);
|
||||||
|
|
||||||
for _ in 0..g.size().min(2) {
|
for _ in 0..g.size().min(2) {
|
||||||
if let Some(name) = g.choose(names) {
|
if let Some(name) = g.choose(names) {
|
||||||
|
|
@ -36,7 +37,7 @@ impl quickcheck::Arbitrary for storage::Remote<storage::Unverified> {
|
||||||
refs.insert(name.to_string(), oid);
|
refs.insert(name.to_string(), oid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage::Remote::new(refs)
|
storage::Remote::new(id, refs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ use std::str::FromStr;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
use crate::storage::{Storage, WriteStorage};
|
use crate::storage::git::Storage;
|
||||||
|
use crate::storage::{WriteRepository, WriteStorage};
|
||||||
|
|
||||||
pub static USER_IDS: Lazy<[UserId; 16]> = Lazy::new(|| {
|
pub static USER_IDS: Lazy<[UserId; 16]> = Lazy::new(|| {
|
||||||
[
|
[
|
||||||
|
|
@ -48,16 +49,23 @@ pub static PROJ_IDS: Lazy<[ProjId; 16]> = Lazy::new(|| {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn storage(path: &Path) -> Storage {
|
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
|
||||||
let mut storage = Storage::open(path).unwrap();
|
let path = path.as_ref();
|
||||||
|
let storage = Storage::new(path);
|
||||||
|
|
||||||
for proj in PROJ_IDS.iter().take(3) {
|
for proj in PROJ_IDS.iter().take(3) {
|
||||||
|
log::debug!("creating {}...", proj);
|
||||||
|
let mut repo = storage.repository(proj).unwrap();
|
||||||
|
|
||||||
for user in USER_IDS.iter().take(3) {
|
for user in USER_IDS.iter().take(3) {
|
||||||
let repo = storage.namespace(proj, user).unwrap();
|
let repo = repo.namespace(user).unwrap();
|
||||||
let head_oid = initial_commit(repo).unwrap();
|
let head_oid = initial_commit(repo).unwrap();
|
||||||
let head = repo.find_commit(head_oid).unwrap();
|
let head = repo.find_commit(head_oid).unwrap();
|
||||||
|
|
||||||
log::debug!("creating {}...", repo.namespace().unwrap());
|
log::debug!("{}: creating {}...", proj, repo.namespace().unwrap());
|
||||||
|
|
||||||
|
repo.reference("refs/rad/root", head_oid, false, "test")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// TODO: Different commits.
|
// TODO: Different commits.
|
||||||
repo.branch("master", &head, false).unwrap();
|
repo.branch("master", &head, false).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::identity::ProjId;
|
use crate::identity::ProjId;
|
||||||
use crate::storage::{Error, Inventory, ReadStorage, Remotes, Unverified, WriteStorage};
|
use crate::storage::{
|
||||||
|
Error, Inventory, ReadStorage, Remotes, Unverified, WriteRepository, WriteStorage,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MockStorage {
|
pub struct MockStorage {
|
||||||
|
|
@ -38,13 +40,22 @@ impl ReadStorage for MockStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WriteStorage for MockStorage {
|
impl WriteStorage for MockStorage {
|
||||||
fn repository(&mut self) -> &mut git2::Repository {
|
type Repository = MockRepository;
|
||||||
|
|
||||||
|
fn repository(&self, _proj: &ProjId) -> Result<Self::Repository, Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MockRepository {}
|
||||||
|
|
||||||
|
impl WriteRepository for MockRepository {
|
||||||
|
fn fetch(&mut self, _url: &str) -> Result<(), git2::Error> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn namespace(
|
fn namespace(
|
||||||
&mut self,
|
&mut self,
|
||||||
_proj: &ProjId,
|
|
||||||
_user: &crate::identity::UserId,
|
_user: &crate::identity::UserId,
|
||||||
) -> Result<&mut git2::Repository, git2::Error> {
|
) -> Result<&mut git2::Repository, git2::Error> {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue