node: Implement signed refs
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
parent
6e86218c36
commit
cd7421eb86
|
|
@ -55,7 +55,7 @@ pub struct Client<R: Reactor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Reactor> Client<R> {
|
impl<R: Reactor> Client<R> {
|
||||||
pub fn new<P: AsRef<Path>, S: Signer>(
|
pub fn new<P: AsRef<Path>, S: Signer + 'static>(
|
||||||
path: P,
|
path: P,
|
||||||
signer: S,
|
signer: S,
|
||||||
) -> Result<Self, nakamoto_net::error::Error> {
|
) -> Result<Self, nakamoto_net::error::Error> {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,17 @@ use ed25519_consensus as ed25519;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use ed25519::Error;
|
||||||
pub use ed25519::Signature;
|
pub use ed25519::Signature;
|
||||||
|
|
||||||
pub trait Signer: Send + Sync + 'static {
|
/// 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;
|
||||||
|
|
||||||
|
pub trait Signer: Send + Sync {
|
||||||
/// Return this signer's public/verification key.
|
/// Return this signer's public/verification key.
|
||||||
fn public_key(&self) -> &PublicKey;
|
fn public_key(&self) -> &PublicKey;
|
||||||
/// Sign a message and return the signature.
|
/// Sign a message and return the signature.
|
||||||
|
|
@ -27,6 +35,19 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Signer for &T
|
||||||
|
where
|
||||||
|
T: Signer + ?Sized,
|
||||||
|
{
|
||||||
|
fn sign(&self, msg: &[u8]) -> Signature {
|
||||||
|
self.deref().sign(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_key(&self) -> &PublicKey {
|
||||||
|
self.deref().public_key()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The public/verification key.
|
/// The public/verification key.
|
||||||
#[derive(Serialize, Deserialize, Eq, Debug, Copy, Clone)]
|
#[derive(Serialize, Deserialize, Eq, Debug, Copy, Clone)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ use radicle_git_ext as git_ext;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
use crate::identity::UserId;
|
use crate::identity::UserId;
|
||||||
use crate::storage::{Remote, Remotes, Unverified};
|
use crate::storage::refs::Refs;
|
||||||
|
use crate::storage::RemoteId;
|
||||||
|
|
||||||
pub use git_ext::Oid;
|
pub use git_ext::Oid;
|
||||||
pub use git_url::Url;
|
pub use git_url::Url;
|
||||||
|
|
@ -30,7 +31,7 @@ pub enum ListRefsError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List remote refs of a project, given the remote URL.
|
/// List remote refs of a project, given the remote URL.
|
||||||
pub fn list_remotes(url: &Url) -> Result<Remotes<Unverified>, ListRefsError> {
|
pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError> {
|
||||||
let url = url.to_string();
|
let url = url.to_string();
|
||||||
let mut remotes = HashMap::default();
|
let mut remotes = HashMap::default();
|
||||||
let mut remote = git2::Remote::create_detached(&url)?;
|
let mut remote = git2::Remote::create_detached(&url)?;
|
||||||
|
|
@ -40,14 +41,12 @@ pub fn list_remotes(url: &Url) -> Result<Remotes<Unverified>, ListRefsError> {
|
||||||
let refs = remote.list()?;
|
let refs = remote.list()?;
|
||||||
for r in refs {
|
for r in refs {
|
||||||
let (id, refname) = parse_ref::<UserId>(r.name())?;
|
let (id, refname) = parse_ref::<UserId>(r.name())?;
|
||||||
let entry = remotes
|
let entry = remotes.entry(id).or_insert_with(Refs::default);
|
||||||
.entry(id)
|
|
||||||
.or_insert_with(|| Remote::new(id, HashMap::default()));
|
|
||||||
|
|
||||||
entry.refs.insert(refname.to_string(), r.oid().into());
|
entry.insert(refname.to_string(), r.oid().into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Remotes::new(remotes))
|
Ok(remotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a ref string.
|
/// Parse a ref string.
|
||||||
|
|
@ -90,6 +89,7 @@ pub fn commit<'a>(
|
||||||
let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
|
let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
|
||||||
let tree_id = repo.index()?.write_tree()?;
|
let tree_id = repo.index()?.write_tree()?;
|
||||||
let tree = repo.find_tree(tree_id)?;
|
let tree = repo.find_tree(tree_id)?;
|
||||||
|
// TODO: Take the ref as parameter.
|
||||||
let oid = repo.commit(None, &sig, &sig, message, &tree, &[parent])?;
|
let oid = repo.commit(None, &sig, &sig, message, &tree, &[parent])?;
|
||||||
let commit = repo.find_commit(oid).unwrap();
|
let commit = repo.find_commit(oid).unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ use crate::protocol::config::ProjectTracking;
|
||||||
use crate::protocol::message::Message;
|
use crate::protocol::message::Message;
|
||||||
use crate::protocol::peer::{Peer, PeerError, PeerState};
|
use crate::protocol::peer::{Peer, PeerError, PeerState};
|
||||||
use crate::storage::{self, ReadRepository, WriteRepository};
|
use crate::storage::{self, ReadRepository, WriteRepository};
|
||||||
use crate::storage::{Inventory, ReadStorage, Remotes, Unverified, WriteStorage};
|
use crate::storage::{Inventory, ReadStorage, Remotes, WriteStorage};
|
||||||
|
|
||||||
pub use crate::protocol::config::{Config, Network};
|
pub use crate::protocol::config::{Config, Network};
|
||||||
|
|
||||||
|
|
@ -339,21 +339,11 @@ where
|
||||||
let user = *self.storage.user_id();
|
let user = *self.storage.user_id();
|
||||||
let repo = self.storage.repository(&proj).unwrap();
|
let repo = self.storage.repository(&proj).unwrap();
|
||||||
let remote = repo.remote(&user).unwrap();
|
let remote = repo.remote(&user).unwrap();
|
||||||
let refs = remote.refs;
|
|
||||||
let signature = self
|
|
||||||
.signer
|
|
||||||
.sign(serde_json::to_vec(&refs).unwrap().as_slice());
|
|
||||||
let peers = self.peers.negotiated().map(|(_, p)| p.addr);
|
let peers = self.peers.negotiated().map(|(_, p)| p.addr);
|
||||||
|
let refs = remote.refs.unverified();
|
||||||
|
|
||||||
self.context.broadcast(
|
self.context
|
||||||
Message::RefsUpdate {
|
.broadcast(Message::RefsUpdate { proj, user, refs }, peers);
|
||||||
proj,
|
|
||||||
user,
|
|
||||||
refs,
|
|
||||||
signature,
|
|
||||||
},
|
|
||||||
peers,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -556,7 +546,7 @@ impl<S, T, G> Iterator for Protocol<S, T, G> {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Lookup {
|
pub struct Lookup {
|
||||||
/// Whether the project was found locally or not.
|
/// Whether the project was found locally or not.
|
||||||
pub local: Option<Remotes<Unverified>>,
|
pub local: Option<Remotes<crypto::Verified>>,
|
||||||
/// A list of remote peers on which the project is known to exist.
|
/// A list of remote peers on which the project is known to exist.
|
||||||
pub remote: Vec<NodeId>,
|
pub remote: Vec<NodeId>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::crypto;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
use crate::protocol::{Context, NodeId, Timestamp, PROTOCOL_VERSION};
|
use crate::protocol::{Context, NodeId, Timestamp, PROTOCOL_VERSION};
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
use crate::storage::refs::SignedRefs;
|
||||||
|
|
||||||
/// Message envelope. All messages sent over the network are wrapped in this type.
|
/// Message envelope. All messages sent over the network are wrapped in this type.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -104,9 +105,7 @@ pub enum Message {
|
||||||
/// User signing.
|
/// User signing.
|
||||||
user: UserId,
|
user: UserId,
|
||||||
/// Updated refs.
|
/// Updated refs.
|
||||||
refs: storage::Refs,
|
refs: SignedRefs<crypto::Unverified>,
|
||||||
/// Signature over the refs.
|
|
||||||
signature: crypto::Signature,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,18 +194,8 @@ impl Peer {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(
|
(PeerState::Negotiated { git, .. }, Message::RefsUpdate { proj, user, refs }) => {
|
||||||
PeerState::Negotiated { git, .. },
|
if refs.verified(&user).is_ok() {
|
||||||
Message::RefsUpdate {
|
|
||||||
proj,
|
|
||||||
user,
|
|
||||||
refs,
|
|
||||||
signature,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
let bytes = serde_json::to_vec(&refs).unwrap();
|
|
||||||
|
|
||||||
if user.verify(&signature, &bytes).is_ok() {
|
|
||||||
// TODO: Buffer/throttle fetches.
|
// TODO: Buffer/throttle fetches.
|
||||||
// TODO: Also pass the updated refs so that we can check whether
|
// TODO: Also pass the updated refs so that we can check whether
|
||||||
// we need to fetch or not.
|
// we need to fetch or not.
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ use git_url::Url;
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::crypto::Verified;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity::ProjId;
|
use crate::identity::ProjId;
|
||||||
use crate::storage::git::RADICLE_ID_REF;
|
use crate::storage::git::RADICLE_ID_REF;
|
||||||
|
use crate::storage::refs::SignedRefs;
|
||||||
use crate::storage::{BranchName, ReadRepository as _};
|
use crate::storage::{BranchName, ReadRepository as _};
|
||||||
use crate::{identity, storage};
|
use crate::{identity, storage};
|
||||||
|
|
||||||
|
|
@ -37,7 +39,7 @@ pub fn init<S: storage::WriteStorage>(
|
||||||
description: &str,
|
description: &str,
|
||||||
default_branch: BranchName,
|
default_branch: BranchName,
|
||||||
storage: S,
|
storage: S,
|
||||||
) -> Result<ProjId, InitError> {
|
) -> Result<(ProjId, SignedRefs<Verified>), InitError> {
|
||||||
let user_id = storage.user_id();
|
let user_id = storage.user_id();
|
||||||
let delegate = identity::Delegate {
|
let delegate = identity::Delegate {
|
||||||
// TODO: Use actual user name.
|
// TODO: Use actual user name.
|
||||||
|
|
@ -114,8 +116,9 @@ pub fn init<S: storage::WriteStorage>(
|
||||||
],
|
],
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
let signed = storage.sign_refs(&project)?;
|
||||||
|
|
||||||
Ok(id)
|
Ok((id, signed))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -136,7 +139,7 @@ mod tests {
|
||||||
let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap();
|
let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap();
|
||||||
let _branch = repo.branch("master", &head, false).unwrap();
|
let _branch = repo.branch("master", &head, false).unwrap();
|
||||||
|
|
||||||
init(
|
let (_id, _refs) = init(
|
||||||
&repo,
|
&repo,
|
||||||
"acme",
|
"acme",
|
||||||
"Acme's repo",
|
"Acme's repo",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod git;
|
pub mod git;
|
||||||
|
pub mod refs;
|
||||||
|
|
||||||
use std::collections::hash_map;
|
use std::collections::hash_map;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
@ -6,19 +7,21 @@ use std::marker::PhantomData;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use git_url::Url;
|
use radicle_git_ext as git_ext;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
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::crypto::{self, Unverified, Verified};
|
||||||
use crate::git::RefError;
|
use crate::git::RefError;
|
||||||
|
use crate::git::Url;
|
||||||
use crate::identity;
|
use crate::identity;
|
||||||
use crate::identity::{ProjId, ProjIdError, UserId};
|
use crate::identity::{ProjId, ProjIdError, UserId};
|
||||||
|
use crate::storage::refs::Refs;
|
||||||
|
|
||||||
pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new(".rad/identity.toml"));
|
use self::refs::SignedRefs;
|
||||||
|
|
||||||
pub type BranchName = String;
|
pub type BranchName = String;
|
||||||
pub type Inventory = Vec<ProjId>;
|
pub type Inventory = Vec<ProjId>;
|
||||||
|
|
@ -30,6 +33,8 @@ pub enum Error {
|
||||||
InvalidRef,
|
InvalidRef,
|
||||||
#[error("git reference error: {0}")]
|
#[error("git reference error: {0}")]
|
||||||
Ref(#[from] RefError),
|
Ref(#[from] RefError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Refs(#[from] refs::Error),
|
||||||
#[error("git: {0}")]
|
#[error("git: {0}")]
|
||||||
Git(#[from] git2::Error),
|
Git(#[from] git2::Error),
|
||||||
#[error("id: {0}")]
|
#[error("id: {0}")]
|
||||||
|
|
@ -42,28 +47,38 @@ pub enum Error {
|
||||||
InvalidHead,
|
InvalidHead,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Refs = HashMap<RefName, Oid>;
|
|
||||||
pub type RemoteId = UserId;
|
pub type RemoteId = UserId;
|
||||||
pub type RefName = String;
|
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.
|
/// Project remotes. Tracks the git state of a project.
|
||||||
#[derive(Debug, 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<V> Deref for Remotes<V> {
|
||||||
pub fn new(remotes: HashMap<RemoteId, Remote<Unverified>>) -> Self {
|
type Target = HashMap<RemoteId, Remote<V>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Remotes<V> {
|
||||||
|
pub fn new(remotes: HashMap<RemoteId, Remote<V>>) -> Self {
|
||||||
Self(remotes)
|
Self(remotes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Remotes<Unverified> {
|
impl Remotes<Verified> {
|
||||||
|
pub fn unverified(self) -> Remotes<Unverified> {
|
||||||
|
Remotes(
|
||||||
|
self.into_iter()
|
||||||
|
.map(|(id, r)| (id, r.unverified()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Default for Remotes<V> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(HashMap::default())
|
Self(HashMap::default())
|
||||||
}
|
}
|
||||||
|
|
@ -90,13 +105,25 @@ impl Into<HashMap<String, Remote<Unverified>>> for Remotes<Unverified> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::from_over_into)]
|
||||||
|
impl<V> Into<HashMap<RemoteId, Refs>> for Remotes<V> {
|
||||||
|
fn into(self) -> HashMap<RemoteId, Refs> {
|
||||||
|
let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into());
|
||||||
|
|
||||||
|
for (k, v) in self.into_iter() {
|
||||||
|
remotes.insert(k, v.refs.into());
|
||||||
|
}
|
||||||
|
remotes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A project remote.
|
/// A project remote.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Remote<V> {
|
pub struct Remote<V> {
|
||||||
/// ID of remote.
|
/// ID of remote.
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
/// Git references published under this remote, and their hashes.
|
/// Git references published under this remote, and their hashes.
|
||||||
pub refs: HashMap<RefName, Oid>,
|
pub refs: SignedRefs<V>,
|
||||||
/// Whether this remote is of a project delegate.
|
/// Whether this remote is of a project delegate.
|
||||||
pub 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.
|
||||||
|
|
@ -104,21 +131,45 @@ pub struct Remote<V> {
|
||||||
verified: PhantomData<V>,
|
verified: PhantomData<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Remote<Unverified> {
|
impl<V> Remote<V> {
|
||||||
pub fn new(id: UserId, refs: HashMap<RefName, Oid>) -> Self {
|
pub fn new(id: UserId, refs: impl Into<SignedRefs<V>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
refs,
|
refs: refs.into(),
|
||||||
delegate: false,
|
delegate: false,
|
||||||
verified: PhantomData,
|
verified: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Remote<Unverified> {
|
||||||
|
pub fn verified(self) -> Result<Remote<Verified>, crypto::Error> {
|
||||||
|
let refs = self.refs.verified(&self.id)?;
|
||||||
|
|
||||||
|
Ok(Remote {
|
||||||
|
id: self.id,
|
||||||
|
refs,
|
||||||
|
delegate: self.delegate,
|
||||||
|
verified: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Remote<Verified> {
|
||||||
|
pub fn unverified(self) -> Remote<Unverified> {
|
||||||
|
Remote {
|
||||||
|
id: self.id,
|
||||||
|
refs: self.refs.unverified(),
|
||||||
|
delegate: self.delegate,
|
||||||
|
verified: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ReadStorage {
|
pub trait ReadStorage {
|
||||||
fn user_id(&self) -> &UserId;
|
fn user_id(&self) -> &UserId;
|
||||||
fn url(&self) -> Url;
|
fn url(&self) -> Url;
|
||||||
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error>;
|
||||||
fn inventory(&self) -> Result<Inventory, Error>;
|
fn inventory(&self) -> Result<Inventory, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,16 +177,26 @@ pub trait WriteStorage: ReadStorage {
|
||||||
type Repository: WriteRepository;
|
type Repository: WriteRepository;
|
||||||
|
|
||||||
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error>;
|
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error>;
|
||||||
|
fn sign_refs(&self, repository: &Self::Repository) -> Result<SignedRefs<Verified>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ReadRepository {
|
pub trait ReadRepository {
|
||||||
fn path(&self) -> &Path;
|
fn path(&self) -> &Path;
|
||||||
fn remote(&self, user: &UserId) -> Result<Remote<Unverified>, Error>;
|
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
|
||||||
fn remotes(&self) -> Result<Remotes<Unverified>, Error>;
|
fn reference(
|
||||||
|
&self,
|
||||||
|
user: &UserId,
|
||||||
|
reference: &str,
|
||||||
|
) -> Result<Option<git2::Reference>, git2::Error>;
|
||||||
|
fn reference_oid(&self, user: &UserId, reference: &str) -> Result<Option<Oid>, git2::Error>;
|
||||||
|
fn references(&self, user: &UserId) -> Result<Refs, Error>;
|
||||||
|
fn remote(&self, user: &UserId) -> Result<Remote<Verified>, refs::Error>;
|
||||||
|
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WriteRepository: ReadRepository {
|
pub trait WriteRepository: ReadRepository {
|
||||||
fn fetch(&mut self, url: &Url) -> Result<(), git2::Error>;
|
fn fetch(&mut self, url: &Url) -> Result<(), git2::Error>;
|
||||||
|
fn raw(&self) -> &git2::Repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S> ReadStorage for T
|
impl<T, S> ReadStorage for T
|
||||||
|
|
@ -155,7 +216,7 @@ where
|
||||||
self.deref().inventory()
|
self.deref().inventory()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
|
||||||
self.deref().get(proj)
|
self.deref().get(proj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +231,10 @@ where
|
||||||
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
|
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
|
||||||
self.deref().repository(proj)
|
self.deref().repository(proj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_refs(&self, repository: &S::Repository) -> Result<SignedRefs<Verified>, Error> {
|
||||||
|
self.deref().sign_refs(repository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,32 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fmt, fs, io};
|
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;
|
||||||
|
|
||||||
pub use radicle_git_ext::Oid;
|
pub use radicle_git_ext::Oid;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
use crate::crypto::Signer;
|
use crate::crypto::{Signer, Verified};
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
|
use crate::storage::refs;
|
||||||
use super::{
|
use crate::storage::refs::{Refs, SignedRefs};
|
||||||
Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, Unverified, WriteRepository,
|
use crate::storage::{
|
||||||
WriteStorage,
|
Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, WriteRepository, WriteStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::RemoteId;
|
||||||
|
|
||||||
pub static RADICLE_ID_REF: Lazy<refspec::PatternString> =
|
pub static RADICLE_ID_REF: Lazy<refspec::PatternString> =
|
||||||
Lazy::new(|| refspec::pattern!("heads/radicle/id"));
|
Lazy::new(|| refspec::pattern!("heads/radicle/id"));
|
||||||
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
|
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
|
||||||
Lazy::new(|| refspec::pattern!("refs/remotes/*"));
|
Lazy::new(|| refspec::pattern!("refs/remotes/*"));
|
||||||
|
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
|
||||||
|
Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
|
||||||
|
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
@ -40,16 +44,28 @@ impl ReadStorage for Storage {
|
||||||
self.signer.public_key()
|
self.signer.public_key()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn url(&self) -> Url {
|
fn url(&self) -> git::Url {
|
||||||
Url {
|
git::Url {
|
||||||
scheme: git_url::Scheme::File,
|
scheme: git_url::Scheme::File,
|
||||||
host: Some(self.path.to_string_lossy().to_string()),
|
host: Some(self.path.to_string_lossy().to_string()),
|
||||||
..Url::default()
|
..git::Url::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, _id: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
fn get(&self, id: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
|
||||||
todo!()
|
// TODO: Don't create a repo here if it doesn't exist?
|
||||||
|
// Perhaps for checking we could have a `contains` method?
|
||||||
|
match self.repository(id) {
|
||||||
|
Ok(r) => {
|
||||||
|
let remotes = r.remotes()?;
|
||||||
|
if remotes.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(remotes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inventory(&self) -> Result<Inventory, Error> {
|
fn inventory(&self) -> Result<Inventory, Error> {
|
||||||
|
|
@ -63,10 +79,23 @@ impl WriteStorage for Storage {
|
||||||
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
|
fn repository(&self, proj: &ProjId) -> Result<Self::Repository, Error> {
|
||||||
Repository::open(self.path.join(proj.to_string()))
|
Repository::open(self.path.join(proj.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_refs(&self, repository: &Repository) -> Result<SignedRefs<Verified>, Error> {
|
||||||
|
let remote = self.signer.public_key();
|
||||||
|
let refs = repository.references(remote)?;
|
||||||
|
let signed = refs.signed(self.signer.clone())?;
|
||||||
|
|
||||||
|
signed.save(remote, repository)?;
|
||||||
|
|
||||||
|
Ok(signed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
pub fn open<P: AsRef<Path>>(path: P, signer: impl Signer) -> Result<Self, io::Error> {
|
pub fn open<P: AsRef<Path>, S: Signer + 'static>(
|
||||||
|
path: P,
|
||||||
|
signer: S,
|
||||||
|
) -> Result<Self, io::Error> {
|
||||||
let path = path.as_ref().to_path_buf();
|
let path = path.as_ref().to_path_buf();
|
||||||
|
|
||||||
match fs::create_dir_all(&path) {
|
match fs::create_dir_all(&path) {
|
||||||
|
|
@ -89,6 +118,13 @@ impl Storage {
|
||||||
self.signer.clone()
|
self.signer.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_signer(&self, signer: impl Signer + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
path: self.path.clone(),
|
||||||
|
signer: Arc::new(signer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn projects(&self) -> Result<Vec<ProjId>, Error> {
|
pub fn projects(&self) -> Result<Vec<ProjId>, Error> {
|
||||||
let mut projects = Vec::new();
|
let mut projects = Vec::new();
|
||||||
|
|
||||||
|
|
@ -100,10 +136,28 @@ impl Storage {
|
||||||
}
|
}
|
||||||
Ok(projects)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inspect(&self) -> Result<(), Error> {
|
||||||
|
for proj in self.projects()? {
|
||||||
|
let repo = self.repository(&proj)?;
|
||||||
|
|
||||||
|
for r in repo.raw().references()? {
|
||||||
|
let r = r?;
|
||||||
|
let name = r.name().ok_or(Error::InvalidRef)?;
|
||||||
|
let oid = r.target().ok_or(Error::InvalidRef)?;
|
||||||
|
|
||||||
|
println!("{} {} {}", proj, oid, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Repository {
|
pub struct Repository {
|
||||||
pub(crate) backend: git2::Repository,
|
pub(crate) backend: git2::Repository,
|
||||||
|
// TODO: Add project id here so we can refer to it
|
||||||
|
// in a bunch of places. We could write it to the
|
||||||
|
// git config for later.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
|
|
@ -117,6 +171,11 @@ impl Repository {
|
||||||
.no_reinit(true)
|
.no_reinit(true)
|
||||||
.external_template(false),
|
.external_template(false),
|
||||||
)?;
|
)?;
|
||||||
|
let mut config = backend.config()?;
|
||||||
|
|
||||||
|
// TODO: Get ahold of user name and/or key.
|
||||||
|
config.set_str("user.name", "radicle")?;
|
||||||
|
config.set_str("user.email", "radicle@localhost")?;
|
||||||
|
|
||||||
Ok(backend)
|
Ok(backend)
|
||||||
}
|
}
|
||||||
|
|
@ -127,15 +186,15 @@ impl Repository {
|
||||||
Ok(Self { backend })
|
Ok(Self { backend })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_reference(&self, remote: &UserId, name: &str) -> Result<Oid, Error> {
|
pub fn inspect(&self) -> Result<(), Error> {
|
||||||
let name = format!("refs/remotes/{}/{}", remote, name);
|
for r in self.backend.references()? {
|
||||||
let target = self
|
let r = r?;
|
||||||
.backend
|
let name = r.name().ok_or(Error::InvalidRef)?;
|
||||||
.find_reference(&name)?
|
let oid = r.target().ok_or(Error::InvalidRef)?;
|
||||||
.target()
|
|
||||||
.ok_or(Error::InvalidRef)?;
|
|
||||||
|
|
||||||
Ok(target.into())
|
println!("{} {}", oid, name);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,38 +203,69 @@ impl ReadRepository for Repository {
|
||||||
self.backend.path()
|
self.backend.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remote(&self, user: &UserId) -> Result<Remote<Unverified>, Error> {
|
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error> {
|
||||||
// TODO: Only fetch standard refs.
|
git_ext::Blob::At {
|
||||||
|
object: oid.into(),
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
.get(&self.backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference(
|
||||||
|
&self,
|
||||||
|
remote: &RemoteId,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<git2::Reference>, git2::Error> {
|
||||||
|
let name = format!("refs/remotes/{remote}/{name}");
|
||||||
|
self.backend.find_reference(&name).map(Some).or_else(|e| {
|
||||||
|
if git_ext::is_not_found_err(&e) {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference_oid(&self, user: &RemoteId, reference: &str) -> Result<Option<Oid>, git2::Error> {
|
||||||
|
let reference = self.reference(user, reference)?;
|
||||||
|
Ok(reference.and_then(|r| r.target().map(|o| o.into())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
|
||||||
|
let refs = SignedRefs::load(remote, self)?;
|
||||||
|
Ok(Remote::new(*remote, refs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn references(&self, remote: &RemoteId) -> Result<Refs, Error> {
|
||||||
|
// TODO: Only return known refs, eg. heads/ rad/ tags/ etc..
|
||||||
let entries = self
|
let entries = self
|
||||||
.backend
|
.backend
|
||||||
.references_glob(format!("refs/remotes/{}/*", user).as_str())?;
|
.references_glob(format!("refs/remotes/{remote}/*").as_str())?;
|
||||||
let mut refs = HashMap::default();
|
let mut refs = BTreeMap::new();
|
||||||
|
|
||||||
for e in entries {
|
for e in entries {
|
||||||
let e = e?;
|
let e = e?;
|
||||||
let name = e.name().ok_or(Error::InvalidRef)?;
|
let name = e.name().ok_or(Error::InvalidRef)?;
|
||||||
let (_, refname) = git::parse_ref::<UserId>(name)?;
|
let (_, refname) = git::parse_ref::<RemoteId>(name)?;
|
||||||
let oid = e.target().ok_or(Error::InvalidRef)?;
|
let oid = e.target().ok_or(Error::InvalidRef)?;
|
||||||
|
|
||||||
refs.insert(refname.to_string(), oid.into());
|
refs.insert(refname.to_string(), oid.into());
|
||||||
}
|
}
|
||||||
Ok(Remote::new(*user, refs))
|
Ok(refs.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
|
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
|
||||||
let refs = self.backend.references_glob(REMOTES_GLOB.as_str())?;
|
// TODO: Should use the id glob here instead of signature.
|
||||||
|
let refs = self.backend.references_glob(SIGNATURES_GLOB.as_str())?;
|
||||||
let mut remotes = HashMap::default();
|
let mut remotes = HashMap::default();
|
||||||
|
|
||||||
for r in refs {
|
for r in refs {
|
||||||
let r = r?;
|
let r = r?;
|
||||||
let name = r.name().ok_or(Error::InvalidRef)?;
|
let name = r.name().ok_or(refs::Error::InvalidRef)?;
|
||||||
let (id, refname) = git::parse_ref::<UserId>(name)?;
|
let (id, _) = git::parse_ref::<RemoteId>(name)?;
|
||||||
let entry = remotes
|
let remote = self.remote(&id)?;
|
||||||
.entry(id)
|
|
||||||
.or_insert_with(|| Remote::new(id, HashMap::default()));
|
|
||||||
let oid = r.target().ok_or(Error::InvalidRef)?;
|
|
||||||
|
|
||||||
entry.refs.insert(refname.to_string(), oid.into());
|
remotes.insert(id, remote);
|
||||||
}
|
}
|
||||||
Ok(Remotes::new(remotes))
|
Ok(Remotes::new(remotes))
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +273,7 @@ impl ReadRepository for Repository {
|
||||||
|
|
||||||
impl WriteRepository for Repository {
|
impl WriteRepository for Repository {
|
||||||
/// Fetch all remotes of a project from the given URL.
|
/// Fetch all remotes of a project from the given URL.
|
||||||
fn fetch(&mut self, url: &Url) -> Result<(), git2::Error> {
|
fn fetch(&mut self, url: &git::Url) -> Result<(), git2::Error> {
|
||||||
// TODO: Have function to fetch specific remotes.
|
// TODO: Have function to fetch specific remotes.
|
||||||
// TODO: Return meaningful info on success.
|
// TODO: Return meaningful info on success.
|
||||||
//
|
//
|
||||||
|
|
@ -207,6 +297,10 @@ impl WriteRepository for Repository {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raw(&self) -> &git2::Repository {
|
||||||
|
&self.backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<git2::Repository> for Repository {
|
impl From<git2::Repository> for Repository {
|
||||||
|
|
@ -219,28 +313,34 @@ impl From<git2::Repository> for Repository {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
|
use crate::storage::refs::SIGNATURE_REF;
|
||||||
use crate::storage::{ReadStorage, WriteRepository};
|
use crate::storage::{ReadStorage, WriteRepository};
|
||||||
|
use crate::test::arbitrary;
|
||||||
use crate::test::crypto::MockSigner;
|
use crate::test::crypto::MockSigner;
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
use git_url::Url;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_remotes() {
|
fn test_remote_refs() {
|
||||||
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_remotes(&Url {
|
let mut refs = git::remote_refs(&git::Url {
|
||||||
host: Some(dir.path().to_string_lossy().to_string()),
|
host: Some(dir.path().to_string_lossy().to_string()),
|
||||||
scheme: git_url::Scheme::File,
|
scheme: git_url::Scheme::File,
|
||||||
path: format!("/{}", proj).into(),
|
path: format!("/{}", proj).into(),
|
||||||
..Url::default()
|
..git::Url::default()
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let remotes = storage.repository(proj).unwrap().remotes().unwrap();
|
let project = storage.repository(proj).unwrap();
|
||||||
|
let remotes = project.remotes().unwrap();
|
||||||
|
|
||||||
assert_eq!(refs, remotes);
|
// Strip the remote refs of sigrefs so we can compare them.
|
||||||
|
for remote in refs.values_mut() {
|
||||||
|
remote.remove(SIGNATURE_REF).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(refs, remotes.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -256,27 +356,56 @@ mod tests {
|
||||||
// Have Bob fetch Alice's refs.
|
// Have Bob fetch Alice's refs.
|
||||||
bob.repository(proj)
|
bob.repository(proj)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.fetch(&Url {
|
.fetch(&git::Url {
|
||||||
host: Some(alice.path().to_string_lossy().to_string()),
|
host: Some(alice.path().to_string_lossy().to_string()),
|
||||||
scheme: git_url::Scheme::File,
|
scheme: git_url::Scheme::File,
|
||||||
path: format!("/{}", proj).into(),
|
path: format!("/{}", proj).into(),
|
||||||
..Url::default()
|
..git::Url::default()
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for (id, _) in remotes.into_iter() {
|
for (id, _) in remotes.into_iter() {
|
||||||
let alice_oid = alice
|
let alice_repo = alice.repository(proj).unwrap();
|
||||||
.repository(proj)
|
let alice_oid = alice_repo.reference(&id, refname).unwrap().unwrap();
|
||||||
.unwrap()
|
|
||||||
.find_reference(&id, refname)
|
|
||||||
.unwrap();
|
|
||||||
let bob_oid = bob
|
|
||||||
.repository(proj)
|
|
||||||
.unwrap()
|
|
||||||
.find_reference(&id, refname)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(alice_oid, bob_oid);
|
let bob_repo = bob.repository(proj).unwrap();
|
||||||
|
let bob_oid = bob_repo.reference(&id, refname).unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(alice_oid.target(), bob_oid.target());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_refs() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let mut rng = fastrand::Rng::new();
|
||||||
|
let signer = MockSigner::new(&mut rng);
|
||||||
|
let storage = Storage::open(tmp.path(), signer).unwrap();
|
||||||
|
let proj_id = arbitrary::gen::<ProjId>(1);
|
||||||
|
let alice = *storage.user_id();
|
||||||
|
let project = storage.repository(&proj_id).unwrap();
|
||||||
|
let backend = &project.backend;
|
||||||
|
let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
|
||||||
|
let head = git::initial_commit(backend, &sig).unwrap();
|
||||||
|
|
||||||
|
let head = git::commit(backend, &head, "Second commit", &alice.to_string()).unwrap();
|
||||||
|
backend
|
||||||
|
.reference(
|
||||||
|
&format!("refs/remotes/{alice}/heads/master"),
|
||||||
|
head.id(),
|
||||||
|
false,
|
||||||
|
"test",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let signed = storage.sign_refs(&project).unwrap();
|
||||||
|
let remote = project.remote(&alice).unwrap();
|
||||||
|
let mut unsigned = project.references(&alice).unwrap();
|
||||||
|
|
||||||
|
// The signed refs doesn't contain the signature ref itself.
|
||||||
|
unsigned.remove(SIGNATURE_REF).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(remote.refs, signed);
|
||||||
|
assert_eq!(*remote.refs, unsigned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use radicle_git_ext as git_ext;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::crypto;
|
||||||
|
use crate::crypto::{PublicKey, Signature, Signer, Unverified, Verified};
|
||||||
|
use crate::git;
|
||||||
|
use crate::git::Oid;
|
||||||
|
use crate::storage;
|
||||||
|
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
|
||||||
|
|
||||||
|
pub const SIGNATURE_REF: &str = "radicle/signature";
|
||||||
|
pub const REFS_BLOB_PATH: &str = "refs";
|
||||||
|
pub const SIGNATURE_BLOB_PATH: &str = "signature";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Updated {
|
||||||
|
/// The computed [`Refs`] were stored as a new commit.
|
||||||
|
Updated { oid: Oid },
|
||||||
|
/// The stored [`Refs`] were the same as the computed ones, so no new commit
|
||||||
|
/// was created.
|
||||||
|
Unchanged { oid: Oid },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("invalid signature: {0}")]
|
||||||
|
InvalidSignature(#[from] crypto::Error),
|
||||||
|
#[error("canonical refs: {0}")]
|
||||||
|
Canonical(#[from] canonical::Error),
|
||||||
|
#[error("invalid reference")]
|
||||||
|
InvalidRef,
|
||||||
|
#[error("invalid reference: {0}")]
|
||||||
|
Ref(#[from] git::RefError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
GitExt(#[from] git_ext::Error),
|
||||||
|
#[error("refs were not found")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The published state of a local repository.
|
||||||
|
#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Refs(BTreeMap<String, Oid>);
|
||||||
|
|
||||||
|
impl Refs {
|
||||||
|
/// Verify the given signature on these refs, and return [`SignedRefs`] on success.
|
||||||
|
pub fn verified(
|
||||||
|
self,
|
||||||
|
signer: &PublicKey,
|
||||||
|
signature: Signature,
|
||||||
|
) -> Result<SignedRefs<Verified>, Error> {
|
||||||
|
let refs = self;
|
||||||
|
let msg = refs.canonical();
|
||||||
|
|
||||||
|
match signer.verify(&signature, &msg) {
|
||||||
|
Ok(()) => Ok(SignedRefs {
|
||||||
|
refs,
|
||||||
|
signature,
|
||||||
|
_verified: PhantomData,
|
||||||
|
}),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign these refs with the given signer and return [`SignedRefs`].
|
||||||
|
pub fn signed<S>(self, signer: S) -> Result<SignedRefs<Verified>, Error>
|
||||||
|
where
|
||||||
|
S: Signer,
|
||||||
|
{
|
||||||
|
let refs = self;
|
||||||
|
let msg = refs.canonical();
|
||||||
|
let signature = signer.sign(&msg);
|
||||||
|
|
||||||
|
Ok(SignedRefs {
|
||||||
|
refs,
|
||||||
|
signature,
|
||||||
|
_verified: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create refs from a canonical representation.
|
||||||
|
pub fn from_canonical(bytes: &[u8]) -> Result<Self, canonical::Error> {
|
||||||
|
let reader = BufReader::new(bytes);
|
||||||
|
let mut refs = BTreeMap::new();
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line?;
|
||||||
|
let (oid, name) = line
|
||||||
|
.split_once(' ')
|
||||||
|
.ok_or(canonical::Error::InvalidFormat)?;
|
||||||
|
|
||||||
|
let name = name.to_owned();
|
||||||
|
let oid = Oid::from_str(oid)?;
|
||||||
|
|
||||||
|
refs.insert(name, oid);
|
||||||
|
}
|
||||||
|
Ok(Self(refs))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canonical(&self) -> Vec<u8> {
|
||||||
|
let mut buf = String::new();
|
||||||
|
let refs = self.iter().filter(|(name, _)| *name != SIGNATURE_REF);
|
||||||
|
|
||||||
|
for (name, oid) in refs {
|
||||||
|
buf.push_str(&oid.to_string());
|
||||||
|
buf.push(' ');
|
||||||
|
buf.push_str(name);
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
buf.into_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Refs> for BTreeMap<String, Oid> {
|
||||||
|
fn from(refs: Refs) -> Self {
|
||||||
|
refs.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> From<SignedRefs<V>> for Refs {
|
||||||
|
fn from(signed: SignedRefs<V>) -> Self {
|
||||||
|
signed.refs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BTreeMap<String, Oid>> for Refs {
|
||||||
|
fn from(refs: BTreeMap<String, Oid>) -> Self {
|
||||||
|
Self(refs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Refs {
|
||||||
|
type Target = BTreeMap<String, Oid>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Refs {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combination of [`Refs`] and a [`Signature`]. The signature is a cryptographic
|
||||||
|
/// signature over the refs. This allows us to easily verify if a set of refs
|
||||||
|
/// came from a particular user.
|
||||||
|
///
|
||||||
|
/// The type parameter keeps track of whether the signature was [`Verified`] or
|
||||||
|
/// [`Unverified`].
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SignedRefs<V> {
|
||||||
|
refs: Refs,
|
||||||
|
signature: Signature,
|
||||||
|
#[serde(skip)]
|
||||||
|
_verified: PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignedRefs<Unverified> {
|
||||||
|
pub fn new(refs: Refs, signature: Signature) -> Self {
|
||||||
|
Self {
|
||||||
|
refs,
|
||||||
|
signature,
|
||||||
|
_verified: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verified(self, signer: &PublicKey) -> Result<SignedRefs<Verified>, crypto::Error> {
|
||||||
|
let canonical = self.refs.canonical();
|
||||||
|
|
||||||
|
match signer.verify(&self.signature, &canonical) {
|
||||||
|
Ok(()) => Ok(SignedRefs {
|
||||||
|
refs: self.refs,
|
||||||
|
signature: self.signature,
|
||||||
|
_verified: PhantomData,
|
||||||
|
}),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignedRefs<Verified> {
|
||||||
|
pub fn load<S>(remote: &RemoteId, repo: &S) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
S: ReadRepository,
|
||||||
|
{
|
||||||
|
if let Some(oid) = repo.reference_oid(remote, SIGNATURE_REF)? {
|
||||||
|
Self::load_at(oid, remote, repo)
|
||||||
|
} else {
|
||||||
|
Err(Error::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_at<S>(oid: Oid, remote: &RemoteId, repo: &S) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
S: storage::ReadRepository,
|
||||||
|
{
|
||||||
|
let refs = repo.blob_at(oid, Path::new(REFS_BLOB_PATH))?;
|
||||||
|
let signature = repo.blob_at(oid, Path::new(SIGNATURE_BLOB_PATH))?;
|
||||||
|
let signature = crypto::Signature::try_from(signature.content())?;
|
||||||
|
|
||||||
|
match remote.verify(&signature, refs.content()) {
|
||||||
|
Ok(()) => {
|
||||||
|
let refs = Refs::from_canonical(refs.content())?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
refs,
|
||||||
|
signature,
|
||||||
|
_verified: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the signed refs to disk.
|
||||||
|
/// This creates a new commit on the signed refs branch, and updates the branch pointer.
|
||||||
|
pub fn save<S: WriteRepository>(
|
||||||
|
&self,
|
||||||
|
// TODO: This should be part of the signed refs.
|
||||||
|
remote: &RemoteId,
|
||||||
|
repo: &S,
|
||||||
|
) -> Result<Updated, Error> {
|
||||||
|
let parent: Option<git2::Commit> = repo
|
||||||
|
.reference(remote, SIGNATURE_REF)?
|
||||||
|
.map(|r| r.peel_to_commit())
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let tree = {
|
||||||
|
let raw = repo.raw();
|
||||||
|
let refs_blob_oid = raw.blob(&self.canonical())?;
|
||||||
|
let sig_blob_oid = raw.blob(&self.signature.to_bytes())?;
|
||||||
|
|
||||||
|
let mut builder = raw.treebuilder(None)?;
|
||||||
|
builder.insert(REFS_BLOB_PATH, refs_blob_oid, 0o100_644)?;
|
||||||
|
builder.insert(SIGNATURE_BLOB_PATH, sig_blob_oid, 0o100_644)?;
|
||||||
|
|
||||||
|
let oid = builder.write()?;
|
||||||
|
|
||||||
|
raw.find_tree(oid)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if let Some(ref parent) = parent {
|
||||||
|
if parent.tree()?.id() == tree.id() {
|
||||||
|
return Ok(Updated::Unchanged {
|
||||||
|
oid: parent.id().into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sigref = format!("refs/remotes/{remote}/{SIGNATURE_REF}");
|
||||||
|
let author = repo.raw().signature()?;
|
||||||
|
let commit = repo.raw().commit(
|
||||||
|
Some(&sigref),
|
||||||
|
&author,
|
||||||
|
&author,
|
||||||
|
&format!("Update {} for {}", SIGNATURE_REF, remote),
|
||||||
|
&tree,
|
||||||
|
&parent.iter().collect::<Vec<&git2::Commit>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match commit {
|
||||||
|
Ok(oid) => Ok(Updated::Updated { oid: oid.into() }),
|
||||||
|
Err(e) => match (e.class(), e.code()) {
|
||||||
|
(git2::ErrorClass::Object, git2::ErrorCode::Modified) => {
|
||||||
|
log::warn!("Concurrent modification of refs: {:?}", e);
|
||||||
|
|
||||||
|
Err(Error::Git(e))
|
||||||
|
}
|
||||||
|
_ => Err(e.into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unverified(self) -> SignedRefs<Unverified> {
|
||||||
|
SignedRefs {
|
||||||
|
refs: self.refs,
|
||||||
|
signature: self.signature,
|
||||||
|
_verified: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Deref for SignedRefs<V> {
|
||||||
|
type Target = Refs;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.refs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod canonical {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("invalid canonical format")]
|
||||||
|
InvalidFormat,
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Test canonical/from_canonical
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::ops::RangeBounds;
|
use std::ops::RangeBounds;
|
||||||
|
|
||||||
|
use quickcheck::Arbitrary;
|
||||||
|
|
||||||
use crate::collections::HashMap;
|
use crate::collections::HashMap;
|
||||||
use crate::crypto::PublicKey;
|
use crate::crypto::{self, Signer};
|
||||||
|
use crate::crypto::{PublicKey, SecretKey};
|
||||||
use crate::hash;
|
use crate::hash;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::ProjId;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
use crate::storage::refs::Refs;
|
||||||
use crate::test::storage::MockStorage;
|
use crate::test::storage::MockStorage;
|
||||||
|
|
||||||
pub fn set<T: Eq + Hash + quickcheck::Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
|
use super::crypto::MockSigner;
|
||||||
|
|
||||||
|
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
|
||||||
let size = fastrand::usize(range);
|
let size = fastrand::usize(range);
|
||||||
let mut set = HashSet::with_capacity(size);
|
let mut set = HashSet::with_capacity(size);
|
||||||
let mut g = quickcheck::Gen::new(size);
|
let mut g = quickcheck::Gen::new(size);
|
||||||
|
|
@ -20,35 +26,34 @@ pub fn set<T: Eq + Hash + quickcheck::Arbitrary>(range: impl RangeBounds<usize>)
|
||||||
set
|
set
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gen<T: quickcheck::Arbitrary>(size: usize) -> T {
|
pub fn gen<T: Arbitrary>(size: usize) -> T {
|
||||||
let mut gen = quickcheck::Gen::new(size);
|
let mut gen = quickcheck::Gen::new(size);
|
||||||
|
|
||||||
T::arbitrary(&mut gen)
|
T::arbitrary(&mut gen)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl quickcheck::Arbitrary for storage::Remotes<storage::Unverified> {
|
impl Arbitrary for storage::Remotes<crypto::Verified> {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
let remotes: HashMap<storage::RemoteId, storage::Remote<storage::Unverified>> =
|
let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
|
||||||
quickcheck::Arbitrary::arbitrary(g);
|
Arbitrary::arbitrary(g);
|
||||||
|
|
||||||
storage::Remotes::new(remotes)
|
storage::Remotes::new(remotes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl quickcheck::Arbitrary for MockStorage {
|
impl Arbitrary for MockStorage {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
let inventory = quickcheck::Arbitrary::arbitrary(g);
|
let inventory = Arbitrary::arbitrary(g);
|
||||||
MockStorage::new(inventory)
|
MockStorage::new(inventory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl quickcheck::Arbitrary for storage::Remote<storage::Unverified> {
|
impl Arbitrary for Refs {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
let mut refs: BTreeMap<storage::RefName, storage::Oid> = BTreeMap::new();
|
||||||
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];
|
||||||
|
// TODO: Use refs other than branch names.
|
||||||
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) {
|
||||||
|
|
@ -59,25 +64,46 @@ impl quickcheck::Arbitrary for storage::Remote<storage::Unverified> {
|
||||||
refs.insert(name.to_string(), oid);
|
refs.insert(name.to_string(), oid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage::Remote::new(id, refs)
|
Self::from(refs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl quickcheck::Arbitrary for ProjId {
|
impl Arbitrary for storage::Remote<crypto::Verified> {
|
||||||
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
|
let refs = Refs::arbitrary(g);
|
||||||
|
let signer = MockSigner::arbitrary(g);
|
||||||
|
let signed = refs.signed(&signer).unwrap();
|
||||||
|
|
||||||
|
storage::Remote::new(*signer.public_key(), signed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arbitrary for MockSigner {
|
||||||
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
|
let mut bytes: [u8; 32] = [0; 32];
|
||||||
|
|
||||||
|
for byte in &mut bytes {
|
||||||
|
*byte = u8::arbitrary(g);
|
||||||
|
}
|
||||||
|
MockSigner::from(SecretKey::from(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arbitrary for ProjId {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
let digest = hash::Digest::arbitrary(g);
|
let digest = hash::Digest::arbitrary(g);
|
||||||
ProjId::from(digest)
|
ProjId::from(digest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl quickcheck::Arbitrary for hash::Digest {
|
impl Arbitrary for hash::Digest {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
let bytes: Vec<u8> = quickcheck::Arbitrary::arbitrary(g);
|
let bytes: Vec<u8> = Arbitrary::arbitrary(g);
|
||||||
hash::Digest::new(&bytes)
|
hash::Digest::new(&bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl quickcheck::Arbitrary for PublicKey {
|
impl Arbitrary for PublicKey {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
use ed25519_consensus::SigningKey;
|
use ed25519_consensus::SigningKey;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use ed25519_consensus as ed25519;
|
||||||
|
|
||||||
use crate::crypto::{PublicKey, SecretKey, Signer};
|
use crate::crypto::{PublicKey, SecretKey, Signer};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MockSigner {
|
pub struct MockSigner {
|
||||||
pk: PublicKey,
|
pk: PublicKey,
|
||||||
sk: SecretKey,
|
sk: SecretKey,
|
||||||
|
|
@ -15,12 +15,14 @@ impl MockSigner {
|
||||||
for byte in &mut bytes {
|
for byte in &mut bytes {
|
||||||
*byte = rng.u8(..);
|
*byte = rng.u8(..);
|
||||||
}
|
}
|
||||||
let sk = SecretKey::from(bytes);
|
Self::from(SecretKey::from(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
impl From<SecretKey> for MockSigner {
|
||||||
pk: sk.verification_key().into(),
|
fn from(sk: SecretKey) -> Self {
|
||||||
sk,
|
let pk = sk.verification_key().into();
|
||||||
}
|
Self { sk, pk }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,6 +38,20 @@ impl Default for MockSigner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for MockSigner {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.pk == other.pk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for MockSigner {}
|
||||||
|
|
||||||
|
impl std::hash::Hash for MockSigner {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.pk.hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Signer for MockSigner {
|
impl Signer for MockSigner {
|
||||||
fn public_key(&self) -> &PublicKey {
|
fn public_key(&self) -> &PublicKey {
|
||||||
&self.pk
|
&self.pk
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,66 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::ProjId;
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
use crate::storage::WriteStorage;
|
use crate::storage::{ReadStorage, WriteStorage};
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
use crate::test::crypto::MockSigner;
|
use crate::test::crypto::MockSigner;
|
||||||
|
|
||||||
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
|
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let storage = Storage::open(path, MockSigner::default()).unwrap();
|
|
||||||
let proj_ids = arbitrary::set::<ProjId>(3..5);
|
let proj_ids = arbitrary::set::<ProjId>(3..5);
|
||||||
let user_ids = arbitrary::set::<UserId>(1..3);
|
let signers = arbitrary::set::<MockSigner>(1..3);
|
||||||
|
let mut storages = signers
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| Storage::open(path, s).unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
crate::test::logger::init(log::Level::Debug);
|
crate::test::logger::init(log::Level::Debug);
|
||||||
|
|
||||||
for proj in proj_ids.iter() {
|
for storage in &storages {
|
||||||
log::debug!("creating {}...", proj);
|
let remote = storage.user_id();
|
||||||
let repo = storage.repository(proj).unwrap();
|
|
||||||
|
|
||||||
for user in user_ids.iter() {
|
log::debug!("signer {}...", remote);
|
||||||
let repo = &repo.backend;
|
|
||||||
let sig = git2::Signature::now(&user.to_string(), "anonymous@radicle.xyz").unwrap();
|
|
||||||
let head = git::initial_commit(repo, &sig).unwrap();
|
|
||||||
|
|
||||||
log::debug!("{}: creating {}...", proj, user);
|
for proj in proj_ids.iter() {
|
||||||
|
let repo = storage.repository(proj).unwrap();
|
||||||
|
let raw = &repo.backend;
|
||||||
|
let sig = git2::Signature::now(&remote.to_string(), "anonymous@radicle.xyz").unwrap();
|
||||||
|
let head = git::initial_commit(raw, &sig).unwrap();
|
||||||
|
|
||||||
repo.reference(
|
log::debug!("{}: creating {}...", remote, proj);
|
||||||
&format!("refs/remotes/{user}/heads/radicle/id"),
|
|
||||||
|
raw.reference(
|
||||||
|
&format!("refs/remotes/{remote}/heads/radicle/id"),
|
||||||
head.id(),
|
head.id(),
|
||||||
false,
|
false,
|
||||||
"test",
|
"test",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let head = git::commit(repo, &head, "Second commit", &user.to_string()).unwrap();
|
let head = git::commit(raw, &head, "Second commit", &remote.to_string()).unwrap();
|
||||||
repo.reference(
|
raw.reference(
|
||||||
&format!("refs/remotes/{user}/heads/master"),
|
&format!("refs/remotes/{remote}/heads/master"),
|
||||||
head.id(),
|
head.id(),
|
||||||
false,
|
false,
|
||||||
"test",
|
"test",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let head = git::commit(repo, &head, "Third commit", &user.to_string()).unwrap();
|
let head = git::commit(raw, &head, "Third commit", &remote.to_string()).unwrap();
|
||||||
repo.reference(
|
raw.reference(
|
||||||
&format!("refs/remotes/{user}/heads/patch/3"),
|
&format!("refs/remotes/{remote}/heads/patch/3"),
|
||||||
head.id(),
|
head.id(),
|
||||||
false,
|
false,
|
||||||
"test",
|
"test",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
storage.sign_refs(&repo).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage
|
storages.pop().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
use git_url::Url;
|
use git_url::Url;
|
||||||
|
|
||||||
|
use crate::crypto::Verified;
|
||||||
use crate::identity::{ProjId, UserId};
|
use crate::identity::{ProjId, UserId};
|
||||||
|
use crate::storage::refs;
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, Unverified, WriteRepository,
|
Error, Inventory, ReadRepository, ReadStorage, Remote, Remotes, WriteRepository, WriteStorage,
|
||||||
WriteStorage,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MockStorage {
|
pub struct MockStorage {
|
||||||
pub inventory: Vec<(ProjId, Remotes<Unverified>)>,
|
pub inventory: Vec<(ProjId, Remotes<Verified>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockStorage {
|
impl MockStorage {
|
||||||
pub fn new(inventory: Vec<(ProjId, Remotes<Unverified>)>) -> Self {
|
pub fn new(inventory: Vec<(ProjId, Remotes<Verified>)>) -> Self {
|
||||||
Self { inventory }
|
Self { inventory }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ impl ReadStorage for MockStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Verified>>, Error> {
|
||||||
if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
|
if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
|
||||||
return Ok(Some(refs.clone()));
|
return Ok(Some(refs.clone()));
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +61,13 @@ impl WriteStorage for MockStorage {
|
||||||
fn repository(&self, _proj: &ProjId) -> Result<Self::Repository, Error> {
|
fn repository(&self, _proj: &ProjId) -> Result<Self::Repository, Error> {
|
||||||
Ok(MockRepository {})
|
Ok(MockRepository {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_refs(
|
||||||
|
&self,
|
||||||
|
_repository: &Self::Repository,
|
||||||
|
) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MockRepository {}
|
pub struct MockRepository {}
|
||||||
|
|
@ -69,11 +77,39 @@ impl ReadRepository for MockRepository {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remote(&self, _user: &UserId) -> Result<Remote<Unverified>, Error> {
|
fn remote(&self, _user: &UserId) -> Result<Remote<Verified>, refs::Error> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remotes(&self) -> Result<Remotes<Unverified>, Error> {
|
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blob_at<'a>(
|
||||||
|
&'a self,
|
||||||
|
_oid: radicle_git_ext::Oid,
|
||||||
|
_path: &'a std::path::Path,
|
||||||
|
) -> Result<git2::Blob<'a>, radicle_git_ext::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference(
|
||||||
|
&self,
|
||||||
|
_user: &UserId,
|
||||||
|
_reference: &str,
|
||||||
|
) -> Result<Option<git2::Reference>, git2::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reference_oid(
|
||||||
|
&self,
|
||||||
|
_user: &UserId,
|
||||||
|
_reference: &str,
|
||||||
|
) -> Result<Option<radicle_git_ext::Oid>, git2::Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn references(&self, _user: &UserId) -> Result<crate::storage::refs::Refs, Error> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,4 +118,8 @@ impl WriteRepository for MockRepository {
|
||||||
fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
|
fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raw(&self) -> &git2::Repository {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue