Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Split repository into two creates
Alexis Sellier committed 3 years ago
commit 46501507beb069b053b4ff871a6d934da529c76a
parent d1ccf1b807252aa1e2c8f7e1eb693df72e20a043
46 files changed +4156 -4142
modified Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [

[[package]]
name = "anyhow"
-
version = "1.0.64"
+
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
+
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"

[[package]]
name = "arrayref"
@@ -115,12 +115,6 @@ dependencies = [
]

[[package]]
-
name = "bs58"
-
version = "0.4.0"
-
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
-

-
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -277,9 +271,9 @@ dependencies = [

[[package]]
name = "digest"
-
version = "0.10.3"
+
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [
 "block-buffer 0.10.3",
 "crypto-common",
@@ -496,9 +490,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
-
version = "0.2.132"
+
version = "0.2.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"

[[package]]
name = "libgit2-sys"
@@ -646,9 +640,9 @@ dependencies = [

[[package]]
name = "once_cell"
-
version = "1.14.0"
+
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
+
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"

[[package]]
name = "opaque-debug"
@@ -765,6 +759,31 @@ dependencies = [
]

[[package]]
+
name = "radicle"
+
version = "0.2.0"
+
dependencies = [
+
 "ed25519-compact",
+
 "fastrand",
+
 "git-ref-format",
+
 "git-url",
+
 "git2",
+
 "log",
+
 "multibase",
+
 "nonempty",
+
 "olpc-cjson",
+
 "once_cell",
+
 "quickcheck",
+
 "quickcheck_macros",
+
 "radicle-git-ext",
+
 "serde",
+
 "serde_json",
+
 "sha2 0.10.6",
+
 "siphasher",
+
 "tempfile",
+
 "thiserror",
+
]
+

+
[[package]]
name = "radicle-git-ext"
version = "0.1.0"
source = "git+https://github.com/radicle-dev/radicle-link?tag=cycle/2022-07-12#541a8161cb24c3b7b10d44f958cc5c5ed05cf443"
@@ -784,30 +803,20 @@ version = "0.2.0"
dependencies = [
 "anyhow",
 "bloomy",
-
 "bs58",
 "byteorder",
 "chrono",
 "colored",
 "crossbeam-channel",
-
 "ed25519-compact",
 "fastrand",
-
 "git-ref-format",
-
 "git-url",
-
 "git2",
 "log",
-
 "multibase",
 "nakamoto-net",
 "nakamoto-net-poll",
 "nonempty",
-
 "olpc-cjson",
-
 "once_cell",
 "quickcheck",
 "quickcheck_macros",
-
 "radicle-git-ext",
+
 "radicle",
 "serde",
 "serde_json",
-
 "sha2 0.10.5",
-
 "siphasher",
 "tempfile",
 "thiserror",
]
@@ -828,9 +837,9 @@ dependencies = [

[[package]]
name = "rand_core"
-
version = "0.6.3"
+
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
 "getrandom",
]
@@ -919,13 +928,13 @@ dependencies = [

[[package]]
name = "sha2"
-
version = "0.10.5"
+
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5"
+
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
 "cfg-if",
 "cpufeatures",
-
 "digest 0.10.3",
+
 "digest 0.10.5",
]

[[package]]
@@ -958,9 +967,9 @@ dependencies = [

[[package]]
name = "syn"
-
version = "1.0.99"
+
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
dependencies = [
 "proc-macro2",
 "quote",
@@ -983,18 +992,18 @@ dependencies = [

[[package]]
name = "thiserror"
-
version = "1.0.34"
+
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
+
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
dependencies = [
 "thiserror-impl",
]

[[package]]
name = "thiserror-impl"
-
version = "1.0.34"
+
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
+
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
dependencies = [
 "proc-macro2",
 "quote",
@@ -1041,15 +1050,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"

[[package]]
name = "unicode-ident"
-
version = "1.0.3"
+
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"

[[package]]
name = "unicode-normalization"
-
version = "0.1.21"
+
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
+
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
 "tinyvec",
]
modified Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-
members = ["radicle-node"]
+
members = ["radicle", "radicle-node"]

[patch.crates-io.radicle-git-ext]
git = "https://github.com/radicle-dev/radicle-link"
modified radicle-node/Cargo.toml
@@ -7,32 +7,26 @@ edition = "2021"

[dependencies]
anyhow = { version = "1" }
-
bs58 = { version = "0.4.0" }
-
ed25519-compact = { version = "1.0.12", features = ["pem"] }
byteorder = { version = "1" }
bloomy = { version = "1.2" }
chrono = { version = "0.4.0" }
colored = { version = "1.9.0" }
crossbeam-channel = { version = "0.5.6" }
fastrand = { version = "1.8.0" }
-
git-ref-format = { version = "0", features = ["serde", "macro"] }
-
git2 = { version = "0.13" }
-
git-url = { version = "0.3.5", features = ["serde1"] }
-
multibase = { version = "0.9.1" }
log = { version = "0.4.17", features = ["std"] }
-
once_cell = { version = "1.13" }
-
olpc-cjson = { version = "0.1.1" }
-
sha2 = { version = "0.10.2" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
-
siphasher = { version = "0.3.10" }
-
radicle-git-ext = { version = "0", features = ["serde"] }
nonempty = { version = "0.8.0", features = ["serialize"] }
nakamoto-net = { version = "0.3.0" }
nakamoto-net-poll = { version = "0.3.0" }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }

+
[dependencies.radicle]
+
path = "../radicle"
+
version = "0.2.0"
+

[dev-dependencies]
+
radicle = { path = "../radicle", version = "*", features = ["test"] }
quickcheck = { version = "1", default-features = false }
quickcheck_macros = { version = "1", default-features = false }
deleted radicle-node/src/collections.rs
@@ -1,44 +0,0 @@
-
//! Useful collections for peer-to-peer networking.
-
use siphasher::sip::SipHasher13;
-

-
/// A `HashMap` which uses [`fastrand::Rng`] for its random state.
-
pub type HashMap<K, V> = std::collections::HashMap<K, V, RandomState>;
-

-
/// A `HashSet` which uses [`fastrand::Rng`] for its random state.
-
pub type HashSet<K> = std::collections::HashSet<K, RandomState>;
-

-
/// Random hasher state.
-
#[derive(Clone)]
-
pub struct RandomState {
-
    key1: u64,
-
    key2: u64,
-
}
-

-
impl Default for RandomState {
-
    fn default() -> Self {
-
        Self::new(fastrand::Rng::new())
-
    }
-
}
-

-
impl RandomState {
-
    fn new(rng: fastrand::Rng) -> Self {
-
        Self {
-
            key1: rng.u64(..),
-
            key2: rng.u64(..),
-
        }
-
    }
-
}
-

-
impl std::hash::BuildHasher for RandomState {
-
    type Hasher = SipHasher13;
-

-
    fn build_hasher(&self) -> Self::Hasher {
-
        SipHasher13::new_with_keys(self.key1, self.key2)
-
    }
-
}
-

-
impl From<fastrand::Rng> for RandomState {
-
    fn from(rng: fastrand::Rng) -> Self {
-
        Self::new(rng)
-
    }
-
}
deleted radicle-node/src/crypto.rs
@@ -1,225 +0,0 @@
-
use std::sync::Arc;
-
use std::{fmt, ops::Deref, str::FromStr};
-

-
use ed25519_compact as ed25519;
-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
pub use ed25519::{Error, KeyPair, Seed};
-

-
/// Verified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub struct Verified;
-
/// Unverified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub struct Unverified;
-

-
pub trait Signer: Send + Sync {
-
    /// Return this signer's public/verification key.
-
    fn public_key(&self) -> &PublicKey;
-
    /// Sign a message and return the signature.
-
    fn sign(&self, msg: &[u8]) -> Signature;
-
}
-

-
impl<T> Signer for Arc<T>
-
where
-
    T: Signer + ?Sized,
-
{
-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.deref().sign(msg)
-
    }
-

-
    fn public_key(&self) -> &PublicKey {
-
        self.deref().public_key()
-
    }
-
}
-

-
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()
-
    }
-
}
-

-
/// Cryptographic signature.
-
#[derive(PartialEq, Eq, Copy, Clone)]
-
pub struct Signature(pub ed25519::Signature);
-

-
impl fmt::Display for Signature {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        let base = multibase::Base::Base58Btc;
-
        write!(f, "{}", multibase::encode(base, self.deref()))
-
    }
-
}
-

-
impl fmt::Debug for Signature {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Signature({})", self)
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum SignatureError {
-
    #[error("invalid multibase string: {0}")]
-
    Multibase(#[from] multibase::Error),
-
    #[error("invalid signature: {0}")]
-
    Invalid(#[from] ed25519::Error),
-
}
-

-
impl From<ed25519::Signature> for Signature {
-
    fn from(other: ed25519::Signature) -> Self {
-
        Self(other)
-
    }
-
}
-

-
impl FromStr for Signature {
-
    type Err = SignatureError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
-

-
        Ok(Self(sig))
-
    }
-
}
-

-
impl Deref for Signature {
-
    type Target = ed25519::Signature;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl From<[u8; 64]> for Signature {
-
    fn from(bytes: [u8; 64]) -> Self {
-
        Self(ed25519::Signature::new(bytes))
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Signature {
-
    type Error = ed25519::Error;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-
        ed25519::Signature::from_slice(bytes).map(Self)
-
    }
-
}
-

-
/// The public/verification key.
-
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
-
#[serde(into = "String", try_from = "String")]
-
pub struct PublicKey(pub ed25519::PublicKey);
-

-
/// The private/signing key.
-
pub type SecretKey = ed25519::SecretKey;
-

-
#[derive(Error, Debug)]
-
pub enum PublicKeyError {
-
    #[error("invalid length {0}")]
-
    InvalidLength(usize),
-
    #[error("invalid multibase string: {0}")]
-
    Multibase(#[from] multibase::Error),
-
    #[error("invalid key: {0}")]
-
    InvalidKey(#[from] ed25519::Error),
-
}
-

-
impl std::hash::Hash for PublicKey {
-
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-
        self.0.deref().hash(state)
-
    }
-
}
-

-
impl fmt::Display for PublicKey {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.to_human())
-
    }
-
}
-

-
impl From<PublicKey> for String {
-
    fn from(other: PublicKey) -> Self {
-
        other.to_human()
-
    }
-
}
-

-
impl fmt::Debug for PublicKey {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "PublicKey({})", self)
-
    }
-
}
-

-
impl PartialEq for PublicKey {
-
    fn eq(&self, other: &Self) -> bool {
-
        self.0 == other.0
-
    }
-
}
-

-
impl From<ed25519::PublicKey> for PublicKey {
-
    fn from(other: ed25519::PublicKey) -> Self {
-
        Self(other)
-
    }
-
}
-

-
impl TryFrom<[u8; 32]> for PublicKey {
-
    type Error = ed25519::Error;
-

-
    fn try_from(other: [u8; 32]) -> Result<Self, Self::Error> {
-
        Ok(Self(ed25519::PublicKey::new(other)))
-
    }
-
}
-

-
impl PublicKey {
-
    pub fn to_human(&self) -> String {
-
        multibase::encode(multibase::Base::Base58Btc, self.0.deref())
-
    }
-
}
-

-
impl FromStr for PublicKey {
-
    type Err = PublicKeyError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let array: [u8; 32] = bytes
-
            .try_into()
-
            .map_err(|v: Vec<u8>| PublicKeyError::InvalidLength(v.len()))?;
-
        let key = ed25519::PublicKey::new(array);
-

-
        Ok(Self(key))
-
    }
-
}
-

-
impl TryFrom<String> for PublicKey {
-
    type Error = PublicKeyError;
-

-
    fn try_from(value: String) -> Result<Self, Self::Error> {
-
        Self::from_str(&value)
-
    }
-
}
-

-
impl Deref for PublicKey {
-
    type Target = ed25519::PublicKey;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use crate::crypto::PublicKey;
-
    use quickcheck_macros::quickcheck;
-
    use std::str::FromStr;
-

-
    #[quickcheck]
-
    fn prop_encode_decode(input: PublicKey) {
-
        let encoded = input.to_string();
-
        let decoded = PublicKey::from_str(&encoded).unwrap();
-

-
        assert_eq!(input, decoded);
-
    }
-
}
deleted radicle-node/src/git.rs
@@ -1,243 +0,0 @@
-
use std::path::Path;
-
use std::str::FromStr;
-

-
use git_ref_format as format;
-
use once_cell::sync::Lazy;
-

-
use crate::collections::HashMap;
-
use crate::crypto::PublicKey;
-
use crate::storage::refs::Refs;
-
use crate::storage::RemoteId;
-

-
pub use ext::Error;
-
pub use ext::Oid;
-
pub use git_ref_format as fmt;
-
pub use git_ref_format::{refname, RefStr, RefString};
-
pub use git_url as url;
-
pub use git_url::Url;
-
pub use radicle_git_ext as ext;
-

-
/// Default port of the `git` transport protocol.
-
pub const PROTOCOL_PORT: u16 = 9418;
-

-
#[derive(thiserror::Error, Debug)]
-
pub enum RefError {
-
    #[error("invalid ref name '{0}'")]
-
    InvalidName(format::RefString),
-
    #[error("invalid ref format: {0}")]
-
    Format(#[from] format::Error),
-
}
-

-
#[derive(thiserror::Error, Debug)]
-
pub enum ListRefsError {
-
    #[error("git error: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("invalid ref: {0}")]
-
    InvalidRef(#[from] RefError),
-
}
-

-
pub mod refs {
-
    use super::*;
-

-
    /// Where project information is kept.
-
    pub static IDENTITY_BRANCH: Lazy<RefString> = Lazy::new(|| refname!("radicle/id"));
-

-
    pub mod storage {
-
        use super::*;
-

-
        pub fn branch(remote: &RemoteId, branch: &str) -> String {
-
            format!("refs/remotes/{remote}/heads/{branch}")
-
        }
-

-
        /// Get the branch used to track project information.
-
        pub fn id(remote: &RemoteId) -> String {
-
            branch(remote, &IDENTITY_BRANCH)
-
        }
-
    }
-

-
    pub mod workdir {
-
        pub fn branch(branch: &str) -> String {
-
            format!("refs/heads/{branch}")
-
        }
-

-
        pub fn note(name: &str) -> String {
-
            format!("refs/notes/{name}")
-
        }
-

-
        pub fn remote_branch(remote: &str, branch: &str) -> String {
-
            format!("refs/remotes/{remote}/{branch}")
-
        }
-

-
        pub fn tag(name: &str) -> String {
-
            format!("refs/tags/{name}")
-
        }
-
    }
-
}
-

-
/// List remote refs of a project, given the remote URL.
-
pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError> {
-
    let url = url.to_string();
-
    let mut remotes = HashMap::default();
-
    let mut remote = git2::Remote::create_detached(&url)?;
-

-
    remote.connect(git2::Direction::Fetch)?;
-

-
    let refs = remote.list()?;
-
    for r in refs {
-
        let (id, refname) = parse_ref::<PublicKey>(r.name())?;
-
        let entry = remotes.entry(id).or_insert_with(Refs::default);
-

-
        entry.insert(refname, r.oid().into());
-
    }
-

-
    Ok(remotes)
-
}
-

-
/// Parse a ref string.
-
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
-
    let input = format::RefStr::try_from_str(s)?;
-
    let suffix = input
-
        .strip_prefix(format::refname!("refs/remotes"))
-
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
-

-
    let mut components = suffix.components();
-
    let id = components
-
        .next()
-
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
-
    let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
-
    let refstr = components.collect::<format::RefString>();
-

-
    Ok((id, refstr))
-
}
-

-
/// Create an initial empty commit.
-
pub fn initial_commit<'a>(
-
    repo: &'a git2::Repository,
-
    sig: &git2::Signature,
-
) -> Result<git2::Commit<'a>, git2::Error> {
-
    let tree_id = repo.index()?.write_tree()?;
-
    let tree = repo.find_tree(tree_id)?;
-
    let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
-
    let commit = repo.find_commit(oid).unwrap();
-

-
    Ok(commit)
-
}
-

-
/// Create a commit and update the given ref to it.
-
pub fn commit<'a>(
-
    repo: &'a git2::Repository,
-
    parent: &'a git2::Commit,
-
    target: &RefStr,
-
    message: &str,
-
    user: &str,
-
) -> Result<git2::Commit<'a>, git2::Error> {
-
    let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
-
    let tree_id = repo.index()?.write_tree()?;
-
    let tree = repo.find_tree(tree_id)?;
-
    let oid = repo.commit(Some(target.as_str()), &sig, &sig, message, &tree, &[parent])?;
-
    let commit = repo.find_commit(oid).unwrap();
-

-
    Ok(commit)
-
}
-

-
/// Push the refs to the radicle remote.
-
pub fn push(repo: &git2::Repository) -> Result<(), git2::Error> {
-
    let mut remote = repo.find_remote("rad")?;
-
    let refspecs = remote.push_refspecs().unwrap();
-
    let refspec = refspecs.into_iter().next().unwrap().unwrap();
-

-
    // The `git2` crate doesn't seem to support push refspecs with '*' in them,
-
    // so we manually replace it with the current branch.
-
    let head = repo.head().unwrap();
-
    let branch = head.shorthand().unwrap();
-
    let refspec = refspec.replace('*', branch);
-

-
    remote.push::<&str>(&[&refspec], None)
-
}
-

-
/// Get the repository head.
-
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
-
    let head = repo.head()?.peel_to_commit()?;
-

-
    Ok(head)
-
}
-

-
/// Write a tree with the given blob at the given path.
-
pub fn write_tree<'r>(
-
    path: &Path,
-
    bytes: &[u8],
-
    repo: &'r git2::Repository,
-
) -> Result<git2::Tree<'r>, Error> {
-
    let blob_id = repo.blob(bytes)?;
-
    let mut builder = repo.treebuilder(None)?;
-
    builder.insert(path, blob_id, 0o100_644)?;
-

-
    let tree_id = builder.write()?;
-
    let tree = repo.find_tree(tree_id)?;
-

-
    Ok(tree)
-
}
-

-
/// Configure a repository's radicle remote.
-
///
-
/// Takes the repository in which to configure the remote, the name of the remote, the public
-
/// key of the remote, and the path to the remote repository on the filesystem.
-
pub fn configure_remote<'r>(
-
    repo: &'r git2::Repository,
-
    remote_name: &str,
-
    remote_id: &RemoteId,
-
    remote_path: &Path,
-
) -> Result<git2::Remote<'r>, git2::Error> {
-
    let url = Url {
-
        scheme: git_url::Scheme::File,
-
        path: remote_path.to_string_lossy().to_string().into(),
-

-
        ..Url::default()
-
    };
-
    let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*");
-
    let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*");
-
    let remote = repo.remote_with_fetch(remote_name, url.to_string().as_str(), &fetch)?;
-
    repo.remote_add_push(remote_name, &push)?;
-

-
    Ok(remote)
-
}
-

-
/// Set the upstream of the given branch to the given remote.
-
///
-
/// This writes to the `config` directly. The entry will look like the
-
/// following:
-
///
-
/// ```text
-
/// [branch "main"]
-
///     remote = rad
-
///     merge = refs/heads/main
-
/// ```
-
pub fn set_upstream(
-
    repo: &git2::Repository,
-
    remote: &str,
-
    branch: &str,
-
    merge: &str,
-
) -> Result<(), git2::Error> {
-
    let mut config = repo.config()?;
-
    let branch_remote = format!("branch.{}.remote", branch);
-
    let branch_merge = format!("branch.{}.merge", branch);
-

-
    config.remove_multivar(&branch_remote, ".*").or_else(|e| {
-
        if ext::is_not_found_err(&e) {
-
            Ok(())
-
        } else {
-
            Err(e)
-
        }
-
    })?;
-
    config.remove_multivar(&branch_merge, ".*").or_else(|e| {
-
        if ext::is_not_found_err(&e) {
-
            Ok(())
-
        } else {
-
            Err(e)
-
        }
-
    })?;
-
    config.set_multivar(&branch_remote, ".*", remote)?;
-
    config.set_multivar(&branch_merge, ".*", merge)?;
-

-
    Ok(())
-
}
deleted radicle-node/src/hash.rs
@@ -1,69 +0,0 @@
-
use std::{convert::TryInto, fmt};
-

-
use serde::{Deserialize, Serialize};
-
use sha2::{
-
    digest::{generic_array::GenericArray, OutputSizeUser},
-
    Digest as _, Sha256,
-
};
-
use thiserror::Error;
-

-
#[derive(Debug, Clone, PartialEq, Eq, Error)]
-
pub enum DecodeError {
-
    #[error("invalid digest length {0}")]
-
    InvalidLength(usize),
-
}
-

-
/// A SHA-256 hash.
-
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Digest([u8; 32]);
-

-
impl Digest {
-
    pub fn new(bytes: impl AsRef<[u8]>) -> Self {
-
        Self::from(Sha256::digest(bytes))
-
    }
-
}
-

-
impl AsRef<[u8; 32]> for Digest {
-
    fn as_ref(&self) -> &[u8; 32] {
-
        &self.0
-
    }
-
}
-

-
impl fmt::Debug for Digest {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Hash({})", self)
-
    }
-
}
-

-
impl fmt::Display for Digest {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        for byte in &self.0 {
-
            write!(f, "{:02x}", byte)?;
-
        }
-
        Ok(())
-
    }
-
}
-

-
impl From<[u8; 32]> for Digest {
-
    fn from(bytes: [u8; 32]) -> Self {
-
        Self(bytes)
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Digest {
-
    type Error = DecodeError;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, DecodeError> {
-
        let bytes: [u8; 32] = bytes
-
            .try_into()
-
            .map_err(|_| DecodeError::InvalidLength(bytes.len()))?;
-

-
        Ok(bytes.into())
-
    }
-
}
-

-
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
-
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
-
        Self(array.into())
-
    }
-
}
deleted radicle-node/src/identity.rs
@@ -1,246 +0,0 @@
-
pub mod doc;
-

-
use std::ops::Deref;
-
use std::path::PathBuf;
-
use std::{ffi::OsString, fmt, str::FromStr};
-

-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
use crate::crypto;
-
use crate::crypto::Verified;
-
use crate::git;
-
use crate::serde_ext;
-
use crate::storage::Remotes;
-

-
pub use crypto::PublicKey;
-
pub use doc::{Delegate, Doc};
-

-
#[derive(Error, Debug)]
-
pub enum IdError {
-
    #[error("invalid git object id: {0}")]
-
    InvalidOid(#[from] git2::Error),
-
    #[error(transparent)]
-
    Multibase(#[from] multibase::Error),
-
}
-

-
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Id(git::Oid);
-

-
impl fmt::Display for Id {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        f.write_str(&self.to_human())
-
    }
-
}
-

-
impl fmt::Debug for Id {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Id({})", self)
-
    }
-
}
-

-
impl Id {
-
    pub fn to_human(&self) -> String {
-
        multibase::encode(multibase::Base::Base58Btc, self.0.as_bytes())
-
    }
-

-
    pub fn from_human(s: &str) -> Result<Self, IdError> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let array: git::Oid = bytes.as_slice().try_into()?;
-

-
        Ok(Self(array))
-
    }
-
}
-

-
impl FromStr for Id {
-
    type Err = IdError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        Self::from_human(s)
-
    }
-
}
-

-
impl TryFrom<OsString> for Id {
-
    type Error = IdError;
-

-
    fn try_from(value: OsString) -> Result<Self, Self::Error> {
-
        let string = value.to_string_lossy();
-
        Self::from_str(&string)
-
    }
-
}
-

-
impl From<git::Oid> for Id {
-
    fn from(oid: git::Oid) -> Self {
-
        Self(oid)
-
    }
-
}
-

-
impl From<git2::Oid> for Id {
-
    fn from(oid: git2::Oid) -> Self {
-
        Self(oid.into())
-
    }
-
}
-

-
impl Deref for Id {
-
    type Target = git::Oid;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
impl serde::Serialize for Id {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: serde::Serializer,
-
    {
-
        serde_ext::string::serialize(self, serializer)
-
    }
-
}
-

-
impl<'de> serde::Deserialize<'de> for Id {
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: serde::Deserializer<'de>,
-
    {
-
        serde_ext::string::deserialize(deserializer)
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum DidError {
-
    #[error("invalid did: {0}")]
-
    Did(String),
-
    #[error("invalid public key: {0}")]
-
    PublicKey(#[from] crypto::PublicKeyError),
-
}
-

-
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
-
#[serde(into = "String", try_from = "String")]
-
pub struct Did(crypto::PublicKey);
-

-
impl Did {
-
    pub fn encode(&self) -> String {
-
        format!("did:key:{}", self.0.to_human())
-
    }
-

-
    pub fn decode(input: &str) -> Result<Self, DidError> {
-
        let key = input
-
            .strip_prefix("did:key:")
-
            .ok_or_else(|| DidError::Did(input.to_owned()))?;
-

-
        crypto::PublicKey::from_str(key)
-
            .map(Did)
-
            .map_err(DidError::from)
-
    }
-
}
-

-
impl From<crypto::PublicKey> for Did {
-
    fn from(key: crypto::PublicKey) -> Self {
-
        Self(key)
-
    }
-
}
-

-
impl From<Did> for String {
-
    fn from(other: Did) -> Self {
-
        other.encode()
-
    }
-
}
-

-
impl TryFrom<String> for Did {
-
    type Error = DidError;
-

-
    fn try_from(value: String) -> Result<Self, Self::Error> {
-
        Self::decode(&value)
-
    }
-
}
-

-
impl fmt::Display for Did {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "{}", self.encode())
-
    }
-
}
-

-
impl fmt::Debug for Did {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Did({:?})", self.to_string())
-
    }
-
}
-

-
impl Deref for Did {
-
    type Target = PublicKey;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.0
-
    }
-
}
-

-
/// A stored and verified project.
-
#[derive(Debug, Clone)]
-
pub struct Project {
-
    /// The project identifier.
-
    pub id: Id,
-
    /// The latest project identity document.
-
    pub doc: Doc<Verified>,
-
    /// The project remotes.
-
    pub remotes: Remotes<Verified>,
-
    /// On-disk file path for this project's repository.
-
    pub path: PathBuf,
-
}
-

-
impl Project {
-
    pub fn delegate(&mut self, name: String, key: crypto::PublicKey) -> bool {
-
        self.doc.delegate(Delegate {
-
            name,
-
            id: Did::from(key),
-
        })
-
    }
-
}
-

-
impl Deref for Project {
-
    type Target = Doc<Verified>;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.doc
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use super::*;
-
    use crate::crypto::PublicKey;
-
    use quickcheck_macros::quickcheck;
-
    use std::collections::HashSet;
-

-
    #[quickcheck]
-
    fn prop_key_equality(a: PublicKey, b: PublicKey) {
-
        assert_ne!(a, b);
-

-
        let mut hm = HashSet::new();
-

-
        assert!(hm.insert(a));
-
        assert!(hm.insert(b));
-
        assert!(!hm.insert(a));
-
        assert!(!hm.insert(b));
-
    }
-

-
    #[quickcheck]
-
    fn prop_from_str(input: Id) {
-
        let encoded = input.to_string();
-
        let decoded = Id::from_str(&encoded).unwrap();
-

-
        assert_eq!(input, decoded);
-
    }
-

-
    #[quickcheck]
-
    fn prop_json_eq_str(pk: PublicKey, proj: Id, did: Did) {
-
        let json = serde_json::to_string(&pk).unwrap();
-
        assert_eq!(format!("\"{}\"", pk), json);
-

-
        let json = serde_json::to_string(&proj).unwrap();
-
        assert_eq!(format!("\"{}\"", proj), json);
-

-
        let json = serde_json::to_string(&did).unwrap();
-
        assert_eq!(format!("\"{}\"", did), json);
-
    }
-
}
deleted radicle-node/src/identity/doc.rs
@@ -1,592 +0,0 @@
-
use std::collections::{BTreeMap, HashMap};
-
use std::fmt::Write as _;
-
use std::io;
-
use std::marker::PhantomData;
-
use std::ops::Deref;
-
use std::path::Path;
-

-
use nonempty::NonEmpty;
-
use once_cell::sync::Lazy;
-
use radicle_git_ext::Oid;
-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
use crate::crypto;
-
use crate::crypto::{Signature, Unverified, Verified};
-
use crate::git;
-
use crate::identity::{Did, Id};
-
use crate::storage::git::trailers;
-
use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};
-

-
pub use crypto::PublicKey;
-

-
/// Untrusted, well-formed input.
-
#[derive(Clone, Copy, Debug)]
-
pub struct Untrusted;
-
/// Signed by quorum of the previous delegation.
-
#[derive(Clone, Copy, Debug)]
-
pub struct Trusted;
-

-
pub static PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
-

-
pub const MAX_STRING_LENGTH: usize = 255;
-
pub const MAX_DELEGATES: usize = 255;
-

-
#[derive(Error, Debug)]
-
pub enum Error {
-
    #[error("json: {0}")]
-
    Json(#[from] serde_json::Error),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("verification: {0}")]
-
    Verification(#[from] VerificationError),
-
    #[error("git: {0}")]
-
    Git(#[from] git::Error),
-
    #[error("git: {0}")]
-
    RawGit(#[from] git2::Error),
-
}
-

-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-
pub struct Delegate {
-
    pub name: String,
-
    pub id: Did,
-
}
-

-
impl Delegate {
-
    fn matches(&self, key: &PublicKey) -> bool {
-
        &self.id.0 == key
-
    }
-
}
-

-
impl From<Delegate> for PublicKey {
-
    fn from(delegate: Delegate) -> Self {
-
        delegate.id.0
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
#[serde(rename_all = "kebab-case")]
-
pub struct Payload {
-
    pub name: String,
-
    pub description: String,    // TODO: Make optional.
-
    pub default_branch: String, // TODO: Make optional.
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
#[serde(transparent)]
-
// TODO: Restrict values.
-
pub struct Namespace(String);
-

-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Doc<V> {
-
    #[serde(rename = "xyz.radicle.project")]
-
    pub payload: Payload,
-
    #[serde(flatten)]
-
    pub extensions: BTreeMap<Namespace, serde_json::Value>,
-
    pub delegates: NonEmpty<Delegate>,
-
    pub threshold: usize,
-

-
    verified: PhantomData<V>,
-
}
-

-
impl Doc<Verified> {
-
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
-
        let mut buf = Vec::new();
-
        let mut serializer =
-
            serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
-

-
        self.serialize(&mut serializer)?;
-
        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
-

-
        Ok((oid.into(), buf))
-
    }
-

-
    /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
-
    pub fn delegate(&mut self, delegate: Delegate) -> bool {
-
        if self.delegates.iter().all(|d| d.id != delegate.id) {
-
            self.delegates.push(delegate);
-
            return true;
-
        }
-
        false
-
    }
-

-
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
-
        let (oid, bytes) = self.encode()?;
-
        let sig = signer.sign(&bytes);
-

-
        Ok((oid, sig))
-
    }
-

-
    pub fn create<'r, S: WriteStorage<'r>>(
-
        &self,
-
        remote: &RemoteId,
-
        msg: &str,
-
        storage: &'r S,
-
    ) -> Result<(Id, git::Oid, S::Repository), Error> {
-
        // You can checkout this branch in your working copy with:
-
        //
-
        //      git fetch rad
-
        //      git checkout -b radicle/id remotes/rad/radicle/id
-
        //
-
        let (doc_oid, doc) = self.encode()?;
-
        let id = Id::from(doc_oid);
-
        let repo = storage.repository(&id).unwrap();
-
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
-
        let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?;
-

-
        drop(tree);
-

-
        Ok((id, oid, repo))
-
    }
-

-
    pub fn update<'r, R: WriteRepository<'r>>(
-
        &self,
-
        remote: &RemoteId,
-
        msg: &str,
-
        signatures: &[(&PublicKey, Signature)],
-
        repo: &R,
-
    ) -> Result<git::Oid, Error> {
-
        let mut msg = format!("{msg}\n\n");
-
        for (key, sig) in signatures {
-
            writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
-
                .expect("in-memory writes don't fail");
-
        }
-

-
        let (_, doc) = self.encode()?;
-
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
-
        let id_ref = git::refs::storage::id(remote);
-
        let head = repo.raw().find_reference(&id_ref)?.peel_to_commit()?;
-
        let oid = Doc::commit(remote, &tree, &msg, &[&head], repo.raw())?;
-

-
        Ok(oid)
-
    }
-

-
    fn commit(
-
        remote: &RemoteId,
-
        tree: &git2::Tree,
-
        msg: &str,
-
        parents: &[&git2::Commit],
-
        repo: &git2::Repository,
-
    ) -> Result<git::Oid, Error> {
-
        let sig = repo
-
            .signature()
-
            .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
-

-
        let id_ref = git::refs::storage::id(remote);
-
        let oid = repo.commit(Some(&id_ref), &sig, &sig, msg, tree, parents)?;
-

-
        Ok(oid.into())
-
    }
-
}
-

-
impl<V> Deref for Doc<V> {
-
    type Target = Payload;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.payload
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum VerificationError {
-
    #[error("invalid name: {0}")]
-
    Name(&'static str),
-
    #[error("invalid description: {0}")]
-
    Description(&'static str),
-
    #[error("invalid default branch: {0}")]
-
    DefaultBranch(&'static str),
-
    #[error("invalid delegates: {0}")]
-
    Delegates(&'static str),
-
    #[error("invalid version `{0}`")]
-
    Version(u32),
-
    #[error("invalid parent: {0}")]
-
    Parent(&'static str),
-
    #[error("invalid threshold `{0}`: {1}")]
-
    Threshold(usize, &'static str),
-
}
-

-
impl Doc<Unverified> {
-
    pub fn initial(
-
        name: String,
-
        description: String,
-
        default_branch: BranchName,
-
        delegate: Delegate,
-
    ) -> Self {
-
        Self {
-
            payload: Payload {
-
                name,
-
                description,
-
                default_branch,
-
            },
-
            extensions: BTreeMap::new(),
-
            delegates: NonEmpty::new(delegate),
-
            threshold: 1,
-
            verified: PhantomData,
-
        }
-
    }
-

-
    pub fn new(
-
        name: String,
-
        description: String,
-
        default_branch: BranchName,
-
        delegates: NonEmpty<Delegate>,
-
        threshold: usize,
-
    ) -> Self {
-
        Self {
-
            payload: Payload {
-
                name,
-
                description,
-
                default_branch,
-
            },
-
            extensions: BTreeMap::new(),
-
            delegates,
-
            threshold,
-
            verified: PhantomData,
-
        }
-
    }
-

-
    pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
-
        serde_json::from_slice(bytes)
-
    }
-

-
    pub fn verified(self) -> Result<Doc<Verified>, VerificationError> {
-
        if self.name.is_empty() {
-
            return Err(VerificationError::Name("name cannot be empty"));
-
        }
-
        if self.name.len() > MAX_STRING_LENGTH {
-
            return Err(VerificationError::Name("name cannot exceed 255 bytes"));
-
        }
-
        if self.description.len() > MAX_STRING_LENGTH {
-
            return Err(VerificationError::Description(
-
                "description cannot exceed 255 bytes",
-
            ));
-
        }
-
        if self.delegates.len() > MAX_DELEGATES {
-
            return Err(VerificationError::Delegates(
-
                "number of delegates cannot exceed 255",
-
            ));
-
        }
-
        if self
-
            .delegates
-
            .iter()
-
            .any(|d| d.name.is_empty() || d.name.len() > MAX_STRING_LENGTH)
-
        {
-
            return Err(VerificationError::Delegates(
-
                "delegate name must not be empty and must not exceed 255 bytes",
-
            ));
-
        }
-
        if self.delegates.is_empty() {
-
            return Err(VerificationError::Delegates(
-
                "delegate list cannot be empty",
-
            ));
-
        }
-
        if self.default_branch.is_empty() {
-
            return Err(VerificationError::DefaultBranch(
-
                "default branch cannot be empty",
-
            ));
-
        }
-
        if self.default_branch.len() > MAX_STRING_LENGTH {
-
            return Err(VerificationError::DefaultBranch(
-
                "default branch cannot exceed 255 bytes",
-
            ));
-
        }
-
        if self.threshold > self.delegates.len() {
-
            return Err(VerificationError::Threshold(
-
                self.threshold,
-
                "threshold cannot exceed number of delegates",
-
            ));
-
        }
-
        if self.threshold == 0 {
-
            return Err(VerificationError::Threshold(
-
                self.threshold,
-
                "threshold cannot be zero",
-
            ));
-
        }
-

-
        Ok(Doc {
-
            payload: self.payload,
-
            extensions: self.extensions,
-
            delegates: self.delegates,
-
            threshold: self.threshold,
-
            verified: PhantomData,
-
        })
-
    }
-

-
    pub fn blob_at<'r, R: ReadRepository<'r>>(
-
        commit: Oid,
-
        repo: &R,
-
    ) -> Result<Option<git2::Blob>, git::Error> {
-
        match repo.blob_at(commit, Path::new(&*PATH)) {
-
            Err(git::ext::Error::NotFound(_)) => Ok(None),
-
            Err(e) => Err(e),
-
            Ok(blob) => Ok(Some(blob)),
-
        }
-
    }
-

-
    pub fn load_at<'r, R: ReadRepository<'r>>(
-
        commit: Oid,
-
        repo: &R,
-
    ) -> Result<Option<(Self, Oid)>, git::Error> {
-
        if let Some(blob) = Self::blob_at(commit, repo)? {
-
            let doc = Doc::from_json(blob.content()).unwrap();
-
            return Ok(Some((doc, blob.id().into())));
-
        }
-
        Ok(None)
-
    }
-

-
    pub fn load<'r, R: ReadRepository<'r>>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<(Self, Oid)>, git::Error> {
-
        if let Some(oid) = Self::head(remote, repo)? {
-
            Self::load_at(oid, repo)
-
        } else {
-
            Ok(None)
-
        }
-
    }
-
}
-

-
impl<V> Doc<V> {
-
    pub fn head<'r, R: ReadRepository<'r>>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<Oid>, git::Error> {
-
        let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
-
        if let Some(oid) = repo.reference_oid(remote, head)? {
-
            Ok(Some(oid))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum IdentityError {
-
    #[error("git: {0}")]
-
    GitRaw(#[from] git2::Error),
-
    #[error("git: {0}")]
-
    Git(#[from] git::Error),
-
    #[error("verification: {0}")]
-
    Verification(#[from] VerificationError),
-
    #[error("root hash `{0}` does not match project")]
-
    MismatchedRoot(Oid),
-
    #[error("commit signature for {0} is invalid: {1}")]
-
    InvalidSignature(PublicKey, crypto::Error),
-
    #[error("quorum not reached: {0} signatures for a threshold of {1}")]
-
    QuorumNotReached(usize, usize),
-
}
-

-
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct Identity<I> {
-
    /// The head of the identity branch. This points to a commit that
-
    /// contains the current document blob.
-
    pub head: Oid,
-
    /// The canonical identifier for this identity.
-
    /// This is the object id of the initial document blob.
-
    pub root: I,
-
    /// The object id of the current document blob.
-
    pub current: Oid,
-
    /// Revision number. The initial document has a revision of `0`.
-
    pub revision: u32,
-
    /// The current document.
-
    pub doc: Doc<Verified>,
-
    /// Signatures over this identity.
-
    pub signatures: HashMap<PublicKey, Signature>,
-
}
-

-
impl Identity<Oid> {
-
    pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
-
        // The root hash must be equal to the id.
-
        if self.root != *id {
-
            return Err(IdentityError::MismatchedRoot(self.root));
-
        }
-

-
        Ok(Identity {
-
            root: id,
-
            head: self.head,
-
            current: self.current,
-
            revision: self.revision,
-
            doc: self.doc,
-
            signatures: self.signatures,
-
        })
-
    }
-
}
-

-
impl Identity<Untrusted> {
-
    pub fn load<'r, R: ReadRepository<'r>>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<Identity<Oid>>, IdentityError> {
-
        if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
-
            let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
-

-
            // Retrieve root document.
-
            let root_oid = history.pop().unwrap()?.into();
-
            let root_blob = Doc::blob_at(root_oid, repo)?.unwrap();
-
            let root: git::Oid = root_blob.id().into();
-
            let trusted = Doc::from_json(root_blob.content()).unwrap();
-
            let revision = history.len() as u32;
-

-
            let mut trusted = trusted.verified()?;
-
            let mut current = root;
-
            let mut signatures = Vec::new();
-

-
            // Traverse the history chronologically.
-
            for oid in history.into_iter().rev() {
-
                let oid = oid?;
-
                let blob = Doc::blob_at(oid.into(), repo)?.unwrap();
-
                let untrusted = Doc::from_json(blob.content()).unwrap();
-
                let untrusted = untrusted.verified()?;
-
                let commit = repo.commit(oid.into())?.unwrap();
-
                let msg = commit.message_raw().unwrap();
-

-
                // Keys that signed the *current* document version.
-
                signatures = trailers::parse_signatures(msg).unwrap();
-
                for (pk, sig) in &signatures {
-
                    if let Err(err) = pk.verify(blob.content(), sig) {
-
                        return Err(IdentityError::InvalidSignature(*pk, err));
-
                    }
-
                }
-

-
                // Check that enough delegates signed this next version.
-
                let quorum = signatures
-
                    .iter()
-
                    .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
-
                    .count();
-
                if quorum < trusted.threshold {
-
                    return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
-
                }
-

-
                trusted = untrusted;
-
                current = blob.id().into();
-
            }
-

-
            return Ok(Some(Identity {
-
                root,
-
                head,
-
                current,
-
                revision,
-
                doc: trusted,
-
                signatures: signatures.into_iter().collect(),
-
            }));
-
        }
-
        Ok(None)
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use crate::prelude::Signer;
-
    use crate::rad;
-
    use crate::storage::git::Storage;
-
    use crate::storage::{ReadStorage, WriteStorage};
-
    use crate::test::{crypto, fixtures};
-

-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    #[test]
-
    fn test_valid_identity() {
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let mut rng = fastrand::Rng::new();
-

-
        let alice = crypto::MockSigner::new(&mut rng);
-
        let bob = crypto::MockSigner::new(&mut rng);
-
        let eve = crypto::MockSigner::new(&mut rng);
-

-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (id, _, _, _) =
-
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
-

-
        // Bob and Eve fork the project from Alice.
-
        rad::fork(&id, alice.public_key(), &bob, &storage).unwrap();
-
        rad::fork(&id, alice.public_key(), &eve, &storage).unwrap();
-

-
        // TODO: In some cases we want to get the repo and the project, but don't
-
        // want to have to create a repository object twice. Perhaps there should
-
        // be a way of getting a project from a repo.
-
        let mut proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
-
        let repo = storage.repository(&id).unwrap();
-

-
        // Make a change to the description and sign it.
-
        proj.doc.payload.description += "!";
-
        proj.sign(&alice)
-
            .and_then(|(_, sig)| {
-
                proj.update(
-
                    alice.public_key(),
-
                    "Update description",
-
                    &[(alice.public_key(), sig)],
-
                    &repo,
-
                )
-
            })
-
            .unwrap();
-

-
        // Add Bob as a delegate, and sign it.
-
        proj.delegate("bob".to_owned(), *bob.public_key());
-
        proj.doc.threshold = 2;
-
        proj.sign(&alice)
-
            .and_then(|(_, sig)| {
-
                proj.update(
-
                    alice.public_key(),
-
                    "Add bob",
-
                    &[(alice.public_key(), sig)],
-
                    &repo,
-
                )
-
            })
-
            .unwrap();
-

-
        // Add Eve as a delegate, and sign it.
-
        proj.delegate("eve".to_owned(), *eve.public_key());
-
        proj.sign(&alice)
-
            .and_then(|(_, alice_sig)| {
-
                proj.sign(&bob).and_then(|(_, bob_sig)| {
-
                    proj.update(
-
                        alice.public_key(),
-
                        "Add eve",
-
                        &[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)],
-
                        &repo,
-
                    )
-
                })
-
            })
-
            .unwrap();
-

-
        // Update description again with signatures by Eve and Bob.
-
        proj.doc.payload.description += "?";
-
        let (current, head) = proj
-
            .sign(&bob)
-
            .and_then(|(_, bob_sig)| {
-
                proj.sign(&eve).and_then(|(blob_id, eve_sig)| {
-
                    proj.update(
-
                        alice.public_key(),
-
                        "Update description",
-
                        &[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)],
-
                        &repo,
-
                    )
-
                    .map(|head| (blob_id, head))
-
                })
-
            })
-
            .unwrap();
-

-
        let identity: Identity<Id> = Identity::load(alice.public_key(), &repo)
-
            .unwrap()
-
            .unwrap()
-
            .verified(id.clone())
-
            .unwrap();
-

-
        assert_eq!(identity.signatures.len(), 2);
-
        assert_eq!(identity.revision, 4);
-
        assert_eq!(identity.root, id);
-
        assert_eq!(identity.current, current);
-
        assert_eq!(identity.head, head);
-
        assert_eq!(identity.doc, proj.doc);
-

-
        let proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
-
        assert_eq!(proj.description, "Acme's repository!?");
-
    }
-

-
    #[quickcheck]
-
    fn prop_encode_decode(doc: Doc<Verified>) {
-
        let (_, bytes) = doc.encode().unwrap();
-
        assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
-
    }
-
}
modified radicle-node/src/lib.rs
@@ -5,23 +5,17 @@ pub mod address_book;
pub mod address_manager;
pub mod client;
pub mod clock;
-
pub mod collections;
pub mod control;
-
pub mod crypto;
pub mod decoder;
-
pub mod git;
-
pub mod hash;
-
pub mod identity;
pub mod logger;
-
pub mod rad;
-
pub mod serde_ext;
pub mod service;
-
pub mod storage;
#[cfg(test)]
pub mod test;
pub mod transport;
pub mod wire;

+
pub use radicle::{collections, crypto, git, hash, identity, rad, storage};
+

pub mod prelude {
    pub use crate::crypto::{PublicKey, Signature, Signer};
    pub use crate::decoder::Decoder;
deleted radicle-node/src/rad.rs
@@ -1,329 +0,0 @@
-
use std::io;
-
use std::path::Path;
-

-
use thiserror::Error;
-

-
use crate::crypto::{Signer, Verified};
-
use crate::git;
-
use crate::identity::Id;
-
use crate::storage::refs::SignedRefs;
-
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
-
use crate::{identity, storage};
-

-
pub const REMOTE_NAME: &str = "rad";
-

-
#[derive(Error, Debug)]
-
pub enum InitError {
-
    #[error("doc: {0}")]
-
    Doc(#[from] identity::doc::Error),
-
    #[error("doc: {0}")]
-
    DocVerification(#[from] identity::doc::VerificationError),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("storage: {0}")]
-
    Storage(#[from] storage::Error),
-
    #[error("cannot initialize project inside a bare repository")]
-
    BareRepo,
-
    #[error("cannot initialize project from detached head state")]
-
    DetachedHead,
-
    #[error("HEAD reference is not valid UTF-8")]
-
    InvalidHead,
-
}
-

-
/// Initialize a new radicle project from a git repository.
-
pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
-
    repo: &git2::Repository,
-
    name: &str,
-
    description: &str,
-
    default_branch: BranchName,
-
    signer: G,
-
    storage: &'r S,
-
) -> Result<(Id, SignedRefs<Verified>), InitError> {
-
    let pk = signer.public_key();
-
    let delegate = identity::Delegate {
-
        // TODO: Use actual user name.
-
        name: String::from("anonymous"),
-
        id: identity::Did::from(*pk),
-
    };
-
    let doc = identity::Doc::initial(
-
        name.to_owned(),
-
        description.to_owned(),
-
        default_branch.clone(),
-
        delegate,
-
    )
-
    .verified()?;
-

-
    let (id, _, project) = doc.create(pk, "Initialize Radicle", storage)?;
-

-
    git::set_upstream(
-
        repo,
-
        REMOTE_NAME,
-
        &default_branch,
-
        &git::refs::storage::branch(pk, &default_branch),
-
    )?;
-

-
    // TODO: Note that you'll likely want to use `RemoteCallbacks` and set
-
    // `push_update_reference` to test whether all the references were pushed
-
    // successfully.
-
    git::configure_remote(repo, REMOTE_NAME, pk, project.path())?.push::<&str>(
-
        &[&format!(
-
            "{}:{}",
-
            &git::refs::workdir::branch(&default_branch),
-
            &git::refs::storage::branch(pk, &default_branch),
-
        )],
-
        None,
-
    )?;
-
    let signed = storage.sign_refs(&project, signer)?;
-

-
    Ok((id, signed))
-
}
-

-
#[derive(Error, Debug)]
-
pub enum ForkError {
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("storage: {0}")]
-
    Storage(#[from] storage::Error),
-
    #[error("project `{0}` was not found in storage")]
-
    NotFound(Id),
-
    #[error("git: invalid reference")]
-
    InvalidReference,
-
}
-

-
/// Create a local tree for an existing project, from an existing remote.
-
pub fn fork<'r, G: Signer, S: storage::WriteStorage<'r>>(
-
    proj: &Id,
-
    remote: &RemoteId,
-
    signer: G,
-
    storage: S,
-
) -> Result<(), ForkError> {
-
    // TODO: Copy tags over?
-

-
    // Creates or copies the following references:
-
    //
-
    // refs/remotes/<pk>/heads/master
-
    // refs/remotes/<pk>/heads/radicle/id
-
    // refs/remotes/<pk>/tags/*
-
    // refs/remotes/<pk>/rad/signature
-

-
    let me = signer.public_key();
-
    let project = storage
-
        .get(remote, proj)?
-
        .ok_or_else(|| ForkError::NotFound(proj.clone()))?;
-
    let repository = storage.repository(proj)?;
-

-
    let raw = repository.raw();
-
    let remote_head = raw
-
        .find_reference(&git::refs::storage::branch(
-
            remote,
-
            &project.doc.default_branch,
-
        ))?
-
        .target()
-
        .ok_or(ForkError::InvalidReference)?;
-
    raw.reference(
-
        &git::refs::storage::branch(me, &project.doc.default_branch),
-
        remote_head,
-
        false,
-
        &format!("creating default branch for {me}"),
-
    )?;
-

-
    let remote_id = raw
-
        .find_reference(&git::refs::storage::id(remote))?
-
        .target()
-
        .ok_or(ForkError::InvalidReference)?;
-
    raw.reference(
-
        &git::refs::storage::id(me),
-
        remote_id,
-
        false,
-
        &format!("creating identity branch for {me}"),
-
    )?;
-

-
    storage.sign_refs(&repository, &signer)?;
-

-
    Ok(())
-
}
-

-
#[derive(Error, Debug)]
-
pub enum CheckoutError {
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("storage: {0}")]
-
    Storage(#[from] storage::Error),
-
    #[error("project `{0}` was not found in storage")]
-
    NotFound(Id),
-
}
-

-
/// Checkout a project from storage as a working copy.
-
/// This effectively does a `git-clone` from storage.
-
pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
-
    proj: &Id,
-
    remote: &RemoteId,
-
    path: P,
-
    storage: S,
-
) -> Result<git2::Repository, CheckoutError> {
-
    // TODO: Decide on whether we can use `clone_local`
-
    // TODO: Look into sharing object databases.
-
    let project = storage
-
        .get(remote, proj)?
-
        .ok_or_else(|| CheckoutError::NotFound(proj.clone()))?;
-

-
    let mut opts = git2::RepositoryInitOptions::new();
-
    opts.no_reinit(true).description(&project.doc.description);
-

-
    let repo = git2::Repository::init_opts(path, &opts)?;
-
    let default_branch = project.doc.default_branch.as_str();
-

-
    // Configure and fetch all refs from remote.
-
    git::configure_remote(&repo, REMOTE_NAME, remote, &project.path)?.fetch::<&str>(
-
        &[],
-
        None,
-
        None,
-
    )?;
-

-
    {
-
        // Setup default branch.
-
        let remote_head_ref = git::refs::workdir::remote_branch(REMOTE_NAME, default_branch);
-
        let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
-
        let _ = repo.branch(default_branch, &remote_head_commit, true)?;
-

-
        // Setup remote tracking for default branch.
-
        git::set_upstream(
-
            &repo,
-
            REMOTE_NAME,
-
            default_branch,
-
            &git::refs::storage::branch(remote, default_branch),
-
        )?;
-
    }
-

-
    Ok(repo)
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use crate::git::fmt::refname;
-
    use crate::identity::{Delegate, Did};
-
    use crate::storage::git::Storage;
-
    use crate::storage::{ReadStorage, WriteStorage};
-
    use crate::test::{crypto, fixtures};
-

-
    #[test]
-
    fn test_init() {
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let signer = crypto::MockSigner::default();
-
        let public_key = *signer.public_key();
-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
-

-
        let (proj, refs) = init(
-
            &repo,
-
            "acme",
-
            "Acme's repo",
-
            BranchName::from("master"),
-
            &signer,
-
            &storage,
-
        )
-
        .unwrap();
-

-
        let project = storage.get(&public_key, &proj).unwrap().unwrap();
-

-
        assert_eq!(project.remotes[&public_key].refs, refs);
-
        assert_eq!(project.id, proj);
-
        assert_eq!(project.doc.name, "acme");
-
        assert_eq!(project.doc.description, "Acme's repo");
-
        assert_eq!(project.doc.default_branch, BranchName::from("master"));
-
        assert_eq!(
-
            project.doc.delegates.first(),
-
            &Delegate {
-
                name: String::from("anonymous"),
-
                id: Did::from(public_key),
-
            }
-
        );
-
    }
-

-
    #[test]
-
    fn test_fork() {
-
        let mut rng = fastrand::Rng::new();
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let alice = crypto::MockSigner::new(&mut rng);
-
        let alice_id = alice.public_key();
-
        let bob = crypto::MockSigner::new(&mut rng);
-
        let bob_id = bob.public_key();
-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
-

-
        // Alice creates a project.
-
        let (id, alice_refs) = init(
-
            &original,
-
            "acme",
-
            "Acme's repo",
-
            BranchName::from("master"),
-
            &alice,
-
            &storage,
-
        )
-
        .unwrap();
-

-
        // Bob forks it and creates a checkout.
-
        fork(&id, alice_id, &bob, &storage).unwrap();
-
        checkout(&id, bob_id, tempdir.path().join("copy"), &storage).unwrap();
-

-
        let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap();
-

-
        assert_eq!(
-
            bob_remote.refs.get(&refname!("master")),
-
            alice_refs.get(&refname!("master"))
-
        );
-
    }
-

-
    #[test]
-
    fn test_checkout() {
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let signer = crypto::MockSigner::default();
-
        let remote_id = signer.public_key();
-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
-

-
        let (id, _) = init(
-
            &original,
-
            "acme",
-
            "Acme's repo",
-
            BranchName::from("master"),
-
            &signer,
-
            &storage,
-
        )
-
        .unwrap();
-

-
        let copy = checkout(&id, remote_id, tempdir.path().join("copy"), &storage).unwrap();
-

-
        assert_eq!(
-
            copy.head().unwrap().target(),
-
            original.head().unwrap().target()
-
        );
-
        assert_eq!(
-
            copy.branch_upstream_name("refs/heads/master")
-
                .unwrap()
-
                .to_vec(),
-
            original
-
                .branch_upstream_name("refs/heads/master")
-
                .unwrap()
-
                .to_vec()
-
        );
-
        assert_eq!(
-
            copy.find_remote(REMOTE_NAME)
-
                .unwrap()
-
                .refspecs()
-
                .into_iter()
-
                .map(|r| r.bytes().to_vec())
-
                .collect::<Vec<_>>(),
-
            original
-
                .find_remote(REMOTE_NAME)
-
                .unwrap()
-
                .refspecs()
-
                .into_iter()
-
                .map(|r| r.bytes().to_vec())
-
                .collect::<Vec<_>>(),
-
        );
-
    }
-
}
deleted radicle-node/src/serde_ext.rs
@@ -1,25 +0,0 @@
-
pub mod string {
-
    use std::fmt::Display;
-
    use std::str::FromStr;
-

-
    use serde::{de, Deserialize, Deserializer, Serializer};
-

-
    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        T: Display,
-
        S: Serializer,
-
    {
-
        serializer.collect_str(value)
-
    }
-

-
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
-
    where
-
        T: FromStr,
-
        T::Err: Display,
-
        D: Deserializer<'de>,
-
    {
-
        String::deserialize(deserializer)?
-
            .parse()
-
            .map_err(de::Error::custom)
-
    }
-
}
modified radicle-node/src/service.rs
@@ -9,7 +9,6 @@ use std::{collections::VecDeque, fmt, net, net::IpAddr};

use crossbeam_channel as chan;
use fastrand::Rng;
-
use git_url::Url;
use log::*;
use nakamoto::{LocalDuration, LocalTime};
use nakamoto_net as nakamoto;
@@ -22,6 +21,8 @@ use crate::address_manager::AddressManager;
use crate::clock::RefClock;
use crate::collections::{HashMap, HashSet};
use crate::crypto;
+
use crate::git;
+
use crate::git::Url;
use crate::identity::{Id, Project};
use crate::service::config::ProjectTracking;
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
@@ -81,7 +82,7 @@ pub enum Event {
#[derive(thiserror::Error, Debug)]
pub enum FetchError {
    #[error(transparent)]
-
    Git(#[from] git2::Error),
+
    Git(#[from] git::raw::Error),
    #[error(transparent)]
    Storage(#[from] storage::Error),
    #[error(transparent)]
@@ -389,7 +390,7 @@ impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Service
                // TODO: Limit the number of seeds we fetch from? Randomize?
                for (_, peer) in seeds {
                    match repo.fetch(&Url {
-
                        scheme: git_url::Scheme::Git,
+
                        scheme: git::url::Scheme::Git,
                        host: Some(peer.addr.ip().to_string()),
                        port: Some(peer.addr.port()),
                        // TODO: Fix upstream crate so that it adds a `/` when needed.
modified radicle-node/src/service/config.rs
@@ -1,9 +1,8 @@
use std::net;

-
use git_url::Url;
-

use crate::collections::HashSet;
use crate::git;
+
use crate::git::Url;
use crate::identity::{Id, PublicKey};
use crate::service::message::{Address, Envelope, Message};

modified radicle-node/src/service/message.rs
@@ -288,7 +288,7 @@ mod tests {
    use quickcheck_macros::quickcheck;

    use crate::crypto::Signer;
-
    use crate::test::crypto::MockSigner;
+
    use crate::test::signer::MockSigner;

    #[quickcheck]
    fn prop_refs_announcement_signing(id: Id, refs: Refs) {
deleted radicle-node/src/storage.rs
@@ -1,306 +0,0 @@
-
pub mod git;
-
pub mod refs;
-

-
use std::collections::hash_map;
-
use std::marker::PhantomData;
-
use std::ops::Deref;
-
use std::path::Path;
-
use std::{fmt, io};
-

-
use thiserror::Error;
-

-
pub use radicle_git_ext::Oid;
-

-
use crate::collections::HashMap;
-
use crate::crypto;
-
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
-
use crate::git::ext as git_ext;
-
use crate::git::Url;
-
use crate::git::{RefError, RefStr, RefString};
-
use crate::identity;
-
use crate::identity::{Id, IdError, Project};
-
use crate::storage::refs::Refs;
-

-
use self::refs::SignedRefs;
-

-
pub type BranchName = String;
-
pub type Inventory = Vec<Id>;
-

-
/// Storage error.
-
#[derive(Error, Debug)]
-
pub enum Error {
-
    #[error("invalid git reference")]
-
    InvalidRef,
-
    #[error("git reference error: {0}")]
-
    Ref(#[from] RefError),
-
    #[error(transparent)]
-
    Refs(#[from] refs::Error),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("id: {0}")]
-
    Id(#[from] IdError),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("doc: {0}")]
-
    Doc(#[from] identity::doc::Error),
-
    #[error("invalid repository head")]
-
    InvalidHead,
-
}
-

-
/// Fetch error.
-
#[derive(Error, Debug)]
-
#[allow(clippy::large_enum_variant)]
-
pub enum FetchError {
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("verify: {0}")]
-
    Verify(#[from] git::VerifyError),
-
}
-

-
pub type RemoteId = PublicKey;
-

-
/// An update to a reference.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum RefUpdate {
-
    Updated { name: RefString, old: Oid, new: Oid },
-
    Created { name: RefString, oid: Oid },
-
    Deleted { name: RefString, oid: Oid },
-
    Skipped { name: RefString, oid: Oid },
-
}
-

-
impl RefUpdate {
-
    pub fn from(name: RefString, old: impl Into<Oid>, new: impl Into<Oid>) -> Self {
-
        let old = old.into();
-
        let new = new.into();
-

-
        if old.is_zero() {
-
            Self::Created { name, oid: new }
-
        } else if new.is_zero() {
-
            Self::Deleted { name, oid: old }
-
        } else if old != new {
-
            Self::Updated { name, old, new }
-
        } else {
-
            Self::Skipped { name, oid: old }
-
        }
-
    }
-
}
-

-
impl fmt::Display for RefUpdate {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::Updated { name, old, new } => {
-
                write!(f, "~ {:.7}..{:.7} {}", old, new, name)
-
            }
-
            Self::Created { name, oid } => {
-
                write!(f, "* 0000000..{:.7} {}", oid, name)
-
            }
-
            Self::Deleted { name, oid } => {
-
                write!(f, "- {:.7}..0000000 {}", oid, name)
-
            }
-
            Self::Skipped { name, oid } => {
-
                write!(f, "= {:.7}..{:.7} {}", oid, oid, name)
-
            }
-
        }
-
    }
-
}
-

-
/// Project remotes. Tracks the git state of a project.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
-

-
impl<V> FromIterator<(RemoteId, Remote<V>)> for Remotes<V> {
-
    fn from_iter<T: IntoIterator<Item = (RemoteId, Remote<V>)>>(iter: T) -> Self {
-
        Self(iter.into_iter().collect())
-
    }
-
}
-

-
impl<V> Deref for Remotes<V> {
-
    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)
-
    }
-
}
-

-
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 {
-
        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()
-
    }
-
}
-

-
impl<V> From<Remotes<V>> for HashMap<RemoteId, Refs> {
-
    fn from(other: Remotes<V>) -> Self {
-
        let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into());
-

-
        for (k, v) in other.into_iter() {
-
            remotes.insert(k, v.refs.into());
-
        }
-
        remotes
-
    }
-
}
-

-
/// A project remote.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Remote<V> {
-
    /// ID of remote.
-
    pub id: PublicKey,
-
    /// Git references published under this remote, and their hashes.
-
    pub refs: SignedRefs<V>,
-
    /// Whether this remote is of a project delegate.
-
    pub delegate: bool,
-
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
-
    verified: PhantomData<V>,
-
}
-

-
impl<V> Remote<V> {
-
    pub fn new(id: PublicKey, refs: impl Into<SignedRefs<V>>) -> Self {
-
        Self {
-
            id,
-
            refs: refs.into(),
-
            delegate: false,
-
            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 {
-
    fn url(&self) -> Url;
-
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error>;
-
    fn inventory(&self) -> Result<Inventory, Error>;
-
}
-

-
pub trait WriteStorage<'r>: ReadStorage {
-
    type Repository: WriteRepository<'r>;
-

-
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error>;
-
    fn sign_refs<G: Signer>(
-
        &self,
-
        repository: &Self::Repository,
-
        signer: G,
-
    ) -> Result<SignedRefs<Verified>, Error>;
-
}
-

-
pub trait ReadRepository<'r> {
-
    type Remotes: Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r;
-

-
    fn is_empty(&self) -> Result<bool, git2::Error>;
-
    fn path(&self) -> &Path;
-
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
-
    fn reference(
-
        &self,
-
        remote: &RemoteId,
-
        reference: &RefStr,
-
    ) -> Result<Option<git2::Reference>, git2::Error>;
-
    fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error>;
-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
-
    fn reference_oid(
-
        &self,
-
        remote: &RemoteId,
-
        reference: &RefStr,
-
    ) -> Result<Option<Oid>, git2::Error>;
-
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
-
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
-
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error>;
-
    /// Return the project associated with this repository.
-
    fn project(&self) -> Result<Project, Error>;
-
}
-

-
pub trait WriteRepository<'r>: ReadRepository<'r> {
-
    fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
-
    fn raw(&self) -> &git2::Repository;
-
}
-

-
impl<T, S> ReadStorage for T
-
where
-
    T: Deref<Target = S>,
-
    S: ReadStorage + 'static,
-
{
-
    fn url(&self) -> Url {
-
        self.deref().url()
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        self.deref().inventory()
-
    }
-

-
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
-
        self.deref().get(remote, proj)
-
    }
-
}
-

-
impl<'r, T, S> WriteStorage<'r> for T
-
where
-
    T: Deref<Target = S>,
-
    S: WriteStorage<'r> + 'static,
-
{
-
    type Repository = S::Repository;
-

-
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
-
        self.deref().repository(proj)
-
    }
-

-
    fn sign_refs<G: Signer>(
-
        &self,
-
        repository: &S::Repository,
-
        signer: G,
-
    ) -> Result<SignedRefs<Verified>, Error> {
-
        self.deref().sign_refs(repository, signer)
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    #[test]
-
    fn test_storage() {}
-
}
deleted radicle-node/src/storage/git.rs
@@ -1,759 +0,0 @@
-
use std::collections::{BTreeMap, HashMap};
-
use std::path::{Path, PathBuf};
-
use std::{fmt, fs, io};
-

-
use git_ref_format::refspec;
-
use once_cell::sync::Lazy;
-

-
pub use radicle_git_ext::Oid;
-

-
use crate::crypto::{Signer, Unverified, Verified};
-
use crate::git;
-
use crate::identity::{self, Doc};
-
use crate::identity::{Id, Project};
-
use crate::storage::refs;
-
use crate::storage::refs::{Refs, SignedRefs};
-
use crate::storage::{
-
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, WriteRepository,
-
    WriteStorage,
-
};
-

-
use super::{RefUpdate, RemoteId};
-

-
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/remotes/*"));
-
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
-

-
#[derive(Error, Debug)]
-
pub enum IdentityError {
-
    #[error("identity branches diverge from each other")]
-
    BranchesDiverge,
-
    #[error("identity branches are in an invalid state")]
-
    InvalidState,
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("git: {0}")]
-
    GitExt(#[from] git::Error),
-
    #[error("refs: {0}")]
-
    Refs(#[from] refs::Error),
-
}
-

-
pub struct Storage {
-
    path: PathBuf,
-
}
-

-
impl fmt::Debug for Storage {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Storage(..)")
-
    }
-
}
-

-
impl ReadStorage for Storage {
-
    fn url(&self) -> git::Url {
-
        git::Url {
-
            scheme: git_url::Scheme::File,
-
            host: None,
-
            path: self.path.to_string_lossy().to_string().into(),
-
            ..git::Url::default()
-
        }
-
    }
-

-
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
-
        // TODO: Don't create a repo here if it doesn't exist?
-
        // Perhaps for checking we could have a `contains` method?
-
        let repo = self.repository(proj)?;
-

-
        if let Some(doc) = repo.identity_of(remote)? {
-
            let remotes = repo.remotes()?.collect::<Result<_, _>>()?;
-
            let path = repo.path().to_path_buf();
-

-
            // TODO: We should check that there is at least one remote, which is
-
            // the one of the local user, otherwise it means the project is in
-
            // an corrupted state.
-

-
            Ok(Some(Project {
-
                id: proj.clone(),
-
                doc,
-
                remotes,
-
                path,
-
            }))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        self.projects()
-
    }
-
}
-

-
impl<'r> WriteStorage<'r> for Storage {
-
    type Repository = Repository;
-

-
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
-
        Repository::open(self.path.join(proj.to_string()))
-
    }
-

-
    fn sign_refs<G: Signer>(
-
        &self,
-
        repository: &Repository,
-
        signer: G,
-
    ) -> Result<SignedRefs<Verified>, Error> {
-
        let remote = signer.public_key();
-
        let refs = repository.references(remote)?;
-
        let signed = refs.signed(&signer)?;
-

-
        signed.save(remote, repository)?;
-

-
        Ok(signed)
-
    }
-
}
-

-
impl Storage {
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
-
        let path = path.as_ref().to_path_buf();
-

-
        match fs::create_dir_all(&path) {
-
            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
-
            Err(err) => return Err(err),
-
            Ok(()) => {}
-
        }
-

-
        Ok(Self { path })
-
    }
-

-
    pub fn path(&self) -> &Path {
-
        self.path.as_path()
-
    }
-

-
    pub fn projects(&self) -> Result<Vec<Id>, Error> {
-
        let mut projects = Vec::new();
-

-
        for result in fs::read_dir(&self.path)? {
-
            let path = result?;
-
            let id = Id::try_from(path.file_name())?;
-

-
            projects.push(id);
-
        }
-
        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(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.
-
}
-

-
#[derive(Debug, Error)]
-
pub enum VerifyError {
-
    #[error("invalid remote `{0}`")]
-
    InvalidRemote(RemoteId),
-
    #[error("invalid target `{2}` for reference `{1}` of remote `{0}`")]
-
    InvalidRefTarget(RemoteId, git::RefString, git2::Oid),
-
    #[error("invalid reference")]
-
    InvalidRef,
-
    #[error("ref error: {0}")]
-
    Ref(#[from] git::RefError),
-
    #[error("refs error: {0}")]
-
    Refs(#[from] refs::Error),
-
    #[error("unknown reference `{1}` in remote `{0}`")]
-
    UnknownRef(RemoteId, git::RefString),
-
    #[error("missing reference `{1}` in remote `{0}`")]
-
    MissingRef(RemoteId, git::RefString),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
}
-

-
impl Repository {
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-
        let backend = match git2::Repository::open_bare(path.as_ref()) {
-
            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),
-
                )?;
-
                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(repo) => Ok(repo),
-
            Err(e) => Err(e),
-
        }?;
-

-
        Ok(Self { backend })
-
    }
-

-
    pub fn head(&self) -> Result<git2::Commit, git2::Error> {
-
        // TODO: Find longest history, get document and get head.
-
        // Perhaps we should even set a local `HEAD` or at least `refs/heads/master`
-
        todo!();
-
    }
-

-
    pub fn verify(&self) -> Result<(), VerifyError> {
-
        let mut remotes: HashMap<RemoteId, Refs> = self
-
            .remotes()?
-
            .map(|remote| {
-
                let (id, remote) = remote?;
-
                Ok((id, remote.refs.into()))
-
            })
-
            .collect::<Result<_, VerifyError>>()?;
-

-
        for r in self.backend.references()? {
-
            let r = r?;
-
            let name = r.name().ok_or(VerifyError::InvalidRef)?;
-
            let oid = r.target().ok_or(VerifyError::InvalidRef)?;
-
            let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
-

-
            if refname == *refs::SIGNATURE_REF {
-
                continue;
-
            }
-
            let remote = remotes
-
                .get_mut(&remote_id)
-
                .ok_or(VerifyError::InvalidRemote(remote_id))?;
-
            let signed_oid = remote
-
                .remove(&refname)
-
                .ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
-

-
            if git::Oid::from(oid) != signed_oid {
-
                return Err(VerifyError::InvalidRefTarget(remote_id, refname, oid));
-
            }
-
        }
-

-
        // The refs that are left in the map, are ones that were signed, but are not
-
        // in the repository.
-
        for (id, refs) in remotes.into_iter() {
-
            if let Some((name, _)) = refs.into_iter().next() {
-
                return Err(VerifyError::MissingRef(id, name));
-
            }
-
        }
-

-
        Ok(())
-
    }
-

-
    pub fn inspect(&self) -> Result<(), Error> {
-
        for r in self.backend.references()? {
-
            let r = r?;
-
            let name = r.name().ok_or(Error::InvalidRef)?;
-
            let oid = r.target().ok_or(Error::InvalidRef)?;
-

-
            println!("{} {}", oid, name);
-
        }
-
        Ok(())
-
    }
-

-
    pub fn identity_of(
-
        &self,
-
        remote: &RemoteId,
-
    ) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
-
        if let Some((doc, _)) = identity::Doc::load(remote, self)? {
-
            Ok(Some(doc.verified().unwrap()))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    /// Return the canonical identity [`git::Oid`] and document.
-
    pub fn identity(&self) -> Result<(git::Oid, identity::Doc<Unverified>), IdentityError> {
-
        let mut heads = Vec::new();
-
        for remote in self.remote_ids()? {
-
            let remote = remote?;
-
            let oid = Doc::<Unverified>::head(&remote, self)?.unwrap();
-

-
            heads.push(oid.into());
-
        }
-
        // Keep track of the longest identity branch.
-
        let mut longest = heads.pop().ok_or(IdentityError::InvalidState)?;
-

-
        for head in &heads {
-
            let base = self.raw().merge_base(*head, longest)?;
-

-
            if base == longest {
-
                // `head` is a successor of `longest`. Update `longest`.
-
                //
-
                //   o head
-
                //   |
-
                //   o longest (base)
-
                //   |
-
                //
-
                longest = *head;
-
            } else if base == *head || *head == longest {
-
                // `head` is an ancestor of `longest`, or equal to it. Do nothing.
-
                //
-
                //   o longest             o longest, head (base)
-
                //   |                     |
-
                //   o head (base)   OR    o
-
                //   |                     |
-
                //
-
            } else {
-
                // The merge base between `head` and `longest` (`base`)
-
                // is neither `head` nor `longest`. Therefore, the branches have
-
                // diverged.
-
                //
-
                //    longest   head
-
                //           \ /
-
                //            o (base)
-
                //            |
-
                //
-
                return Err(IdentityError::BranchesDiverge);
-
            }
-
        }
-

-
        Doc::load_at(longest.into(), self)?
-
            .ok_or(refs::Error::NotFound)
-
            .map(|(doc, _)| (longest.into(), doc))
-
            .map_err(IdentityError::from)
-
    }
-

-
    pub fn remote_ids(
-
        &self,
-
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git2::Error> {
-
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
-
            |reference| -> Result<RemoteId, refs::Error> {
-
                let r = reference?;
-
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
-
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
-

-
                Ok(id)
-
            },
-
        );
-
        Ok(iter)
-
    }
-
}
-

-
impl<'r> ReadRepository<'r> for Repository {
-
    type Remotes = Box<dyn Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r>;
-

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
-
        let some = self.remotes()?.next().is_some();
-
        Ok(!some)
-
    }
-

-
    fn path(&self) -> &Path {
-
        self.backend.path()
-
    }
-

-
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::Error> {
-
        git::ext::Blob::At {
-
            object: oid.into(),
-
            path,
-
        }
-
        .get(&self.backend)
-
    }
-

-
    fn reference(
-
        &self,
-
        remote: &RemoteId,
-
        name: &git::RefStr,
-
    ) -> Result<Option<git2::Reference>, git2::Error> {
-
        let name = name.strip_prefix(git::refname!("refs")).unwrap_or(name);
-
        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 commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
-
        self.backend.find_commit(oid.into()).map(Some).or_else(|e| {
-
            if git::ext::is_not_found_err(&e) {
-
                Ok(None)
-
            } else {
-
                Err(e)
-
            }
-
        })
-
    }
-

-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
-
        let mut revwalk = self.backend.revwalk()?;
-
        revwalk.push(head.into())?;
-

-
        Ok(revwalk)
-
    }
-

-
    fn reference_oid(
-
        &self,
-
        remote: &RemoteId,
-
        reference: &git::RefStr,
-
    ) -> Result<Option<Oid>, git2::Error> {
-
        let reference = self.reference(remote, 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
-
            .backend
-
            .references_glob(format!("refs/remotes/{remote}/*").as_str())?;
-
        let mut refs = BTreeMap::new();
-

-
        for e in entries {
-
            let e = e?;
-
            let name = e.name().ok_or(Error::InvalidRef)?;
-
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
-
            let oid = e.target().ok_or(Error::InvalidRef)?;
-

-
            refs.insert(refname, oid.into());
-
        }
-
        Ok(refs.into())
-
    }
-

-
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error> {
-
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
-
            |reference| -> Result<(RemoteId, Remote<Verified>), refs::Error> {
-
                let r = reference?;
-
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
-
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
-
                let remote = self.remote(&id)?;
-

-
                Ok((id, remote))
-
            },
-
        );
-

-
        Ok(Box::new(iter))
-
    }
-

-
    fn project(&self) -> Result<Project, Error> {
-
        todo!()
-
    }
-
}
-

-
impl<'r> WriteRepository<'r> for Repository {
-
    /// Fetch all remotes of a project from the given URL.
-
    fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, FetchError> {
-
        // TODO: Have function to fetch specific remotes.
-
        //
-
        // Repository layout should look like this:
-
        //
-
        //   /refs/remotes/<remote>
-
        //         /heads
-
        //           /master
-
        //         /tags
-
        //         ...
-
        //
-
        let url = url.to_string();
-
        let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
-
        let mut updates = Vec::new();
-
        let mut callbacks = git2::RemoteCallbacks::new();
-
        let tempdir = tempfile::tempdir()?;
-
        // TODO: Comment
-
        let staging = {
-
            let mut builder = git2::build::RepoBuilder::new();
-
            let path = tempdir.path().join("git");
-
            let staging_repo = builder
-
                .bare(true)
-
                // TODO: Comment
-
                // TODO: Due to this, I think we'll have to run GC when there is a failure.
-
                .clone_local(git2::build::CloneLocal::Local)
-
                .clone(
-
                    &git::Url {
-
                        scheme: git::url::Scheme::File,
-
                        path: self.backend.path().to_string_lossy().to_string().into(),
-
                        ..git::Url::default()
-
                    }
-
                    .to_string(),
-
                    &path,
-
                )?;
-

-
            // In case we fetch an invalid update, we want to make sure nothing is deleted.
-
            let mut opts = git2::FetchOptions::default();
-
            opts.prune(git2::FetchPrune::Off);
-

-
            staging_repo
-
                .remote_anonymous(&url)?
-
                .fetch(refs, Some(&mut opts), None)?;
-
            // TODO: Comment
-
            Repository::from(staging_repo).verify()?;
-

-
            path
-
        };
-

-
        callbacks.update_tips(|name, old, new| {
-
            if let Ok(name) = git::RefString::try_from(name) {
-
                updates.push(RefUpdate::from(name, old, new));
-
            } else {
-
                log::warn!("Invalid ref `{}` detected; aborting fetch", name);
-
                return false;
-
            }
-
            // Returning `true` ensures the process is not aborted.
-
            true
-
        });
-

-
        {
-
            let mut remote = self.backend.remote_anonymous(
-
                &git::Url {
-
                    scheme: git::url::Scheme::File,
-
                    path: staging.to_string_lossy().to_string().into(),
-
                    ..git::Url::default()
-
                }
-
                .to_string(),
-
            )?;
-
            let mut opts = git2::FetchOptions::default();
-
            opts.remote_callbacks(callbacks);
-

-
            // TODO: Make sure we verify before pruning, as pruning may get us into
-
            // a state we can't roll back.
-
            opts.prune(git2::FetchPrune::On);
-
            remote.fetch(refs, Some(&mut opts), None)?;
-
        }
-

-
        Ok(updates)
-
    }
-

-
    fn raw(&self) -> &git2::Repository {
-
        &self.backend
-
    }
-
}
-

-
impl From<git2::Repository> for Repository {
-
    fn from(backend: git2::Repository) -> Self {
-
        Self { backend }
-
    }
-
}
-

-
pub mod trailers {
-
    use std::str::FromStr;
-

-
    use super::*;
-
    use crate::crypto::{PublicKey, PublicKeyError};
-
    use crate::crypto::{Signature, SignatureError};
-

-
    pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
-

-
    #[derive(Error, Debug)]
-
    pub enum Error {
-
        #[error("invalid format for signature trailer")]
-
        SignatureTrailerFormat,
-
        #[error("invalid public key in signature trailer")]
-
        PublicKey(#[from] PublicKeyError),
-
        #[error("invalid signature in trailer")]
-
        Signature(#[from] SignatureError),
-
    }
-

-
    pub fn parse_signatures(msg: &str) -> Result<Vec<(PublicKey, Signature)>, Error> {
-
        let trailers =
-
            git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
-
        let mut signatures = Vec::with_capacity(trailers.len());
-

-
        for (key, val) in trailers.iter() {
-
            if key == SIGNATURE_TRAILER {
-
                if let Some((pk, sig)) = val.split_once(' ') {
-
                    let pk = PublicKey::from_str(pk)?;
-
                    let sig = Signature::from_str(sig)?;
-

-
                    signatures.push((pk, sig));
-
                } else {
-
                    return Err(Error::SignatureTrailerFormat);
-
                }
-
            }
-
        }
-
        Ok(signatures)
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use crate::assert_matches;
-
    use crate::git;
-
    use crate::storage::refs::SIGNATURE_REF;
-
    use crate::storage::{ReadStorage, RefUpdate, WriteRepository};
-
    use crate::test::arbitrary;
-
    use crate::test::crypto::MockSigner;
-
    use crate::test::fixtures;
-

-
    #[test]
-
    fn test_remote_refs() {
-
        let dir = tempfile::tempdir().unwrap();
-
        let storage = fixtures::storage(dir.path());
-
        let inv = storage.inventory().unwrap();
-
        let proj = inv.first().unwrap();
-
        let mut refs = git::remote_refs(&git::Url {
-
            host: Some(dir.path().to_string_lossy().to_string()),
-
            scheme: git_url::Scheme::File,
-
            path: format!("/{}", proj).into(),
-
            ..git::Url::default()
-
        })
-
        .unwrap();
-

-
        let project = storage.repository(proj).unwrap();
-
        let remotes = project.remotes().unwrap();
-

-
        // Strip the remote refs of sigrefs so we can compare them.
-
        for remote in refs.values_mut() {
-
            remote.remove(&*SIGNATURE_REF).unwrap();
-
        }
-

-
        let remotes = remotes
-
            .map(|remote| remote.map(|(id, r): (RemoteId, Remote<Verified>)| (id, r.refs.into())))
-
            .collect::<Result<_, _>>()
-
            .unwrap();
-

-
        assert_eq!(refs, remotes);
-
    }
-

-
    #[test]
-
    fn test_fetch() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let alice = fixtures::storage(tmp.path().join("alice"));
-
        let bob = Storage::open(tmp.path().join("bob")).unwrap();
-
        let inventory = alice.inventory().unwrap();
-
        let proj = inventory.first().unwrap();
-
        let repo = alice.repository(proj).unwrap();
-
        let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
-
        let refname = git::refname!("heads/master");
-

-
        // Have Bob fetch Alice's refs.
-
        let updates = bob
-
            .repository(proj)
-
            .unwrap()
-
            .fetch(&git::Url {
-
                scheme: git_url::Scheme::File,
-
                path: alice
-
                    .path()
-
                    .join(proj.to_string())
-
                    .to_string_lossy()
-
                    .into_owned()
-
                    .into(),
-
                ..git::Url::default()
-
            })
-
            .unwrap();
-

-
        // Four refs are created for each remote.
-
        assert_eq!(updates.len(), remotes.len() * 4);
-

-
        for update in updates {
-
            assert_matches!(
-
                update,
-
                RefUpdate::Created { name, .. } if name.starts_with("refs/remotes")
-
            );
-
        }
-

-
        for remote in remotes {
-
            let (id, _) = remote.unwrap();
-
            let alice_repo = alice.repository(proj).unwrap();
-
            let alice_oid = alice_repo.reference(&id, &refname).unwrap().unwrap();
-

-
            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_fetch_update() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let alice = Storage::open(tmp.path().join("alice/storage")).unwrap();
-
        let bob = Storage::open(tmp.path().join("bob/storage")).unwrap();
-

-
        let alice_signer = MockSigner::new(&mut fastrand::Rng::new());
-
        let alice_id = alice_signer.public_key();
-
        let (proj_id, _, proj_repo, alice_head) =
-
            fixtures::project(tmp.path().join("alice/project"), &alice, &alice_signer).unwrap();
-

-
        let refname = git::refname!("refs/heads/master");
-
        let alice_url = git::Url {
-
            scheme: git_url::Scheme::File,
-
            path: alice
-
                .path()
-
                .join(proj_id.to_string())
-
                .to_string_lossy()
-
                .into_owned()
-
                .into(),
-
            ..git::Url::default()
-
        };
-

-
        // Have Bob fetch Alice's refs.
-
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
-
        // Three refs are created: the branch, the signature and the id.
-
        assert_eq!(updates.len(), 3);
-

-
        let alice_proj_storage = alice.repository(&proj_id).unwrap();
-
        let alice_head = proj_repo.find_commit(alice_head).unwrap();
-
        let alice_head = git::commit(&proj_repo, &alice_head, &refname, "Making changes", "Alice")
-
            .unwrap()
-
            .id();
-
        git::push(&proj_repo).unwrap();
-
        alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
-

-
        // Have Bob fetch Alice's new commit.
-
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
-
        // The branch and signature refs are updated.
-
        assert_matches!(
-
            updates.as_slice(),
-
            &[RefUpdate::Updated { .. }, RefUpdate::Updated { .. }]
-
        );
-

-
        // Bob's storage is updated.
-
        let bob_repo = bob.repository(&proj_id).unwrap();
-
        let bob_master = bob_repo.reference(alice_id, &refname).unwrap().unwrap();
-

-
        assert_eq!(bob_master.target().unwrap(), alice_head);
-
    }
-

-
    #[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()).unwrap();
-
        let proj_id = arbitrary::gen::<Id>(1);
-
        let alice = *signer.public_key();
-
        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();
-

-
        git::commit(
-
            backend,
-
            &head,
-
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
-
            "Second commit",
-
            &alice.to_string(),
-
        )
-
        .unwrap();
-

-
        let signed = storage.sign_refs(&project, &signer).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);
-
    }
-
}
deleted radicle-node/src/storage/refs.rs
@@ -1,378 +0,0 @@
-
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 once_cell::sync::Lazy;
-
use radicle_git_ext as git_ext;
-
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};
-
use crate::wire;
-

-
pub static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("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)]
-
pub struct Refs(BTreeMap<git::RefString, 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(&msg, &signature) {
-
            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 = git::RefString::try_from(name)?;
-
            let oid = Oid::from_str(oid)?;
-

-
            if oid.is_zero() {
-
                continue;
-
            }
-
            refs.insert(name, oid);
-
        }
-
        Ok(Self(refs))
-
    }
-

-
    pub fn canonical(&self) -> Vec<u8> {
-
        let mut buf = String::new();
-
        let refs = self
-
            .iter()
-
            .filter(|(name, oid)| *name != &*SIGNATURE_REF && !oid.is_zero());
-

-
        for (name, oid) in refs {
-
            buf.push_str(&oid.to_string());
-
            buf.push(' ');
-
            buf.push_str(name);
-
            buf.push('\n');
-
        }
-
        buf.into_bytes()
-
    }
-
}
-

-
impl IntoIterator for Refs {
-
    type Item = (git::RefString, Oid);
-
    type IntoIter = std::collections::btree_map::IntoIter<git::RefString, Oid>;
-

-
    fn into_iter(self) -> Self::IntoIter {
-
        self.0.into_iter()
-
    }
-
}
-

-
impl From<Refs> for BTreeMap<git::RefString, 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<git::RefString, Oid>> for Refs {
-
    fn from(refs: BTreeMap<git::RefString, Oid>) -> Self {
-
        Self(refs)
-
    }
-
}
-

-
impl Deref for Refs {
-
    type Target = BTreeMap<git::RefString, 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 key.
-
///
-
/// The type parameter keeps track of whether the signature was [`Verified`] or
-
/// [`Unverified`].
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct SignedRefs<V> {
-
    refs: Refs,
-
    signature: Signature,
-
    _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> {
-
        match self.verify(signer) {
-
            Ok(()) => Ok(SignedRefs {
-
                refs: self.refs,
-
                signature: self.signature,
-
                _verified: PhantomData,
-
            }),
-
            Err(e) => Err(e),
-
        }
-
    }
-

-
    pub fn verify(&self, signer: &PublicKey) -> Result<(), crypto::Error> {
-
        let canonical = self.refs.canonical();
-

-
        match signer.verify(&canonical, &self.signature) {
-
            Ok(()) => Ok(()),
-
            Err(e) => Err(e),
-
        }
-
    }
-
}
-

-
impl SignedRefs<Verified> {
-
    pub fn load<'r, S>(remote: &RemoteId, repo: &S) -> Result<Self, Error>
-
    where
-
        S: ReadRepository<'r>,
-
    {
-
        if let Some(oid) = repo.reference_oid(remote, &SIGNATURE_REF)? {
-
            Self::load_at(oid, remote, repo)
-
        } else {
-
            Err(Error::NotFound)
-
        }
-
    }
-

-
    pub fn load_at<'r, S>(oid: Oid, remote: &RemoteId, repo: &S) -> Result<Self, Error>
-
    where
-
        S: storage::ReadRepository<'r>,
-
    {
-
        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 = signature.content().try_into()?;
-

-
        match remote.verify(refs.content(), &signature) {
-
            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<'r, S: WriteRepository<'r>>(
-
        &self,
-
        // TODO: This should be part of the signed refs.
-
        remote: &RemoteId,
-
        repo: &S,
-
    ) -> Result<Updated, Error> {
-
        let sigref = &*SIGNATURE_REF;
-
        let parent: Option<git2::Commit> = repo
-
            .reference(remote, sigref)?
-
            .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.as_ref())?;
-

-
            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}/{sigref}");
-
        let author = repo.raw().signature()?;
-
        let commit = repo.raw().commit(
-
            Some(&sigref),
-
            &author,
-
            &author,
-
            &format!("Update {} for {}", sigref, 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> wire::Encode for SignedRefs<V> {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = 0;
-

-
        n += self.refs.encode(writer)?;
-
        n += self.signature.encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for SignedRefs<Unverified> {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let refs = Refs::decode(reader)?;
-
        let signature = Signature::decode(reader)?;
-

-
        Ok(Self {
-
            refs,
-
            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(transparent)]
-
        InvalidRef(#[from] git_ref_format::Error),
-
        #[error("invalid canonical format")]
-
        InvalidFormat,
-
        #[error(transparent)]
-
        Io(#[from] io::Error),
-
        #[error(transparent)]
-
        Git(#[from] git2::Error),
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    #[quickcheck]
-
    fn prop_canonical_roundtrip(refs: Refs) {
-
        let encoded = refs.canonical();
-
        let decoded = Refs::from_canonical(&encoded).unwrap();
-

-
        assert_eq!(refs, decoded);
-
    }
-
}
modified radicle-node/src/test.rs
@@ -1,10 +1,11 @@
pub(crate) mod arbitrary;
-
pub(crate) mod assert;
-
pub(crate) mod crypto;
-
pub(crate) mod fixtures;
pub(crate) mod handle;
pub(crate) mod logger;
pub(crate) mod peer;
pub(crate) mod simulator;
-
pub(crate) mod storage;
pub(crate) mod tests;
+

+
#[cfg(test)]
+
pub use radicle::assert_matches;
+
#[cfg(test)]
+
pub use radicle::test::*;
modified radicle-node/src/test/arbitrary.rs
@@ -1,72 +1,20 @@
-
use std::collections::{BTreeMap, HashSet};
-
use std::hash::Hash;
-
use std::iter;
use std::net;
-
use std::ops::RangeBounds;
-
use std::path::PathBuf;

use bloomy::BloomFilter;
-
use nonempty::NonEmpty;
use quickcheck::Arbitrary;

-
use crate::collections::HashMap;
use crate::crypto;
-
use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified};
-
use crate::git;
-
use crate::hash;
-
use crate::identity::{doc::Delegate, doc::Doc, Did, Id, Project};
+
use crate::identity::Id;
use crate::service::filter::{Filter, FILTER_SIZE};
use crate::service::message::{
    Address, Envelope, InventoryAnnouncement, Message, NodeAnnouncement, RefsAnnouncement,
    Subscribe,
};
use crate::service::{NodeId, Timestamp};
-
use crate::storage;
-
use crate::storage::refs::{Refs, SignedRefs};
-
use crate::test::storage::MockStorage;
+
use crate::storage::refs::Refs;
use crate::wire::message::MessageType;

-
use super::crypto::MockSigner;
-

-
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
-
    let size = fastrand::usize(range);
-
    let mut set = HashSet::with_capacity(size);
-
    let mut g = quickcheck::Gen::new(size);
-

-
    while set.len() < size {
-
        set.insert(T::arbitrary(&mut g));
-
    }
-
    set
-
}
-

-
pub fn gen<T: Arbitrary>(size: usize) -> T {
-
    let mut gen = quickcheck::Gen::new(size);
-

-
    T::arbitrary(&mut gen)
-
}
-

-
#[derive(Clone, Debug)]
-
pub struct ByteArray<const N: usize>([u8; N]);
-

-
impl<const N: usize> ByteArray<N> {
-
    pub fn into_inner(self) -> [u8; N] {
-
        self.0
-
    }
-

-
    pub fn as_slice(&self) -> &[u8] {
-
        self.0.as_slice()
-
    }
-
}
-

-
impl<const N: usize> Arbitrary for ByteArray<N> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let mut bytes: [u8; N] = [0; N];
-
        for byte in &mut bytes {
-
            *byte = u8::arbitrary(g);
-
        }
-
        Self(bytes)
-
    }
-
}
+
pub use radicle::test::arbitrary::*;

impl Arbitrary for Filter {
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
@@ -159,178 +107,3 @@ impl Arbitrary for Address {
        }
    }
}
-

-
impl Arbitrary for storage::Remotes<crypto::Verified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
-
            Arbitrary::arbitrary(g);
-

-
        storage::Remotes::new(remotes)
-
    }
-
}
-

-
impl Arbitrary for MockStorage {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let inventory = Arbitrary::arbitrary(g);
-
        MockStorage::new(inventory)
-
    }
-
}
-

-
impl Arbitrary for Project {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let doc = Doc::<Verified>::arbitrary(g);
-
        let (oid, _) = doc.encode().unwrap();
-
        let id = Id::from(oid);
-
        let remotes = storage::Remotes::arbitrary(g);
-
        let path = PathBuf::arbitrary(g);
-

-
        Self {
-
            id,
-
            doc,
-
            remotes,
-
            path,
-
        }
-
    }
-
}
-

-
impl Arbitrary for Did {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        Self::from(PublicKey::arbitrary(g))
-
    }
-
}
-

-
impl Arbitrary for Delegate {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        Self {
-
            name: String::arbitrary(g),
-
            id: Did::arbitrary(g),
-
        }
-
    }
-
}
-

-
impl Arbitrary for Doc<Unverified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let name = String::arbitrary(g);
-
        let description = String::arbitrary(g);
-
        let default_branch = String::arbitrary(g);
-
        let delegate = Delegate::arbitrary(g);
-

-
        Self::initial(name, description, default_branch, delegate)
-
    }
-
}
-

-
impl Arbitrary for Doc<Verified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
-
        let name = iter::repeat_with(|| rng.alphanumeric())
-
            .take(rng.usize(1..16))
-
            .collect();
-
        let description = iter::repeat_with(|| rng.alphanumeric())
-
            .take(rng.usize(0..32))
-
            .collect();
-
        let default_branch = iter::repeat_with(|| rng.alphanumeric())
-
            .take(rng.usize(1..16))
-
            .collect();
-
        let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
-
            name: iter::repeat_with(|| rng.alphanumeric())
-
                .take(rng.usize(1..16))
-
                .collect(),
-
            id: Did::arbitrary(g),
-
        })
-
        .take(rng.usize(1..6))
-
        .collect::<Vec<_>>()
-
        .try_into()
-
        .unwrap();
-
        let threshold = delegates.len() / 2 + 1;
-
        let doc: Doc<Unverified> =
-
            Doc::new(name, description, default_branch, delegates, threshold);
-

-
        doc.verified().unwrap()
-
    }
-
}
-

-
impl Arbitrary for SignedRefs<Unverified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
-
        let signature = crypto::Signature::from(bytes.into_inner());
-
        let refs = Refs::arbitrary(g);
-

-
        Self::new(refs, signature)
-
    }
-
}
-

-
impl Arbitrary for Refs {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
-
        let mut bytes: [u8; 20] = [0; 20];
-
        let names = &[
-
            "heads/master",
-
            "heads/feature/1",
-
            "heads/feature/2",
-
            "heads/feature/3",
-
            "heads/radicle/id",
-
            "tags/v1.0",
-
            "tags/v2.0",
-
            "notes/1",
-
        ];
-

-
        for _ in 0..g.size().min(names.len()) {
-
            if let Some(name) = g.choose(names) {
-
                for byte in &mut bytes {
-
                    *byte = u8::arbitrary(g);
-
                }
-
                let oid = storage::Oid::try_from(&bytes[..]).unwrap();
-
                let name = git::RefString::try_from(*name).unwrap();
-

-
                refs.insert(name, oid);
-
            }
-
        }
-
        Self::from(refs)
-
    }
-
}
-

-
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 bytes: ByteArray<32> = Arbitrary::arbitrary(g);
-
        let seed = Seed::new(bytes.into_inner());
-
        let sk = KeyPair::from_seed(seed).sk;
-

-
        MockSigner::from(sk)
-
    }
-
}
-

-
impl Arbitrary for Id {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes = ByteArray::<20>::arbitrary(g);
-
        let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
-

-
        Id::from(oid)
-
    }
-
}
-

-
impl Arbitrary for hash::Digest {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: Vec<u8> = Arbitrary::arbitrary(g);
-
        hash::Digest::new(&bytes)
-
    }
-
}
-

-
impl Arbitrary for PublicKey {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
-
        let seed = Seed::new(bytes.into_inner());
-
        let keypair = KeyPair::from_seed(seed);
-

-
        PublicKey(keypair.pk)
-
    }
-
}
deleted radicle-node/src/test/assert.rs
@@ -1,296 +0,0 @@
-
// Copyright (c) 2016 Murarth
-
//
-
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
-
// and associated documentation files (the "Software"), to deal in the Software without
-
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
-
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
-
// Software is furnished to do so, subject to the following conditions:
-
//
-
// The above copyright notice and this permission notice shall be included in all copies or
-
// substantial portions of the Software.
-
//
-
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
-
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-

-
//! Provides a macro, `assert_matches!`, which tests whether a value
-
//! matches a given pattern, causing a panic if the match fails.
-
//!
-
//! See the macro [`assert_matches!`] documentation for more information.
-
//!
-
//! Also provides a debug-only counterpart, [`debug_assert_matches!`].
-
//!
-
//! See the macro [`debug_assert_matches!`] documentation for more information
-
//! about this macro.
-
//!
-
//! [`assert_matches!`]: macro.assert_matches.html
-
//! [`debug_assert_matches!`]: macro.debug_assert_matches.html
-

-
#![deny(missing_docs)]
-

-
/// Asserts that an expression matches a given pattern.
-
///
-
/// A guard expression may be supplied to add further restrictions to the
-
/// expected value of the expression.
-
///
-
/// A `match` arm may be supplied to perform additional assertions or to yield
-
/// a value from the macro invocation.
-
///
-
#[macro_export]
-
macro_rules! assert_matches {
-
    ( $e:expr , $($pat:pat_param)|+ ) => {
-
        match $e {
-
            $($pat)|+ => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr ) => {
-
        match $e {
-
            $($pat)|+ if $cond => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+ if $cond))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr ) => {
-
        match $e {
-
            $($pat)|+ => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr ) => {
-
        match $e {
-
            $($pat)|+ if $cond => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+ if $cond))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+), format_args!($($arg)*))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ if $cond => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+), format_args!($($arg)*))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ if $cond => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
-
        }
-
    };
-
}
-

-
/// Asserts that an expression matches a given pattern.
-
///
-
/// Unlike [`assert_matches!`], `debug_assert_matches!` statements are only enabled
-
/// in non-optimized builds by default. An optimized build will omit all
-
/// `debug_assert_matches!` statements unless `-C debug-assertions` is passed
-
/// to the compiler.
-
///
-
/// See the macro [`assert_matches!`] documentation for more information.
-
///
-
/// [`assert_matches!`]: macro.assert_matches.html
-
#[macro_export(local_inner_macros)]
-
macro_rules! debug_assert_matches {
-
    ( $($tt:tt)* ) => { {
-
        if _assert_matches_cfg!(debug_assertions) {
-
            assert_matches!($($tt)*);
-
        }
-
    } }
-
}
-

-
#[doc(hidden)]
-
#[macro_export]
-
macro_rules! _assert_matches_cfg {
-
    ( $($tt:tt)* ) => { cfg!($($tt)*) }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use std::panic::{catch_unwind, UnwindSafe};
-

-
    #[derive(Debug)]
-
    enum Foo {
-
        A(i32),
-
        B(&'static str),
-
        C(&'static str),
-
    }
-

-
    #[test]
-
    fn test_assert_succeed() {
-
        let a = Foo::A(123);
-

-
        assert_matches!(a, Foo::A(_));
-
        assert_matches!(a, Foo::A(123));
-
        assert_matches!(a, Foo::A(i) if i == 123);
-
        assert_matches!(a, Foo::A(42) | Foo::A(123));
-

-
        let b = Foo::B("foo");
-

-
        assert_matches!(b, Foo::B(_));
-
        assert_matches!(b, Foo::B("foo"));
-
        assert_matches!(b, Foo::B(s) if s == "foo");
-
        assert_matches!(b, Foo::B(s) => assert_eq!(s, "foo"));
-
        assert_matches!(b, Foo::B(s) => { assert_eq!(s, "foo") });
-
        assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "foo"));
-
        assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo") });
-

-
        let c = Foo::C("foo");
-

-
        assert_matches!(c, Foo::B(_) | Foo::C(_));
-
        assert_matches!(c, Foo::B("foo") | Foo::C("foo"));
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo");
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) => assert_eq!(s, "foo"));
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) => { assert_eq!(s, "foo") });
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => assert_eq!(s, "foo"));
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => { assert_eq!(s, "foo") });
-
    }
-

-
    #[test]
-
    #[should_panic]
-
    fn test_assert_panic_0() {
-
        let a = Foo::A(123);
-

-
        assert_matches!(a, Foo::B(_));
-
    }
-

-
    #[test]
-
    #[should_panic]
-
    fn test_assert_panic_1() {
-
        let b = Foo::B("foo");
-

-
        assert_matches!(b, Foo::B("bar"));
-
    }
-

-
    #[test]
-
    #[should_panic]
-
    fn test_assert_panic_2() {
-
        let b = Foo::B("foo");
-

-
        assert_matches!(b, Foo::B(s) if s == "bar");
-
    }
-

-
    #[test]
-
    fn test_assert_no_move() {
-
        let b = &mut Foo::A(0);
-
        assert_matches!(*b, Foo::A(0));
-
    }
-

-
    #[test]
-
    fn assert_with_message() {
-
        let a = Foo::A(0);
-

-
        assert_matches!(a, Foo::A(_), "o noes");
-
        assert_matches!(a, Foo::A(n) if n == 0, "o noes");
-
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes");
-
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
-
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes");
-
        assert_matches!(a, Foo::A(n) if n == 0 => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
-
        assert_matches!(a, Foo::A(_), "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(_), "o noes {value:?}", value = a);
-
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {value:?}", value=a);
-
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {value:?}", value=a);
-
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {value:?}", value=a);
-
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes {value:?}", value=a);
-
    }
-

-
    fn panic_message<F>(f: F) -> String
-
    where
-
        F: FnOnce() + UnwindSafe,
-
    {
-
        let err = catch_unwind(f).expect_err("function did not panic");
-

-
        *err.downcast::<String>()
-
            .expect("function panicked with non-String value")
-
    }
-

-
    #[test]
-
    fn test_panic_message() {
-
        let a = Foo::A(1);
-

-
        // expr, pat
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_));
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
-
        );
-

-
        // expr, pat if cond
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
-
        );
-

-
        // expr, pat => arm
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_) => {});
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
-
        );
-

-
        // expr, pat if cond => arm
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo" => {});
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
-
        );
-

-
        // expr, pat, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_), "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
-
        );
-

-
        // expr, pat if cond, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo", "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
-
        );
-

-
        // expr, pat => arm, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_) => {}, "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
-
        );
-

-
        // expr, pat if cond => arm, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo" => {}, "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
-
        );
-
    }
-
}
deleted radicle-node/src/test/crypto.rs
@@ -1,65 +0,0 @@
-
use crate::crypto::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer};
-

-
#[derive(Debug, Clone)]
-
pub struct MockSigner {
-
    pk: PublicKey,
-
    sk: SecretKey,
-
}
-

-
impl MockSigner {
-
    pub fn new(rng: &mut fastrand::Rng) -> Self {
-
        let mut bytes: [u8; 32] = [0; 32];
-

-
        for byte in &mut bytes {
-
            *byte = rng.u8(..);
-
        }
-
        let seed = Seed::new(bytes);
-
        let keypair = KeyPair::from_seed(seed);
-

-
        Self::from(keypair.sk)
-
    }
-
}
-

-
impl From<SecretKey> for MockSigner {
-
    fn from(sk: SecretKey) -> Self {
-
        let pk = sk.public_key().into();
-
        Self { sk, pk }
-
    }
-
}
-

-
impl Default for MockSigner {
-
    fn default() -> Self {
-
        let seed = Seed::generate();
-
        let keypair = KeyPair::from_seed(seed);
-
        let sk = keypair.sk;
-

-
        Self {
-
            pk: sk.public_key().into(),
-
            sk,
-
        }
-
    }
-
}
-

-
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 {
-
    fn public_key(&self) -> &PublicKey {
-
        &self.pk
-
    }
-

-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.sk.sign(msg, None).into()
-
    }
-
}
deleted radicle-node/src/test/fixtures.rs
@@ -1,116 +0,0 @@
-
use std::path::Path;
-

-
use crate::crypto::{Signer, Verified};
-
use crate::git;
-
use crate::identity::Id;
-
use crate::rad;
-
use crate::storage::git::Storage;
-
use crate::storage::refs::SignedRefs;
-
use crate::storage::{BranchName, WriteStorage};
-
use crate::test::arbitrary;
-
use crate::test::crypto::MockSigner;
-

-
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
-
    let path = path.as_ref();
-
    let proj_ids = arbitrary::set::<Id>(3..=3);
-
    let signers = arbitrary::set::<MockSigner>(3..=3);
-
    let storage = Storage::open(path).unwrap();
-

-
    crate::test::logger::init(log::Level::Debug);
-

-
    for signer in signers {
-
        let remote = signer.public_key();
-

-
        log::debug!("signer {}...", remote);
-

-
        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();
-

-
            log::debug!("{}: creating {}...", remote, proj);
-

-
            raw.reference(
-
                &format!("refs/remotes/{remote}/heads/radicle/id"),
-
                head.id(),
-
                false,
-
                "test",
-
            )
-
            .unwrap();
-

-
            let head = git::commit(
-
                raw,
-
                &head,
-
                &git::RefString::try_from(format!("refs/remotes/{remote}/heads/master")).unwrap(),
-
                "Second commit",
-
                &remote.to_string(),
-
            )
-
            .unwrap();
-

-
            git::commit(
-
                raw,
-
                &head,
-
                &git::RefString::try_from(format!("refs/remotes/{remote}/heads/patch/3")).unwrap(),
-
                "Third commit",
-
                &remote.to_string(),
-
            )
-
            .unwrap();
-

-
            storage.sign_refs(&repo, &signer).unwrap();
-
        }
-
    }
-
    storage
-
}
-

-
/// Create a new repository at the given path, and initialize it into a project.
-
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
-
    path: P,
-
    storage: &'r S,
-
    signer: G,
-
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
-
    let (repo, head) = repository(path);
-
    let (id, refs) = rad::init(
-
        &repo,
-
        "acme",
-
        "Acme's repository",
-
        BranchName::from("master"),
-
        signer,
-
        storage,
-
    )?;
-

-
    Ok((id, refs, repo, head))
-
}
-

-
/// Creates a regular repository at the given path with a couple of commits.
-
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
-
    let repo = git2::Repository::init(path).unwrap();
-
    let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
-
    let head = git::initial_commit(&repo, &sig).unwrap();
-
    let oid = git::commit(
-
        &repo,
-
        &head,
-
        git::refname!("refs/heads/master").as_refstr(),
-
        "Second commit",
-
        "anonymous",
-
    )
-
    .unwrap()
-
    .id();
-

-
    // Look, I don't really understand why we have to do this, but we do.
-
    drop(head);
-

-
    (repo, oid)
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

-
    #[test]
-
    fn smoke() {
-
        let tmp = tempfile::tempdir().unwrap();
-

-
        storage(&tmp.path());
-
    }
-
}
modified radicle-node/src/test/peer.rs
@@ -1,18 +1,18 @@
use std::net;
use std::ops::{Deref, DerefMut};

-
use git_url::Url;
use log::*;

use crate::address_book::{KnownAddress, Source};
use crate::clock::RefClock;
use crate::collections::HashMap;
+
use crate::git::Url;
use crate::service;
use crate::service::config::*;
use crate::service::message::*;
use crate::service::*;
use crate::storage::WriteStorage;
-
use crate::test::crypto::MockSigner;
+
use crate::test::signer::MockSigner;
use crate::test::simulator;
use crate::{Link, LocalTime};

deleted radicle-node/src/test/storage.rs
@@ -1,142 +0,0 @@
-
use git_url::Url;
-

-
use crate::crypto::{Signer, Verified};
-
use crate::git;
-
use crate::identity::{Id, Project};
-
use crate::storage::{refs, RefUpdate};
-
use crate::storage::{
-
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, RemoteId, WriteRepository,
-
    WriteStorage,
-
};
-

-
#[derive(Clone, Debug)]
-
pub struct MockStorage {
-
    pub inventory: Vec<Project>,
-
}
-

-
impl MockStorage {
-
    pub fn new(inventory: Vec<Project>) -> Self {
-
        Self { inventory }
-
    }
-

-
    pub fn empty() -> Self {
-
        Self {
-
            inventory: Vec::new(),
-
        }
-
    }
-
}
-

-
impl ReadStorage for MockStorage {
-
    fn url(&self) -> Url {
-
        Url {
-
            scheme: git_url::Scheme::Radicle,
-
            host: Some("mock".to_string()),
-
            ..Url::default()
-
        }
-
    }
-

-
    fn get(&self, _remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
-
        if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) {
-
            return Ok(Some(proj.clone()));
-
        }
-
        Ok(None)
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        let inventory = self
-
            .inventory
-
            .iter()
-
            .map(|proj| proj.id.clone())
-
            .collect::<Vec<_>>();
-

-
        Ok(inventory)
-
    }
-
}
-

-
impl WriteStorage<'_> for MockStorage {
-
    type Repository = MockRepository;
-

-
    fn repository(&self, _proj: &Id) -> Result<Self::Repository, Error> {
-
        Ok(MockRepository {})
-
    }
-

-
    fn sign_refs<G: Signer>(
-
        &self,
-
        _repository: &Self::Repository,
-
        _signer: G,
-
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
-
        todo!()
-
    }
-
}
-

-
pub struct MockRepository {}
-

-
impl ReadRepository<'_> for MockRepository {
-
    type Remotes = std::iter::Empty<Result<(RemoteId, Remote<Verified>), refs::Error>>;
-

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
-
        Ok(true)
-
    }
-

-
    fn path(&self) -> &std::path::Path {
-
        todo!()
-
    }
-

-
    fn remote(&self, _remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
-
        todo!()
-
    }
-

-
    fn remotes(&self) -> Result<Self::Remotes, git2::Error> {
-
        todo!()
-
    }
-

-
    fn commit(&self, _oid: git::Oid) -> Result<Option<git2::Commit>, git2::Error> {
-
        todo!()
-
    }
-

-
    fn revwalk(&self, _head: git::Oid) -> Result<git2::Revwalk, git2::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,
-
        _remote: &RemoteId,
-
        _reference: &git::RefStr,
-
    ) -> Result<Option<git2::Reference>, git2::Error> {
-
        todo!()
-
    }
-

-
    fn reference_oid(
-
        &self,
-
        _remote: &RemoteId,
-
        _reference: &git::RefStr,
-
    ) -> Result<Option<radicle_git_ext::Oid>, git2::Error> {
-
        todo!()
-
    }
-

-
    fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> {
-
        todo!()
-
    }
-

-
    fn project(&self) -> Result<Project, Error> {
-
        todo!()
-
    }
-
}
-

-
impl WriteRepository<'_> for MockRepository {
-
    fn fetch(&mut self, _url: &Url) -> Result<Vec<RefUpdate>, FetchError> {
-
        Ok(vec![])
-
    }
-

-
    fn raw(&self) -> &git2::Repository {
-
        todo!()
-
    }
-
}
modified radicle-node/src/test/tests.rs
@@ -11,15 +11,17 @@ use crate::service::peer::*;
use crate::service::*;
use crate::storage::git::Storage;
use crate::storage::ReadStorage;
+
use crate::test::assert_matches;
use crate::test::fixtures;
#[allow(unused)]
use crate::test::logger;
use crate::test::peer::Peer;
+
use crate::test::signer::MockSigner;
use crate::test::simulator;
use crate::test::simulator::{Peer as _, Simulation};
use crate::test::storage::MockStorage;
-
use crate::{assert_matches, Link, LocalTime};
use crate::{client, identity, rad, service, storage, test};
+
use crate::{Link, LocalTime};

// NOTE
//
@@ -136,7 +138,8 @@ fn test_inventory_sync() {
        [7, 7, 7, 7],
        Storage::open(tmp.path().join("alice")).unwrap(),
    );
-
    let bob_storage = fixtures::storage(tmp.path().join("bob"));
+
    let bob_signer = MockSigner::default();
+
    let bob_storage = fixtures::storage(tmp.path().join("bob"), bob_signer).unwrap();
    let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
    let now = LocalTime::now().as_secs();
    let projs = bob.storage().inventory().unwrap();
modified radicle-node/src/wire.rs
@@ -12,7 +12,7 @@ use nakamoto_net as nakamoto;
use nakamoto_net::Link;

use crate::address_book;
-
use crate::crypto::{PublicKey, Signature, Signer};
+
use crate::crypto::{PublicKey, Signature, Signer, Unverified};
use crate::decoder::Decoder;
use crate::git;
use crate::git::fmt;
@@ -21,6 +21,7 @@ use crate::identity::Id;
use crate::service;
use crate::service::filter;
use crate::storage::refs::Refs;
+
use crate::storage::refs::SignedRefs;
use crate::storage::WriteStorage;

/// The default type we use to represent sizes.
@@ -266,7 +267,7 @@ impl Decode for git::Oid {
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
        let len = usize::decode(reader)?;
        #[allow(non_upper_case_globals)]
-
        const expected: usize = mem::size_of::<git2::Oid>();
+
        const expected: usize = mem::size_of::<git::raw::Oid>();

        if len != expected {
            return Err(Error::InvalidSize {
@@ -276,7 +277,7 @@ impl Decode for git::Oid {
        }

        let buf: [u8; expected] = Decode::decode(reader)?;
-
        let oid = git2::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
+
        let oid = git::raw::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
        let oid = git::Oid::from(oid);

        Ok(oid)
@@ -414,6 +415,26 @@ impl Decode for filter::Filter {
    }
}

+
impl<V> Encode for SignedRefs<V> {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = 0;
+

+
        n += self.refs.encode(writer)?;
+
        n += self.signature.encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl Decode for SignedRefs<Unverified> {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let refs = Refs::decode(reader)?;
+
        let signature = Signature::decode(reader)?;
+

+
        Ok(Self::new(refs, signature))
+
    }
+
}
+

#[derive(Debug)]
pub struct Wire<S, T, G> {
    inboxes: HashMap<IpAddr, Decoder>,
added radicle/Cargo.toml
@@ -0,0 +1,37 @@
+
[package]
+
name = "radicle"
+
license = "MIT OR Apache-2.0"
+
version = "0.2.0"
+
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
+
edition = "2021"
+

+
[features]
+
default = []
+
test = ["quickcheck"]
+

+
[dependencies]
+
ed25519-compact = { version = "1.0.12", features = ["pem"] }
+
fastrand = { version = "1.8.0" }
+
git-ref-format = { version = "0", features = ["serde", "macro"] }
+
git2 = { version = "0.13" }
+
git-url = { version = "0.3.5", features = ["serde1"] }
+
multibase = { version = "0.9.1" }
+
log = { version = "0.4.17", features = ["std"] }
+
once_cell = { version = "1.13" }
+
olpc-cjson = { version = "0.1.1" }
+
sha2 = { version = "0.10.2" }
+
serde = { version = "1", features = ["derive"] }
+
serde_json = { version = "1", features = ["preserve_order"] }
+
siphasher = { version = "0.3.10" }
+
radicle-git-ext = { version = "0", features = ["serde"] }
+
nonempty = { version = "0.8.0", features = ["serialize"] }
+
tempfile = { version = "3.3.0" }
+
thiserror = { version = "1" }
+

+
[dependencies.quickcheck]
+
version = "1"
+
default-features = false
+
optional = true
+

+
[dev-dependencies]
+
quickcheck_macros = { version = "1", default-features = false }
added radicle/src/collections.rs
@@ -0,0 +1,44 @@
+
//! Useful collections for peer-to-peer networking.
+
use siphasher::sip::SipHasher13;
+

+
/// A `HashMap` which uses [`fastrand::Rng`] for its random state.
+
pub type HashMap<K, V> = std::collections::HashMap<K, V, RandomState>;
+

+
/// A `HashSet` which uses [`fastrand::Rng`] for its random state.
+
pub type HashSet<K> = std::collections::HashSet<K, RandomState>;
+

+
/// Random hasher state.
+
#[derive(Clone)]
+
pub struct RandomState {
+
    key1: u64,
+
    key2: u64,
+
}
+

+
impl Default for RandomState {
+
    fn default() -> Self {
+
        Self::new(fastrand::Rng::new())
+
    }
+
}
+

+
impl RandomState {
+
    fn new(rng: fastrand::Rng) -> Self {
+
        Self {
+
            key1: rng.u64(..),
+
            key2: rng.u64(..),
+
        }
+
    }
+
}
+

+
impl std::hash::BuildHasher for RandomState {
+
    type Hasher = SipHasher13;
+

+
    fn build_hasher(&self) -> Self::Hasher {
+
        SipHasher13::new_with_keys(self.key1, self.key2)
+
    }
+
}
+

+
impl From<fastrand::Rng> for RandomState {
+
    fn from(rng: fastrand::Rng) -> Self {
+
        Self::new(rng)
+
    }
+
}
added radicle/src/crypto.rs
@@ -0,0 +1,225 @@
+
use std::sync::Arc;
+
use std::{fmt, ops::Deref, str::FromStr};
+

+
use ed25519_compact as ed25519;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
pub use ed25519::{Error, KeyPair, Seed};
+

+
/// Verified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub struct Verified;
+
/// Unverified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub struct Unverified;
+

+
pub trait Signer: Send + Sync {
+
    /// Return this signer's public/verification key.
+
    fn public_key(&self) -> &PublicKey;
+
    /// Sign a message and return the signature.
+
    fn sign(&self, msg: &[u8]) -> Signature;
+
}
+

+
impl<T> Signer for Arc<T>
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+
}
+

+
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()
+
    }
+
}
+

+
/// Cryptographic signature.
+
#[derive(PartialEq, Eq, Copy, Clone)]
+
pub struct Signature(pub ed25519::Signature);
+

+
impl fmt::Display for Signature {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        let base = multibase::Base::Base58Btc;
+
        write!(f, "{}", multibase::encode(base, self.deref()))
+
    }
+
}
+

+
impl fmt::Debug for Signature {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Signature({})", self)
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum SignatureError {
+
    #[error("invalid multibase string: {0}")]
+
    Multibase(#[from] multibase::Error),
+
    #[error("invalid signature: {0}")]
+
    Invalid(#[from] ed25519::Error),
+
}
+

+
impl From<ed25519::Signature> for Signature {
+
    fn from(other: ed25519::Signature) -> Self {
+
        Self(other)
+
    }
+
}
+

+
impl FromStr for Signature {
+
    type Err = SignatureError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
+

+
        Ok(Self(sig))
+
    }
+
}
+

+
impl Deref for Signature {
+
    type Target = ed25519::Signature;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl From<[u8; 64]> for Signature {
+
    fn from(bytes: [u8; 64]) -> Self {
+
        Self(ed25519::Signature::new(bytes))
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Signature {
+
    type Error = ed25519::Error;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+
        ed25519::Signature::from_slice(bytes).map(Self)
+
    }
+
}
+

+
/// The public/verification key.
+
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
+
#[serde(into = "String", try_from = "String")]
+
pub struct PublicKey(pub ed25519::PublicKey);
+

+
/// The private/signing key.
+
pub type SecretKey = ed25519::SecretKey;
+

+
#[derive(Error, Debug)]
+
pub enum PublicKeyError {
+
    #[error("invalid length {0}")]
+
    InvalidLength(usize),
+
    #[error("invalid multibase string: {0}")]
+
    Multibase(#[from] multibase::Error),
+
    #[error("invalid key: {0}")]
+
    InvalidKey(#[from] ed25519::Error),
+
}
+

+
impl std::hash::Hash for PublicKey {
+
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
        self.0.deref().hash(state)
+
    }
+
}
+

+
impl fmt::Display for PublicKey {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", self.to_human())
+
    }
+
}
+

+
impl From<PublicKey> for String {
+
    fn from(other: PublicKey) -> Self {
+
        other.to_human()
+
    }
+
}
+

+
impl fmt::Debug for PublicKey {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "PublicKey({})", self)
+
    }
+
}
+

+
impl PartialEq for PublicKey {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.0 == other.0
+
    }
+
}
+

+
impl From<ed25519::PublicKey> for PublicKey {
+
    fn from(other: ed25519::PublicKey) -> Self {
+
        Self(other)
+
    }
+
}
+

+
impl TryFrom<[u8; 32]> for PublicKey {
+
    type Error = ed25519::Error;
+

+
    fn try_from(other: [u8; 32]) -> Result<Self, Self::Error> {
+
        Ok(Self(ed25519::PublicKey::new(other)))
+
    }
+
}
+

+
impl PublicKey {
+
    pub fn to_human(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, self.0.deref())
+
    }
+
}
+

+
impl FromStr for PublicKey {
+
    type Err = PublicKeyError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let array: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|v: Vec<u8>| PublicKeyError::InvalidLength(v.len()))?;
+
        let key = ed25519::PublicKey::new(array);
+

+
        Ok(Self(key))
+
    }
+
}
+

+
impl TryFrom<String> for PublicKey {
+
    type Error = PublicKeyError;
+

+
    fn try_from(value: String) -> Result<Self, Self::Error> {
+
        Self::from_str(&value)
+
    }
+
}
+

+
impl Deref for PublicKey {
+
    type Target = ed25519::PublicKey;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use crate::crypto::PublicKey;
+
    use quickcheck_macros::quickcheck;
+
    use std::str::FromStr;
+

+
    #[quickcheck]
+
    fn prop_encode_decode(input: PublicKey) {
+
        let encoded = input.to_string();
+
        let decoded = PublicKey::from_str(&encoded).unwrap();
+

+
        assert_eq!(input, decoded);
+
    }
+
}
added radicle/src/git.rs
@@ -0,0 +1,244 @@
+
use std::path::Path;
+
use std::str::FromStr;
+

+
use git_ref_format as format;
+
use once_cell::sync::Lazy;
+

+
use crate::collections::HashMap;
+
use crate::crypto::PublicKey;
+
use crate::storage::refs::Refs;
+
use crate::storage::RemoteId;
+

+
pub use ext::Error;
+
pub use ext::Oid;
+
pub use git2 as raw;
+
pub use git_ref_format as fmt;
+
pub use git_ref_format::{refname, RefStr, RefString};
+
pub use git_url as url;
+
pub use git_url::Url;
+
pub use radicle_git_ext as ext;
+

+
/// Default port of the `git` transport protocol.
+
pub const PROTOCOL_PORT: u16 = 9418;
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum RefError {
+
    #[error("invalid ref name '{0}'")]
+
    InvalidName(format::RefString),
+
    #[error("invalid ref format: {0}")]
+
    Format(#[from] format::Error),
+
}
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum ListRefsError {
+
    #[error("git error: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("invalid ref: {0}")]
+
    InvalidRef(#[from] RefError),
+
}
+

+
pub mod refs {
+
    use super::*;
+

+
    /// Where project information is kept.
+
    pub static IDENTITY_BRANCH: Lazy<RefString> = Lazy::new(|| refname!("radicle/id"));
+

+
    pub mod storage {
+
        use super::*;
+

+
        pub fn branch(remote: &RemoteId, branch: &str) -> String {
+
            format!("refs/remotes/{remote}/heads/{branch}")
+
        }
+

+
        /// Get the branch used to track project information.
+
        pub fn id(remote: &RemoteId) -> String {
+
            branch(remote, &IDENTITY_BRANCH)
+
        }
+
    }
+

+
    pub mod workdir {
+
        pub fn branch(branch: &str) -> String {
+
            format!("refs/heads/{branch}")
+
        }
+

+
        pub fn note(name: &str) -> String {
+
            format!("refs/notes/{name}")
+
        }
+

+
        pub fn remote_branch(remote: &str, branch: &str) -> String {
+
            format!("refs/remotes/{remote}/{branch}")
+
        }
+

+
        pub fn tag(name: &str) -> String {
+
            format!("refs/tags/{name}")
+
        }
+
    }
+
}
+

+
/// List remote refs of a project, given the remote URL.
+
pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError> {
+
    let url = url.to_string();
+
    let mut remotes = HashMap::default();
+
    let mut remote = git2::Remote::create_detached(&url)?;
+

+
    remote.connect(git2::Direction::Fetch)?;
+

+
    let refs = remote.list()?;
+
    for r in refs {
+
        let (id, refname) = parse_ref::<PublicKey>(r.name())?;
+
        let entry = remotes.entry(id).or_insert_with(Refs::default);
+

+
        entry.insert(refname, r.oid().into());
+
    }
+

+
    Ok(remotes)
+
}
+

+
/// Parse a ref string.
+
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
+
    let input = format::RefStr::try_from_str(s)?;
+
    let suffix = input
+
        .strip_prefix(format::refname!("refs/remotes"))
+
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
+

+
    let mut components = suffix.components();
+
    let id = components
+
        .next()
+
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
+
    let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
+
    let refstr = components.collect::<format::RefString>();
+

+
    Ok((id, refstr))
+
}
+

+
/// Create an initial empty commit.
+
pub fn initial_commit<'a>(
+
    repo: &'a git2::Repository,
+
    sig: &git2::Signature,
+
) -> Result<git2::Commit<'a>, git2::Error> {
+
    let tree_id = repo.index()?.write_tree()?;
+
    let tree = repo.find_tree(tree_id)?;
+
    let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
+
    let commit = repo.find_commit(oid).unwrap();
+

+
    Ok(commit)
+
}
+

+
/// Create a commit and update the given ref to it.
+
pub fn commit<'a>(
+
    repo: &'a git2::Repository,
+
    parent: &'a git2::Commit,
+
    target: &RefStr,
+
    message: &str,
+
    user: &str,
+
) -> Result<git2::Commit<'a>, git2::Error> {
+
    let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
+
    let tree_id = repo.index()?.write_tree()?;
+
    let tree = repo.find_tree(tree_id)?;
+
    let oid = repo.commit(Some(target.as_str()), &sig, &sig, message, &tree, &[parent])?;
+
    let commit = repo.find_commit(oid).unwrap();
+

+
    Ok(commit)
+
}
+

+
/// Push the refs to the radicle remote.
+
pub fn push(repo: &git2::Repository) -> Result<(), git2::Error> {
+
    let mut remote = repo.find_remote("rad")?;
+
    let refspecs = remote.push_refspecs().unwrap();
+
    let refspec = refspecs.into_iter().next().unwrap().unwrap();
+

+
    // The `git2` crate doesn't seem to support push refspecs with '*' in them,
+
    // so we manually replace it with the current branch.
+
    let head = repo.head().unwrap();
+
    let branch = head.shorthand().unwrap();
+
    let refspec = refspec.replace('*', branch);
+

+
    remote.push::<&str>(&[&refspec], None)
+
}
+

+
/// Get the repository head.
+
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
+
    let head = repo.head()?.peel_to_commit()?;
+

+
    Ok(head)
+
}
+

+
/// Write a tree with the given blob at the given path.
+
pub fn write_tree<'r>(
+
    path: &Path,
+
    bytes: &[u8],
+
    repo: &'r git2::Repository,
+
) -> Result<git2::Tree<'r>, Error> {
+
    let blob_id = repo.blob(bytes)?;
+
    let mut builder = repo.treebuilder(None)?;
+
    builder.insert(path, blob_id, 0o100_644)?;
+

+
    let tree_id = builder.write()?;
+
    let tree = repo.find_tree(tree_id)?;
+

+
    Ok(tree)
+
}
+

+
/// Configure a repository's radicle remote.
+
///
+
/// Takes the repository in which to configure the remote, the name of the remote, the public
+
/// key of the remote, and the path to the remote repository on the filesystem.
+
pub fn configure_remote<'r>(
+
    repo: &'r git2::Repository,
+
    remote_name: &str,
+
    remote_id: &RemoteId,
+
    remote_path: &Path,
+
) -> Result<git2::Remote<'r>, git2::Error> {
+
    let url = Url {
+
        scheme: git_url::Scheme::File,
+
        path: remote_path.to_string_lossy().to_string().into(),
+

+
        ..Url::default()
+
    };
+
    let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*");
+
    let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*");
+
    let remote = repo.remote_with_fetch(remote_name, url.to_string().as_str(), &fetch)?;
+
    repo.remote_add_push(remote_name, &push)?;
+

+
    Ok(remote)
+
}
+

+
/// Set the upstream of the given branch to the given remote.
+
///
+
/// This writes to the `config` directly. The entry will look like the
+
/// following:
+
///
+
/// ```text
+
/// [branch "main"]
+
///     remote = rad
+
///     merge = refs/heads/main
+
/// ```
+
pub fn set_upstream(
+
    repo: &git2::Repository,
+
    remote: &str,
+
    branch: &str,
+
    merge: &str,
+
) -> Result<(), git2::Error> {
+
    let mut config = repo.config()?;
+
    let branch_remote = format!("branch.{}.remote", branch);
+
    let branch_merge = format!("branch.{}.merge", branch);
+

+
    config.remove_multivar(&branch_remote, ".*").or_else(|e| {
+
        if ext::is_not_found_err(&e) {
+
            Ok(())
+
        } else {
+
            Err(e)
+
        }
+
    })?;
+
    config.remove_multivar(&branch_merge, ".*").or_else(|e| {
+
        if ext::is_not_found_err(&e) {
+
            Ok(())
+
        } else {
+
            Err(e)
+
        }
+
    })?;
+
    config.set_multivar(&branch_remote, ".*", remote)?;
+
    config.set_multivar(&branch_merge, ".*", merge)?;
+

+
    Ok(())
+
}
added radicle/src/hash.rs
@@ -0,0 +1,69 @@
+
use std::{convert::TryInto, fmt};
+

+
use serde::{Deserialize, Serialize};
+
use sha2::{
+
    digest::{generic_array::GenericArray, OutputSizeUser},
+
    Digest as _, Sha256,
+
};
+
use thiserror::Error;
+

+
#[derive(Debug, Clone, PartialEq, Eq, Error)]
+
pub enum DecodeError {
+
    #[error("invalid digest length {0}")]
+
    InvalidLength(usize),
+
}
+

+
/// A SHA-256 hash.
+
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Digest([u8; 32]);
+

+
impl Digest {
+
    pub fn new(bytes: impl AsRef<[u8]>) -> Self {
+
        Self::from(Sha256::digest(bytes))
+
    }
+
}
+

+
impl AsRef<[u8; 32]> for Digest {
+
    fn as_ref(&self) -> &[u8; 32] {
+
        &self.0
+
    }
+
}
+

+
impl fmt::Debug for Digest {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Hash({})", self)
+
    }
+
}
+

+
impl fmt::Display for Digest {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        for byte in &self.0 {
+
            write!(f, "{:02x}", byte)?;
+
        }
+
        Ok(())
+
    }
+
}
+

+
impl From<[u8; 32]> for Digest {
+
    fn from(bytes: [u8; 32]) -> Self {
+
        Self(bytes)
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Digest {
+
    type Error = DecodeError;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, DecodeError> {
+
        let bytes: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|_| DecodeError::InvalidLength(bytes.len()))?;
+

+
        Ok(bytes.into())
+
    }
+
}
+

+
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
+
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
+
        Self(array.into())
+
    }
+
}
added radicle/src/identity.rs
@@ -0,0 +1,246 @@
+
pub mod doc;
+

+
use std::ops::Deref;
+
use std::path::PathBuf;
+
use std::{ffi::OsString, fmt, str::FromStr};
+

+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::crypto;
+
use crate::crypto::Verified;
+
use crate::git;
+
use crate::serde_ext;
+
use crate::storage::Remotes;
+

+
pub use crypto::PublicKey;
+
pub use doc::{Delegate, Doc};
+

+
#[derive(Error, Debug)]
+
pub enum IdError {
+
    #[error("invalid git object id: {0}")]
+
    InvalidOid(#[from] git2::Error),
+
    #[error(transparent)]
+
    Multibase(#[from] multibase::Error),
+
}
+

+
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Id(git::Oid);
+

+
impl fmt::Display for Id {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        f.write_str(&self.to_human())
+
    }
+
}
+

+
impl fmt::Debug for Id {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Id({})", self)
+
    }
+
}
+

+
impl Id {
+
    pub fn to_human(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, self.0.as_bytes())
+
    }
+

+
    pub fn from_human(s: &str) -> Result<Self, IdError> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let array: git::Oid = bytes.as_slice().try_into()?;
+

+
        Ok(Self(array))
+
    }
+
}
+

+
impl FromStr for Id {
+
    type Err = IdError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::from_human(s)
+
    }
+
}
+

+
impl TryFrom<OsString> for Id {
+
    type Error = IdError;
+

+
    fn try_from(value: OsString) -> Result<Self, Self::Error> {
+
        let string = value.to_string_lossy();
+
        Self::from_str(&string)
+
    }
+
}
+

+
impl From<git::Oid> for Id {
+
    fn from(oid: git::Oid) -> Self {
+
        Self(oid)
+
    }
+
}
+

+
impl From<git2::Oid> for Id {
+
    fn from(oid: git2::Oid) -> Self {
+
        Self(oid.into())
+
    }
+
}
+

+
impl Deref for Id {
+
    type Target = git::Oid;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
impl serde::Serialize for Id {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: serde::Serializer,
+
    {
+
        serde_ext::string::serialize(self, serializer)
+
    }
+
}
+

+
impl<'de> serde::Deserialize<'de> for Id {
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: serde::Deserializer<'de>,
+
    {
+
        serde_ext::string::deserialize(deserializer)
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum DidError {
+
    #[error("invalid did: {0}")]
+
    Did(String),
+
    #[error("invalid public key: {0}")]
+
    PublicKey(#[from] crypto::PublicKeyError),
+
}
+

+
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
+
#[serde(into = "String", try_from = "String")]
+
pub struct Did(crypto::PublicKey);
+

+
impl Did {
+
    pub fn encode(&self) -> String {
+
        format!("did:key:{}", self.0.to_human())
+
    }
+

+
    pub fn decode(input: &str) -> Result<Self, DidError> {
+
        let key = input
+
            .strip_prefix("did:key:")
+
            .ok_or_else(|| DidError::Did(input.to_owned()))?;
+

+
        crypto::PublicKey::from_str(key)
+
            .map(Did)
+
            .map_err(DidError::from)
+
    }
+
}
+

+
impl From<crypto::PublicKey> for Did {
+
    fn from(key: crypto::PublicKey) -> Self {
+
        Self(key)
+
    }
+
}
+

+
impl From<Did> for String {
+
    fn from(other: Did) -> Self {
+
        other.encode()
+
    }
+
}
+

+
impl TryFrom<String> for Did {
+
    type Error = DidError;
+

+
    fn try_from(value: String) -> Result<Self, Self::Error> {
+
        Self::decode(&value)
+
    }
+
}
+

+
impl fmt::Display for Did {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", self.encode())
+
    }
+
}
+

+
impl fmt::Debug for Did {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Did({:?})", self.to_string())
+
    }
+
}
+

+
impl Deref for Did {
+
    type Target = PublicKey;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.0
+
    }
+
}
+

+
/// A stored and verified project.
+
#[derive(Debug, Clone)]
+
pub struct Project {
+
    /// The project identifier.
+
    pub id: Id,
+
    /// The latest project identity document.
+
    pub doc: Doc<Verified>,
+
    /// The project remotes.
+
    pub remotes: Remotes<Verified>,
+
    /// On-disk file path for this project's repository.
+
    pub path: PathBuf,
+
}
+

+
impl Project {
+
    pub fn delegate(&mut self, name: String, key: crypto::PublicKey) -> bool {
+
        self.doc.delegate(Delegate {
+
            name,
+
            id: Did::from(key),
+
        })
+
    }
+
}
+

+
impl Deref for Project {
+
    type Target = Doc<Verified>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.doc
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+
    use crate::crypto::PublicKey;
+
    use quickcheck_macros::quickcheck;
+
    use std::collections::HashSet;
+

+
    #[quickcheck]
+
    fn prop_key_equality(a: PublicKey, b: PublicKey) {
+
        assert_ne!(a, b);
+

+
        let mut hm = HashSet::new();
+

+
        assert!(hm.insert(a));
+
        assert!(hm.insert(b));
+
        assert!(!hm.insert(a));
+
        assert!(!hm.insert(b));
+
    }
+

+
    #[quickcheck]
+
    fn prop_from_str(input: Id) {
+
        let encoded = input.to_string();
+
        let decoded = Id::from_str(&encoded).unwrap();
+

+
        assert_eq!(input, decoded);
+
    }
+

+
    #[quickcheck]
+
    fn prop_json_eq_str(pk: PublicKey, proj: Id, did: Did) {
+
        let json = serde_json::to_string(&pk).unwrap();
+
        assert_eq!(format!("\"{}\"", pk), json);
+

+
        let json = serde_json::to_string(&proj).unwrap();
+
        assert_eq!(format!("\"{}\"", proj), json);
+

+
        let json = serde_json::to_string(&did).unwrap();
+
        assert_eq!(format!("\"{}\"", did), json);
+
    }
+
}
added radicle/src/identity/doc.rs
@@ -0,0 +1,593 @@
+
use std::collections::{BTreeMap, HashMap};
+
use std::fmt::Write as _;
+
use std::io;
+
use std::marker::PhantomData;
+
use std::ops::Deref;
+
use std::path::Path;
+

+
use nonempty::NonEmpty;
+
use once_cell::sync::Lazy;
+
use radicle_git_ext::Oid;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::crypto;
+
use crate::crypto::{Signature, Unverified, Verified};
+
use crate::git;
+
use crate::identity::{Did, Id};
+
use crate::storage::git::trailers;
+
use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};
+

+
pub use crypto::PublicKey;
+

+
/// Untrusted, well-formed input.
+
#[derive(Clone, Copy, Debug)]
+
pub struct Untrusted;
+
/// Signed by quorum of the previous delegation.
+
#[derive(Clone, Copy, Debug)]
+
pub struct Trusted;
+

+
pub static PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
+

+
pub const MAX_STRING_LENGTH: usize = 255;
+
pub const MAX_DELEGATES: usize = 255;
+

+
#[derive(Error, Debug)]
+
pub enum Error {
+
    #[error("json: {0}")]
+
    Json(#[from] serde_json::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("verification: {0}")]
+
    Verification(#[from] VerificationError),
+
    #[error("git: {0}")]
+
    Git(#[from] git::Error),
+
    #[error("git: {0}")]
+
    RawGit(#[from] git2::Error),
+
}
+

+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+
pub struct Delegate {
+
    pub name: String,
+
    pub id: Did,
+
}
+

+
impl Delegate {
+
    fn matches(&self, key: &PublicKey) -> bool {
+
        &self.id.0 == key
+
    }
+
}
+

+
impl From<Delegate> for PublicKey {
+
    fn from(delegate: Delegate) -> Self {
+
        delegate.id.0
+
    }
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "kebab-case")]
+
pub struct Payload {
+
    pub name: String,
+
    pub description: String,    // TODO: Make optional.
+
    pub default_branch: String, // TODO: Make optional.
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(transparent)]
+
// TODO: Restrict values.
+
pub struct Namespace(String);
+

+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Doc<V> {
+
    #[serde(rename = "xyz.radicle.project")]
+
    pub payload: Payload,
+
    #[serde(flatten)]
+
    pub extensions: BTreeMap<Namespace, serde_json::Value>,
+
    pub delegates: NonEmpty<Delegate>,
+
    pub threshold: usize,
+

+
    verified: PhantomData<V>,
+
}
+

+
impl Doc<Verified> {
+
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
+
        let mut buf = Vec::new();
+
        let mut serializer =
+
            serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
+

+
        self.serialize(&mut serializer)?;
+
        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
+

+
        Ok((oid.into(), buf))
+
    }
+

+
    /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
+
    pub fn delegate(&mut self, delegate: Delegate) -> bool {
+
        if self.delegates.iter().all(|d| d.id != delegate.id) {
+
            self.delegates.push(delegate);
+
            return true;
+
        }
+
        false
+
    }
+

+
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
+
        let (oid, bytes) = self.encode()?;
+
        let sig = signer.sign(&bytes);
+

+
        Ok((oid, sig))
+
    }
+

+
    pub fn create<'r, S: WriteStorage<'r>>(
+
        &self,
+
        remote: &RemoteId,
+
        msg: &str,
+
        storage: &'r S,
+
    ) -> Result<(Id, git::Oid, S::Repository), Error> {
+
        // You can checkout this branch in your working copy with:
+
        //
+
        //      git fetch rad
+
        //      git checkout -b radicle/id remotes/rad/radicle/id
+
        //
+
        let (doc_oid, doc) = self.encode()?;
+
        let id = Id::from(doc_oid);
+
        let repo = storage.repository(&id).unwrap();
+
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
+
        let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?;
+

+
        drop(tree);
+

+
        Ok((id, oid, repo))
+
    }
+

+
    pub fn update<'r, R: WriteRepository<'r>>(
+
        &self,
+
        remote: &RemoteId,
+
        msg: &str,
+
        signatures: &[(&PublicKey, Signature)],
+
        repo: &R,
+
    ) -> Result<git::Oid, Error> {
+
        let mut msg = format!("{msg}\n\n");
+
        for (key, sig) in signatures {
+
            writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
+
                .expect("in-memory writes don't fail");
+
        }
+

+
        let (_, doc) = self.encode()?;
+
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
+
        let id_ref = git::refs::storage::id(remote);
+
        let head = repo.raw().find_reference(&id_ref)?.peel_to_commit()?;
+
        let oid = Doc::commit(remote, &tree, &msg, &[&head], repo.raw())?;
+

+
        Ok(oid)
+
    }
+

+
    fn commit(
+
        remote: &RemoteId,
+
        tree: &git2::Tree,
+
        msg: &str,
+
        parents: &[&git2::Commit],
+
        repo: &git2::Repository,
+
    ) -> Result<git::Oid, Error> {
+
        let sig = repo
+
            .signature()
+
            .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
+

+
        let id_ref = git::refs::storage::id(remote);
+
        let oid = repo.commit(Some(&id_ref), &sig, &sig, msg, tree, parents)?;
+

+
        Ok(oid.into())
+
    }
+
}
+

+
impl<V> Deref for Doc<V> {
+
    type Target = Payload;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.payload
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum VerificationError {
+
    #[error("invalid name: {0}")]
+
    Name(&'static str),
+
    #[error("invalid description: {0}")]
+
    Description(&'static str),
+
    #[error("invalid default branch: {0}")]
+
    DefaultBranch(&'static str),
+
    #[error("invalid delegates: {0}")]
+
    Delegates(&'static str),
+
    #[error("invalid version `{0}`")]
+
    Version(u32),
+
    #[error("invalid parent: {0}")]
+
    Parent(&'static str),
+
    #[error("invalid threshold `{0}`: {1}")]
+
    Threshold(usize, &'static str),
+
}
+

+
impl Doc<Unverified> {
+
    pub fn initial(
+
        name: String,
+
        description: String,
+
        default_branch: BranchName,
+
        delegate: Delegate,
+
    ) -> Self {
+
        Self {
+
            payload: Payload {
+
                name,
+
                description,
+
                default_branch,
+
            },
+
            extensions: BTreeMap::new(),
+
            delegates: NonEmpty::new(delegate),
+
            threshold: 1,
+
            verified: PhantomData,
+
        }
+
    }
+

+
    pub fn new(
+
        name: String,
+
        description: String,
+
        default_branch: BranchName,
+
        delegates: NonEmpty<Delegate>,
+
        threshold: usize,
+
    ) -> Self {
+
        Self {
+
            payload: Payload {
+
                name,
+
                description,
+
                default_branch,
+
            },
+
            extensions: BTreeMap::new(),
+
            delegates,
+
            threshold,
+
            verified: PhantomData,
+
        }
+
    }
+

+
    pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
+
        serde_json::from_slice(bytes)
+
    }
+

+
    pub fn verified(self) -> Result<Doc<Verified>, VerificationError> {
+
        if self.name.is_empty() {
+
            return Err(VerificationError::Name("name cannot be empty"));
+
        }
+
        if self.name.len() > MAX_STRING_LENGTH {
+
            return Err(VerificationError::Name("name cannot exceed 255 bytes"));
+
        }
+
        if self.description.len() > MAX_STRING_LENGTH {
+
            return Err(VerificationError::Description(
+
                "description cannot exceed 255 bytes",
+
            ));
+
        }
+
        if self.delegates.len() > MAX_DELEGATES {
+
            return Err(VerificationError::Delegates(
+
                "number of delegates cannot exceed 255",
+
            ));
+
        }
+
        if self
+
            .delegates
+
            .iter()
+
            .any(|d| d.name.is_empty() || d.name.len() > MAX_STRING_LENGTH)
+
        {
+
            return Err(VerificationError::Delegates(
+
                "delegate name must not be empty and must not exceed 255 bytes",
+
            ));
+
        }
+
        if self.delegates.is_empty() {
+
            return Err(VerificationError::Delegates(
+
                "delegate list cannot be empty",
+
            ));
+
        }
+
        if self.default_branch.is_empty() {
+
            return Err(VerificationError::DefaultBranch(
+
                "default branch cannot be empty",
+
            ));
+
        }
+
        if self.default_branch.len() > MAX_STRING_LENGTH {
+
            return Err(VerificationError::DefaultBranch(
+
                "default branch cannot exceed 255 bytes",
+
            ));
+
        }
+
        if self.threshold > self.delegates.len() {
+
            return Err(VerificationError::Threshold(
+
                self.threshold,
+
                "threshold cannot exceed number of delegates",
+
            ));
+
        }
+
        if self.threshold == 0 {
+
            return Err(VerificationError::Threshold(
+
                self.threshold,
+
                "threshold cannot be zero",
+
            ));
+
        }
+

+
        Ok(Doc {
+
            payload: self.payload,
+
            extensions: self.extensions,
+
            delegates: self.delegates,
+
            threshold: self.threshold,
+
            verified: PhantomData,
+
        })
+
    }
+

+
    pub fn blob_at<'r, R: ReadRepository<'r>>(
+
        commit: Oid,
+
        repo: &R,
+
    ) -> Result<Option<git2::Blob>, git::Error> {
+
        match repo.blob_at(commit, Path::new(&*PATH)) {
+
            Err(git::ext::Error::NotFound(_)) => Ok(None),
+
            Err(e) => Err(e),
+
            Ok(blob) => Ok(Some(blob)),
+
        }
+
    }
+

+
    pub fn load_at<'r, R: ReadRepository<'r>>(
+
        commit: Oid,
+
        repo: &R,
+
    ) -> Result<Option<(Self, Oid)>, git::Error> {
+
        if let Some(blob) = Self::blob_at(commit, repo)? {
+
            let doc = Doc::from_json(blob.content()).unwrap();
+
            return Ok(Some((doc, blob.id().into())));
+
        }
+
        Ok(None)
+
    }
+

+
    pub fn load<'r, R: ReadRepository<'r>>(
+
        remote: &RemoteId,
+
        repo: &R,
+
    ) -> Result<Option<(Self, Oid)>, git::Error> {
+
        if let Some(oid) = Self::head(remote, repo)? {
+
            Self::load_at(oid, repo)
+
        } else {
+
            Ok(None)
+
        }
+
    }
+
}
+

+
impl<V> Doc<V> {
+
    pub fn head<'r, R: ReadRepository<'r>>(
+
        remote: &RemoteId,
+
        repo: &R,
+
    ) -> Result<Option<Oid>, git::Error> {
+
        let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
+
        if let Some(oid) = repo.reference_oid(remote, head)? {
+
            Ok(Some(oid))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum IdentityError {
+
    #[error("git: {0}")]
+
    GitRaw(#[from] git2::Error),
+
    #[error("git: {0}")]
+
    Git(#[from] git::Error),
+
    #[error("verification: {0}")]
+
    Verification(#[from] VerificationError),
+
    #[error("root hash `{0}` does not match project")]
+
    MismatchedRoot(Oid),
+
    #[error("commit signature for {0} is invalid: {1}")]
+
    InvalidSignature(PublicKey, crypto::Error),
+
    #[error("quorum not reached: {0} signatures for a threshold of {1}")]
+
    QuorumNotReached(usize, usize),
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub struct Identity<I> {
+
    /// The head of the identity branch. This points to a commit that
+
    /// contains the current document blob.
+
    pub head: Oid,
+
    /// The canonical identifier for this identity.
+
    /// This is the object id of the initial document blob.
+
    pub root: I,
+
    /// The object id of the current document blob.
+
    pub current: Oid,
+
    /// Revision number. The initial document has a revision of `0`.
+
    pub revision: u32,
+
    /// The current document.
+
    pub doc: Doc<Verified>,
+
    /// Signatures over this identity.
+
    pub signatures: HashMap<PublicKey, Signature>,
+
}
+

+
impl Identity<Oid> {
+
    pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
+
        // The root hash must be equal to the id.
+
        if self.root != *id {
+
            return Err(IdentityError::MismatchedRoot(self.root));
+
        }
+

+
        Ok(Identity {
+
            root: id,
+
            head: self.head,
+
            current: self.current,
+
            revision: self.revision,
+
            doc: self.doc,
+
            signatures: self.signatures,
+
        })
+
    }
+
}
+

+
impl Identity<Untrusted> {
+
    pub fn load<'r, R: ReadRepository<'r>>(
+
        remote: &RemoteId,
+
        repo: &R,
+
    ) -> Result<Option<Identity<Oid>>, IdentityError> {
+
        if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
+
            let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
+

+
            // Retrieve root document.
+
            let root_oid = history.pop().unwrap()?.into();
+
            let root_blob = Doc::blob_at(root_oid, repo)?.unwrap();
+
            let root: git::Oid = root_blob.id().into();
+
            let trusted = Doc::from_json(root_blob.content()).unwrap();
+
            let revision = history.len() as u32;
+

+
            let mut trusted = trusted.verified()?;
+
            let mut current = root;
+
            let mut signatures = Vec::new();
+

+
            // Traverse the history chronologically.
+
            for oid in history.into_iter().rev() {
+
                let oid = oid?;
+
                let blob = Doc::blob_at(oid.into(), repo)?.unwrap();
+
                let untrusted = Doc::from_json(blob.content()).unwrap();
+
                let untrusted = untrusted.verified()?;
+
                let commit = repo.commit(oid.into())?.unwrap();
+
                let msg = commit.message_raw().unwrap();
+

+
                // Keys that signed the *current* document version.
+
                signatures = trailers::parse_signatures(msg).unwrap();
+
                for (pk, sig) in &signatures {
+
                    if let Err(err) = pk.verify(blob.content(), sig) {
+
                        return Err(IdentityError::InvalidSignature(*pk, err));
+
                    }
+
                }
+

+
                // Check that enough delegates signed this next version.
+
                let quorum = signatures
+
                    .iter()
+
                    .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
+
                    .count();
+
                if quorum < trusted.threshold {
+
                    return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
+
                }
+

+
                trusted = untrusted;
+
                current = blob.id().into();
+
            }
+

+
            return Ok(Some(Identity {
+
                root,
+
                head,
+
                current,
+
                revision,
+
                doc: trusted,
+
                signatures: signatures.into_iter().collect(),
+
            }));
+
        }
+
        Ok(None)
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use crate::crypto::Signer;
+
    use crate::rad;
+
    use crate::storage::git::Storage;
+
    use crate::storage::{ReadStorage, WriteStorage};
+
    use crate::test::fixtures;
+
    use crate::test::signer::MockSigner;
+

+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    #[test]
+
    fn test_valid_identity() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let mut rng = fastrand::Rng::new();
+

+
        let alice = MockSigner::new(&mut rng);
+
        let bob = MockSigner::new(&mut rng);
+
        let eve = MockSigner::new(&mut rng);
+

+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (id, _, _, _) =
+
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
+

+
        // Bob and Eve fork the project from Alice.
+
        rad::fork(&id, alice.public_key(), &bob, &storage).unwrap();
+
        rad::fork(&id, alice.public_key(), &eve, &storage).unwrap();
+

+
        // TODO: In some cases we want to get the repo and the project, but don't
+
        // want to have to create a repository object twice. Perhaps there should
+
        // be a way of getting a project from a repo.
+
        let mut proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
+
        let repo = storage.repository(&id).unwrap();
+

+
        // Make a change to the description and sign it.
+
        proj.doc.payload.description += "!";
+
        proj.sign(&alice)
+
            .and_then(|(_, sig)| {
+
                proj.update(
+
                    alice.public_key(),
+
                    "Update description",
+
                    &[(alice.public_key(), sig)],
+
                    &repo,
+
                )
+
            })
+
            .unwrap();
+

+
        // Add Bob as a delegate, and sign it.
+
        proj.delegate("bob".to_owned(), *bob.public_key());
+
        proj.doc.threshold = 2;
+
        proj.sign(&alice)
+
            .and_then(|(_, sig)| {
+
                proj.update(
+
                    alice.public_key(),
+
                    "Add bob",
+
                    &[(alice.public_key(), sig)],
+
                    &repo,
+
                )
+
            })
+
            .unwrap();
+

+
        // Add Eve as a delegate, and sign it.
+
        proj.delegate("eve".to_owned(), *eve.public_key());
+
        proj.sign(&alice)
+
            .and_then(|(_, alice_sig)| {
+
                proj.sign(&bob).and_then(|(_, bob_sig)| {
+
                    proj.update(
+
                        alice.public_key(),
+
                        "Add eve",
+
                        &[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)],
+
                        &repo,
+
                    )
+
                })
+
            })
+
            .unwrap();
+

+
        // Update description again with signatures by Eve and Bob.
+
        proj.doc.payload.description += "?";
+
        let (current, head) = proj
+
            .sign(&bob)
+
            .and_then(|(_, bob_sig)| {
+
                proj.sign(&eve).and_then(|(blob_id, eve_sig)| {
+
                    proj.update(
+
                        alice.public_key(),
+
                        "Update description",
+
                        &[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)],
+
                        &repo,
+
                    )
+
                    .map(|head| (blob_id, head))
+
                })
+
            })
+
            .unwrap();
+

+
        let identity: Identity<Id> = Identity::load(alice.public_key(), &repo)
+
            .unwrap()
+
            .unwrap()
+
            .verified(id.clone())
+
            .unwrap();
+

+
        assert_eq!(identity.signatures.len(), 2);
+
        assert_eq!(identity.revision, 4);
+
        assert_eq!(identity.root, id);
+
        assert_eq!(identity.current, current);
+
        assert_eq!(identity.head, head);
+
        assert_eq!(identity.doc, proj.doc);
+

+
        let proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
+
        assert_eq!(proj.description, "Acme's repository!?");
+
    }
+

+
    #[quickcheck]
+
    fn prop_encode_decode(doc: Doc<Verified>) {
+
        let (_, bytes) = doc.encode().unwrap();
+
        assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
+
    }
+
}
added radicle/src/lib.rs
@@ -0,0 +1,10 @@
+
pub mod collections;
+
pub mod crypto;
+
pub mod git;
+
pub mod hash;
+
pub mod identity;
+
pub mod rad;
+
pub mod serde_ext;
+
pub mod storage;
+
#[cfg(feature = "test")]
+
pub mod test;
added radicle/src/rad.rs
@@ -0,0 +1,329 @@
+
use std::io;
+
use std::path::Path;
+

+
use thiserror::Error;
+

+
use crate::crypto::{Signer, Verified};
+
use crate::git;
+
use crate::identity::Id;
+
use crate::storage::refs::SignedRefs;
+
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
+
use crate::{identity, storage};
+

+
pub const REMOTE_NAME: &str = "rad";
+

+
#[derive(Error, Debug)]
+
pub enum InitError {
+
    #[error("doc: {0}")]
+
    Doc(#[from] identity::doc::Error),
+
    #[error("doc: {0}")]
+
    DocVerification(#[from] identity::doc::VerificationError),
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("storage: {0}")]
+
    Storage(#[from] storage::Error),
+
    #[error("cannot initialize project inside a bare repository")]
+
    BareRepo,
+
    #[error("cannot initialize project from detached head state")]
+
    DetachedHead,
+
    #[error("HEAD reference is not valid UTF-8")]
+
    InvalidHead,
+
}
+

+
/// Initialize a new radicle project from a git repository.
+
pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
+
    repo: &git2::Repository,
+
    name: &str,
+
    description: &str,
+
    default_branch: BranchName,
+
    signer: G,
+
    storage: &'r S,
+
) -> Result<(Id, SignedRefs<Verified>), InitError> {
+
    let pk = signer.public_key();
+
    let delegate = identity::Delegate {
+
        // TODO: Use actual user name.
+
        name: String::from("anonymous"),
+
        id: identity::Did::from(*pk),
+
    };
+
    let doc = identity::Doc::initial(
+
        name.to_owned(),
+
        description.to_owned(),
+
        default_branch.clone(),
+
        delegate,
+
    )
+
    .verified()?;
+

+
    let (id, _, project) = doc.create(pk, "Initialize Radicle", storage)?;
+

+
    git::set_upstream(
+
        repo,
+
        REMOTE_NAME,
+
        &default_branch,
+
        &git::refs::storage::branch(pk, &default_branch),
+
    )?;
+

+
    // TODO: Note that you'll likely want to use `RemoteCallbacks` and set
+
    // `push_update_reference` to test whether all the references were pushed
+
    // successfully.
+
    git::configure_remote(repo, REMOTE_NAME, pk, project.path())?.push::<&str>(
+
        &[&format!(
+
            "{}:{}",
+
            &git::refs::workdir::branch(&default_branch),
+
            &git::refs::storage::branch(pk, &default_branch),
+
        )],
+
        None,
+
    )?;
+
    let signed = storage.sign_refs(&project, signer)?;
+

+
    Ok((id, signed))
+
}
+

+
#[derive(Error, Debug)]
+
pub enum ForkError {
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("storage: {0}")]
+
    Storage(#[from] storage::Error),
+
    #[error("project `{0}` was not found in storage")]
+
    NotFound(Id),
+
    #[error("git: invalid reference")]
+
    InvalidReference,
+
}
+

+
/// Create a local tree for an existing project, from an existing remote.
+
pub fn fork<'r, G: Signer, S: storage::WriteStorage<'r>>(
+
    proj: &Id,
+
    remote: &RemoteId,
+
    signer: G,
+
    storage: S,
+
) -> Result<(), ForkError> {
+
    // TODO: Copy tags over?
+

+
    // Creates or copies the following references:
+
    //
+
    // refs/remotes/<pk>/heads/master
+
    // refs/remotes/<pk>/heads/radicle/id
+
    // refs/remotes/<pk>/tags/*
+
    // refs/remotes/<pk>/rad/signature
+

+
    let me = signer.public_key();
+
    let project = storage
+
        .get(remote, proj)?
+
        .ok_or_else(|| ForkError::NotFound(proj.clone()))?;
+
    let repository = storage.repository(proj)?;
+

+
    let raw = repository.raw();
+
    let remote_head = raw
+
        .find_reference(&git::refs::storage::branch(
+
            remote,
+
            &project.doc.default_branch,
+
        ))?
+
        .target()
+
        .ok_or(ForkError::InvalidReference)?;
+
    raw.reference(
+
        &git::refs::storage::branch(me, &project.doc.default_branch),
+
        remote_head,
+
        false,
+
        &format!("creating default branch for {me}"),
+
    )?;
+

+
    let remote_id = raw
+
        .find_reference(&git::refs::storage::id(remote))?
+
        .target()
+
        .ok_or(ForkError::InvalidReference)?;
+
    raw.reference(
+
        &git::refs::storage::id(me),
+
        remote_id,
+
        false,
+
        &format!("creating identity branch for {me}"),
+
    )?;
+

+
    storage.sign_refs(&repository, &signer)?;
+

+
    Ok(())
+
}
+

+
#[derive(Error, Debug)]
+
pub enum CheckoutError {
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("storage: {0}")]
+
    Storage(#[from] storage::Error),
+
    #[error("project `{0}` was not found in storage")]
+
    NotFound(Id),
+
}
+

+
/// Checkout a project from storage as a working copy.
+
/// This effectively does a `git-clone` from storage.
+
pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
+
    proj: &Id,
+
    remote: &RemoteId,
+
    path: P,
+
    storage: S,
+
) -> Result<git2::Repository, CheckoutError> {
+
    // TODO: Decide on whether we can use `clone_local`
+
    // TODO: Look into sharing object databases.
+
    let project = storage
+
        .get(remote, proj)?
+
        .ok_or_else(|| CheckoutError::NotFound(proj.clone()))?;
+

+
    let mut opts = git2::RepositoryInitOptions::new();
+
    opts.no_reinit(true).description(&project.doc.description);
+

+
    let repo = git2::Repository::init_opts(path, &opts)?;
+
    let default_branch = project.doc.default_branch.as_str();
+

+
    // Configure and fetch all refs from remote.
+
    git::configure_remote(&repo, REMOTE_NAME, remote, &project.path)?.fetch::<&str>(
+
        &[],
+
        None,
+
        None,
+
    )?;
+

+
    {
+
        // Setup default branch.
+
        let remote_head_ref = git::refs::workdir::remote_branch(REMOTE_NAME, default_branch);
+
        let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
+
        let _ = repo.branch(default_branch, &remote_head_commit, true)?;
+

+
        // Setup remote tracking for default branch.
+
        git::set_upstream(
+
            &repo,
+
            REMOTE_NAME,
+
            default_branch,
+
            &git::refs::storage::branch(remote, default_branch),
+
        )?;
+
    }
+

+
    Ok(repo)
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use crate::git::fmt::refname;
+
    use crate::identity::{Delegate, Did};
+
    use crate::storage::git::Storage;
+
    use crate::storage::{ReadStorage, WriteStorage};
+
    use crate::test::{fixtures, signer::MockSigner};
+

+
    #[test]
+
    fn test_init() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let signer = MockSigner::default();
+
        let public_key = *signer.public_key();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
+

+
        let (proj, refs) = init(
+
            &repo,
+
            "acme",
+
            "Acme's repo",
+
            BranchName::from("master"),
+
            &signer,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        let project = storage.get(&public_key, &proj).unwrap().unwrap();
+

+
        assert_eq!(project.remotes[&public_key].refs, refs);
+
        assert_eq!(project.id, proj);
+
        assert_eq!(project.doc.name, "acme");
+
        assert_eq!(project.doc.description, "Acme's repo");
+
        assert_eq!(project.doc.default_branch, BranchName::from("master"));
+
        assert_eq!(
+
            project.doc.delegates.first(),
+
            &Delegate {
+
                name: String::from("anonymous"),
+
                id: Did::from(public_key),
+
            }
+
        );
+
    }
+

+
    #[test]
+
    fn test_fork() {
+
        let mut rng = fastrand::Rng::new();
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let alice = MockSigner::new(&mut rng);
+
        let alice_id = alice.public_key();
+
        let bob = MockSigner::new(&mut rng);
+
        let bob_id = bob.public_key();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
+

+
        // Alice creates a project.
+
        let (id, alice_refs) = init(
+
            &original,
+
            "acme",
+
            "Acme's repo",
+
            BranchName::from("master"),
+
            &alice,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        // Bob forks it and creates a checkout.
+
        fork(&id, alice_id, &bob, &storage).unwrap();
+
        checkout(&id, bob_id, tempdir.path().join("copy"), &storage).unwrap();
+

+
        let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap();
+

+
        assert_eq!(
+
            bob_remote.refs.get(&refname!("master")),
+
            alice_refs.get(&refname!("master"))
+
        );
+
    }
+

+
    #[test]
+
    fn test_checkout() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let signer = MockSigner::default();
+
        let remote_id = signer.public_key();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
+

+
        let (id, _) = init(
+
            &original,
+
            "acme",
+
            "Acme's repo",
+
            BranchName::from("master"),
+
            &signer,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        let copy = checkout(&id, remote_id, tempdir.path().join("copy"), &storage).unwrap();
+

+
        assert_eq!(
+
            copy.head().unwrap().target(),
+
            original.head().unwrap().target()
+
        );
+
        assert_eq!(
+
            copy.branch_upstream_name("refs/heads/master")
+
                .unwrap()
+
                .to_vec(),
+
            original
+
                .branch_upstream_name("refs/heads/master")
+
                .unwrap()
+
                .to_vec()
+
        );
+
        assert_eq!(
+
            copy.find_remote(REMOTE_NAME)
+
                .unwrap()
+
                .refspecs()
+
                .into_iter()
+
                .map(|r| r.bytes().to_vec())
+
                .collect::<Vec<_>>(),
+
            original
+
                .find_remote(REMOTE_NAME)
+
                .unwrap()
+
                .refspecs()
+
                .into_iter()
+
                .map(|r| r.bytes().to_vec())
+
                .collect::<Vec<_>>(),
+
        );
+
    }
+
}
added radicle/src/serde_ext.rs
@@ -0,0 +1,25 @@
+
pub mod string {
+
    use std::fmt::Display;
+
    use std::str::FromStr;
+

+
    use serde::{de, Deserialize, Deserializer, Serializer};
+

+
    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        T: Display,
+
        S: Serializer,
+
    {
+
        serializer.collect_str(value)
+
    }
+

+
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
+
    where
+
        T: FromStr,
+
        T::Err: Display,
+
        D: Deserializer<'de>,
+
    {
+
        String::deserialize(deserializer)?
+
            .parse()
+
            .map_err(de::Error::custom)
+
    }
+
}
added radicle/src/storage.rs
@@ -0,0 +1,306 @@
+
pub mod git;
+
pub mod refs;
+

+
use std::collections::hash_map;
+
use std::marker::PhantomData;
+
use std::ops::Deref;
+
use std::path::Path;
+
use std::{fmt, io};
+

+
use thiserror::Error;
+

+
pub use radicle_git_ext::Oid;
+

+
use crate::collections::HashMap;
+
use crate::crypto;
+
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
+
use crate::git::ext as git_ext;
+
use crate::git::Url;
+
use crate::git::{RefError, RefStr, RefString};
+
use crate::identity;
+
use crate::identity::{Id, IdError, Project};
+
use crate::storage::refs::Refs;
+

+
use self::refs::SignedRefs;
+

+
pub type BranchName = String;
+
pub type Inventory = Vec<Id>;
+

+
/// Storage error.
+
#[derive(Error, Debug)]
+
pub enum Error {
+
    #[error("invalid git reference")]
+
    InvalidRef,
+
    #[error("git reference error: {0}")]
+
    Ref(#[from] RefError),
+
    #[error(transparent)]
+
    Refs(#[from] refs::Error),
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("id: {0}")]
+
    Id(#[from] IdError),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("doc: {0}")]
+
    Doc(#[from] identity::doc::Error),
+
    #[error("invalid repository head")]
+
    InvalidHead,
+
}
+

+
/// Fetch error.
+
#[derive(Error, Debug)]
+
#[allow(clippy::large_enum_variant)]
+
pub enum FetchError {
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("verify: {0}")]
+
    Verify(#[from] git::VerifyError),
+
}
+

+
pub type RemoteId = PublicKey;
+

+
/// An update to a reference.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub enum RefUpdate {
+
    Updated { name: RefString, old: Oid, new: Oid },
+
    Created { name: RefString, oid: Oid },
+
    Deleted { name: RefString, oid: Oid },
+
    Skipped { name: RefString, oid: Oid },
+
}
+

+
impl RefUpdate {
+
    pub fn from(name: RefString, old: impl Into<Oid>, new: impl Into<Oid>) -> Self {
+
        let old = old.into();
+
        let new = new.into();
+

+
        if old.is_zero() {
+
            Self::Created { name, oid: new }
+
        } else if new.is_zero() {
+
            Self::Deleted { name, oid: old }
+
        } else if old != new {
+
            Self::Updated { name, old, new }
+
        } else {
+
            Self::Skipped { name, oid: old }
+
        }
+
    }
+
}
+

+
impl fmt::Display for RefUpdate {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::Updated { name, old, new } => {
+
                write!(f, "~ {:.7}..{:.7} {}", old, new, name)
+
            }
+
            Self::Created { name, oid } => {
+
                write!(f, "* 0000000..{:.7} {}", oid, name)
+
            }
+
            Self::Deleted { name, oid } => {
+
                write!(f, "- {:.7}..0000000 {}", oid, name)
+
            }
+
            Self::Skipped { name, oid } => {
+
                write!(f, "= {:.7}..{:.7} {}", oid, oid, name)
+
            }
+
        }
+
    }
+
}
+

+
/// Project remotes. Tracks the git state of a project.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
+

+
impl<V> FromIterator<(RemoteId, Remote<V>)> for Remotes<V> {
+
    fn from_iter<T: IntoIterator<Item = (RemoteId, Remote<V>)>>(iter: T) -> Self {
+
        Self(iter.into_iter().collect())
+
    }
+
}
+

+
impl<V> Deref for Remotes<V> {
+
    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)
+
    }
+
}
+

+
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 {
+
        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()
+
    }
+
}
+

+
impl<V> From<Remotes<V>> for HashMap<RemoteId, Refs> {
+
    fn from(other: Remotes<V>) -> Self {
+
        let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into());
+

+
        for (k, v) in other.into_iter() {
+
            remotes.insert(k, v.refs.into());
+
        }
+
        remotes
+
    }
+
}
+

+
/// A project remote.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Remote<V> {
+
    /// ID of remote.
+
    pub id: PublicKey,
+
    /// Git references published under this remote, and their hashes.
+
    pub refs: SignedRefs<V>,
+
    /// Whether this remote is of a project delegate.
+
    pub delegate: bool,
+
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
+
    verified: PhantomData<V>,
+
}
+

+
impl<V> Remote<V> {
+
    pub fn new(id: PublicKey, refs: impl Into<SignedRefs<V>>) -> Self {
+
        Self {
+
            id,
+
            refs: refs.into(),
+
            delegate: false,
+
            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 {
+
    fn url(&self) -> Url;
+
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error>;
+
    fn inventory(&self) -> Result<Inventory, Error>;
+
}
+

+
pub trait WriteStorage<'r>: ReadStorage {
+
    type Repository: WriteRepository<'r>;
+

+
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error>;
+
    fn sign_refs<G: Signer>(
+
        &self,
+
        repository: &Self::Repository,
+
        signer: G,
+
    ) -> Result<SignedRefs<Verified>, Error>;
+
}
+

+
pub trait ReadRepository<'r> {
+
    type Remotes: Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r;
+

+
    fn is_empty(&self) -> Result<bool, git2::Error>;
+
    fn path(&self) -> &Path;
+
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
+
    fn reference(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &RefStr,
+
    ) -> Result<Option<git2::Reference>, git2::Error>;
+
    fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error>;
+
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
+
    fn reference_oid(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &RefStr,
+
    ) -> Result<Option<Oid>, git2::Error>;
+
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
+
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
+
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error>;
+
    /// Return the project associated with this repository.
+
    fn project(&self) -> Result<Project, Error>;
+
}
+

+
pub trait WriteRepository<'r>: ReadRepository<'r> {
+
    fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
+
    fn raw(&self) -> &git2::Repository;
+
}
+

+
impl<T, S> ReadStorage for T
+
where
+
    T: Deref<Target = S>,
+
    S: ReadStorage + 'static,
+
{
+
    fn url(&self) -> Url {
+
        self.deref().url()
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        self.deref().inventory()
+
    }
+

+
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
+
        self.deref().get(remote, proj)
+
    }
+
}
+

+
impl<'r, T, S> WriteStorage<'r> for T
+
where
+
    T: Deref<Target = S>,
+
    S: WriteStorage<'r> + 'static,
+
{
+
    type Repository = S::Repository;
+

+
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
+
        self.deref().repository(proj)
+
    }
+

+
    fn sign_refs<G: Signer>(
+
        &self,
+
        repository: &S::Repository,
+
        signer: G,
+
    ) -> Result<SignedRefs<Verified>, Error> {
+
        self.deref().sign_refs(repository, signer)
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    #[test]
+
    fn test_storage() {}
+
}
added radicle/src/storage/git.rs
@@ -0,0 +1,761 @@
+
use std::collections::{BTreeMap, HashMap};
+
use std::path::{Path, PathBuf};
+
use std::{fmt, fs, io};
+

+
use git_ref_format::refspec;
+
use once_cell::sync::Lazy;
+

+
use crate::crypto::{Signer, Unverified, Verified};
+
use crate::git;
+
use crate::identity;
+
use crate::identity::{Doc, Id, Project};
+
use crate::storage::refs;
+
use crate::storage::refs::{Refs, SignedRefs};
+
use crate::storage::{
+
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, WriteRepository,
+
    WriteStorage,
+
};
+

+
pub use crate::git::*;
+

+
use super::{RefUpdate, RemoteId};
+

+
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
+
    Lazy::new(|| refspec::pattern!("refs/remotes/*"));
+
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
+
    Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
+

+
#[derive(Error, Debug)]
+
pub enum IdentityError {
+
    #[error("identity branches diverge from each other")]
+
    BranchesDiverge,
+
    #[error("identity branches are in an invalid state")]
+
    InvalidState,
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("git: {0}")]
+
    GitExt(#[from] git::Error),
+
    #[error("refs: {0}")]
+
    Refs(#[from] refs::Error),
+
}
+

+
pub struct Storage {
+
    path: PathBuf,
+
}
+

+
impl fmt::Debug for Storage {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Storage(..)")
+
    }
+
}
+

+
impl ReadStorage for Storage {
+
    fn url(&self) -> Url {
+
        Url {
+
            scheme: git_url::Scheme::File,
+
            host: None,
+
            path: self.path.to_string_lossy().to_string().into(),
+
            ..Url::default()
+
        }
+
    }
+

+
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
+
        // TODO: Don't create a repo here if it doesn't exist?
+
        // Perhaps for checking we could have a `contains` method?
+
        let repo = self.repository(proj)?;
+

+
        if let Some(doc) = repo.identity_of(remote)? {
+
            let remotes = repo.remotes()?.collect::<Result<_, _>>()?;
+
            let path = repo.path().to_path_buf();
+

+
            // TODO: We should check that there is at least one remote, which is
+
            // the one of the local user, otherwise it means the project is in
+
            // an corrupted state.
+

+
            Ok(Some(Project {
+
                id: proj.clone(),
+
                doc,
+
                remotes,
+
                path,
+
            }))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        self.projects()
+
    }
+
}
+

+
impl<'r> WriteStorage<'r> for Storage {
+
    type Repository = Repository;
+

+
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
+
        Repository::open(self.path.join(proj.to_string()))
+
    }
+

+
    fn sign_refs<G: Signer>(
+
        &self,
+
        repository: &Repository,
+
        signer: G,
+
    ) -> Result<SignedRefs<Verified>, Error> {
+
        let remote = signer.public_key();
+
        let refs = repository.references(remote)?;
+
        let signed = refs.signed(&signer)?;
+

+
        signed.save(remote, repository)?;
+

+
        Ok(signed)
+
    }
+
}
+

+
impl Storage {
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
+
        let path = path.as_ref().to_path_buf();
+

+
        match fs::create_dir_all(&path) {
+
            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
+
            Err(err) => return Err(err),
+
            Ok(()) => {}
+
        }
+

+
        Ok(Self { path })
+
    }
+

+
    pub fn path(&self) -> &Path {
+
        self.path.as_path()
+
    }
+

+
    pub fn projects(&self) -> Result<Vec<Id>, Error> {
+
        let mut projects = Vec::new();
+

+
        for result in fs::read_dir(&self.path)? {
+
            let path = result?;
+
            let id = Id::try_from(path.file_name())?;
+

+
            projects.push(id);
+
        }
+
        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(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.
+
}
+

+
#[derive(Debug, Error)]
+
pub enum VerifyError {
+
    #[error("invalid remote `{0}`")]
+
    InvalidRemote(RemoteId),
+
    #[error("invalid target `{2}` for reference `{1}` of remote `{0}`")]
+
    InvalidRefTarget(RemoteId, RefString, git2::Oid),
+
    #[error("invalid reference")]
+
    InvalidRef,
+
    #[error("ref error: {0}")]
+
    Ref(#[from] git::RefError),
+
    #[error("refs error: {0}")]
+
    Refs(#[from] refs::Error),
+
    #[error("unknown reference `{1}` in remote `{0}`")]
+
    UnknownRef(RemoteId, git::RefString),
+
    #[error("missing reference `{1}` in remote `{0}`")]
+
    MissingRef(RemoteId, git::RefString),
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
}
+

+
impl Repository {
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let backend = match git2::Repository::open_bare(path.as_ref()) {
+
            Err(e) if ext::is_not_found_err(&e) => {
+
                let backend = git2::Repository::init_opts(
+
                    path,
+
                    git2::RepositoryInitOptions::new()
+
                        .bare(true)
+
                        .no_reinit(true)
+
                        .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(repo) => Ok(repo),
+
            Err(e) => Err(e),
+
        }?;
+

+
        Ok(Self { backend })
+
    }
+

+
    pub fn head(&self) -> Result<git2::Commit, git2::Error> {
+
        // TODO: Find longest history, get document and get head.
+
        // Perhaps we should even set a local `HEAD` or at least `refs/heads/master`
+
        todo!();
+
    }
+

+
    pub fn verify(&self) -> Result<(), VerifyError> {
+
        let mut remotes: HashMap<RemoteId, Refs> = self
+
            .remotes()?
+
            .map(|remote| {
+
                let (id, remote) = remote?;
+
                Ok((id, remote.refs.into()))
+
            })
+
            .collect::<Result<_, VerifyError>>()?;
+

+
        for r in self.backend.references()? {
+
            let r = r?;
+
            let name = r.name().ok_or(VerifyError::InvalidRef)?;
+
            let oid = r.target().ok_or(VerifyError::InvalidRef)?;
+
            let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
+

+
            if refname == *refs::SIGNATURE_REF {
+
                continue;
+
            }
+
            let remote = remotes
+
                .get_mut(&remote_id)
+
                .ok_or(VerifyError::InvalidRemote(remote_id))?;
+
            let signed_oid = remote
+
                .remove(&refname)
+
                .ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
+

+
            if Oid::from(oid) != signed_oid {
+
                return Err(VerifyError::InvalidRefTarget(remote_id, refname, oid));
+
            }
+
        }
+

+
        // The refs that are left in the map, are ones that were signed, but are not
+
        // in the repository.
+
        for (id, refs) in remotes.into_iter() {
+
            if let Some((name, _)) = refs.into_iter().next() {
+
                return Err(VerifyError::MissingRef(id, name));
+
            }
+
        }
+

+
        Ok(())
+
    }
+

+
    pub fn inspect(&self) -> Result<(), Error> {
+
        for r in self.backend.references()? {
+
            let r = r?;
+
            let name = r.name().ok_or(Error::InvalidRef)?;
+
            let oid = r.target().ok_or(Error::InvalidRef)?;
+

+
            println!("{} {}", oid, name);
+
        }
+
        Ok(())
+
    }
+

+
    pub fn identity_of(
+
        &self,
+
        remote: &RemoteId,
+
    ) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
+
        if let Some((doc, _)) = identity::Doc::load(remote, self)? {
+
            Ok(Some(doc.verified().unwrap()))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    /// Return the canonical identity [`git::Oid`] and document.
+
    pub fn identity(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
+
        let mut heads = Vec::new();
+
        for remote in self.remote_ids()? {
+
            let remote = remote?;
+
            let oid = Doc::<Unverified>::head(&remote, self)?.unwrap();
+

+
            heads.push(oid.into());
+
        }
+
        // Keep track of the longest identity branch.
+
        let mut longest = heads.pop().ok_or(IdentityError::InvalidState)?;
+

+
        for head in &heads {
+
            let base = self.raw().merge_base(*head, longest)?;
+

+
            if base == longest {
+
                // `head` is a successor of `longest`. Update `longest`.
+
                //
+
                //   o head
+
                //   |
+
                //   o longest (base)
+
                //   |
+
                //
+
                longest = *head;
+
            } else if base == *head || *head == longest {
+
                // `head` is an ancestor of `longest`, or equal to it. Do nothing.
+
                //
+
                //   o longest             o longest, head (base)
+
                //   |                     |
+
                //   o head (base)   OR    o
+
                //   |                     |
+
                //
+
            } else {
+
                // The merge base between `head` and `longest` (`base`)
+
                // is neither `head` nor `longest`. Therefore, the branches have
+
                // diverged.
+
                //
+
                //    longest   head
+
                //           \ /
+
                //            o (base)
+
                //            |
+
                //
+
                return Err(IdentityError::BranchesDiverge);
+
            }
+
        }
+

+
        Doc::load_at(longest.into(), self)?
+
            .ok_or(refs::Error::NotFound)
+
            .map(|(doc, _)| (longest.into(), doc))
+
            .map_err(IdentityError::from)
+
    }
+

+
    pub fn remote_ids(
+
        &self,
+
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git2::Error> {
+
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
+
            |reference| -> Result<RemoteId, refs::Error> {
+
                let r = reference?;
+
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
+
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
+

+
                Ok(id)
+
            },
+
        );
+
        Ok(iter)
+
    }
+
}
+

+
impl<'r> ReadRepository<'r> for Repository {
+
    type Remotes = Box<dyn Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r>;
+

+
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
        let some = self.remotes()?.next().is_some();
+
        Ok(!some)
+
    }
+

+
    fn path(&self) -> &Path {
+
        self.backend.path()
+
    }
+

+
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::Error> {
+
        git::ext::Blob::At {
+
            object: oid.into(),
+
            path,
+
        }
+
        .get(&self.backend)
+
    }
+

+
    fn reference(
+
        &self,
+
        remote: &RemoteId,
+
        name: &git::RefStr,
+
    ) -> Result<Option<git2::Reference>, git2::Error> {
+
        let name = name.strip_prefix(git::refname!("refs")).unwrap_or(name);
+
        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 commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
+
        self.backend.find_commit(oid.into()).map(Some).or_else(|e| {
+
            if git::ext::is_not_found_err(&e) {
+
                Ok(None)
+
            } else {
+
                Err(e)
+
            }
+
        })
+
    }
+

+
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
+
        let mut revwalk = self.backend.revwalk()?;
+
        revwalk.push(head.into())?;
+

+
        Ok(revwalk)
+
    }
+

+
    fn reference_oid(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &git::RefStr,
+
    ) -> Result<Option<Oid>, git2::Error> {
+
        let reference = self.reference(remote, 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
+
            .backend
+
            .references_glob(format!("refs/remotes/{remote}/*").as_str())?;
+
        let mut refs = BTreeMap::new();
+

+
        for e in entries {
+
            let e = e?;
+
            let name = e.name().ok_or(Error::InvalidRef)?;
+
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
+
            let oid = e.target().ok_or(Error::InvalidRef)?;
+

+
            refs.insert(refname, oid.into());
+
        }
+
        Ok(refs.into())
+
    }
+

+
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error> {
+
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
+
            |reference| -> Result<(RemoteId, Remote<Verified>), refs::Error> {
+
                let r = reference?;
+
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
+
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
+
                let remote = self.remote(&id)?;
+

+
                Ok((id, remote))
+
            },
+
        );
+

+
        Ok(Box::new(iter))
+
    }
+

+
    fn project(&self) -> Result<Project, Error> {
+
        todo!()
+
    }
+
}
+

+
impl<'r> WriteRepository<'r> for Repository {
+
    /// Fetch all remotes of a project from the given URL.
+
    fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, FetchError> {
+
        // TODO: Have function to fetch specific remotes.
+
        //
+
        // Repository layout should look like this:
+
        //
+
        //   /refs/remotes/<remote>
+
        //         /heads
+
        //           /master
+
        //         /tags
+
        //         ...
+
        //
+
        let url = url.to_string();
+
        let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
+
        let mut updates = Vec::new();
+
        let mut callbacks = git2::RemoteCallbacks::new();
+
        let tempdir = tempfile::tempdir()?;
+
        // TODO: Comment
+
        let staging = {
+
            let mut builder = git2::build::RepoBuilder::new();
+
            let path = tempdir.path().join("git");
+
            let staging_repo = builder
+
                .bare(true)
+
                // TODO: Comment
+
                // TODO: Due to this, I think we'll have to run GC when there is a failure.
+
                .clone_local(git2::build::CloneLocal::Local)
+
                .clone(
+
                    &git::Url {
+
                        scheme: git::url::Scheme::File,
+
                        path: self.backend.path().to_string_lossy().to_string().into(),
+
                        ..git::Url::default()
+
                    }
+
                    .to_string(),
+
                    &path,
+
                )?;
+

+
            // In case we fetch an invalid update, we want to make sure nothing is deleted.
+
            let mut opts = git2::FetchOptions::default();
+
            opts.prune(git2::FetchPrune::Off);
+

+
            staging_repo
+
                .remote_anonymous(&url)?
+
                .fetch(refs, Some(&mut opts), None)?;
+
            // TODO: Comment
+
            Repository::from(staging_repo).verify()?;
+

+
            path
+
        };
+

+
        callbacks.update_tips(|name, old, new| {
+
            if let Ok(name) = git::RefString::try_from(name) {
+
                updates.push(RefUpdate::from(name, old, new));
+
            } else {
+
                log::warn!("Invalid ref `{}` detected; aborting fetch", name);
+
                return false;
+
            }
+
            // Returning `true` ensures the process is not aborted.
+
            true
+
        });
+

+
        {
+
            let mut remote = self.backend.remote_anonymous(
+
                &git::Url {
+
                    scheme: git::url::Scheme::File,
+
                    path: staging.to_string_lossy().to_string().into(),
+
                    ..git::Url::default()
+
                }
+
                .to_string(),
+
            )?;
+
            let mut opts = git2::FetchOptions::default();
+
            opts.remote_callbacks(callbacks);
+

+
            // TODO: Make sure we verify before pruning, as pruning may get us into
+
            // a state we can't roll back.
+
            opts.prune(git2::FetchPrune::On);
+
            remote.fetch(refs, Some(&mut opts), None)?;
+
        }
+

+
        Ok(updates)
+
    }
+

+
    fn raw(&self) -> &git2::Repository {
+
        &self.backend
+
    }
+
}
+

+
impl From<git2::Repository> for Repository {
+
    fn from(backend: git2::Repository) -> Self {
+
        Self { backend }
+
    }
+
}
+

+
pub mod trailers {
+
    use std::str::FromStr;
+

+
    use super::*;
+
    use crate::crypto::{PublicKey, PublicKeyError};
+
    use crate::crypto::{Signature, SignatureError};
+

+
    pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
+

+
    #[derive(Error, Debug)]
+
    pub enum Error {
+
        #[error("invalid format for signature trailer")]
+
        SignatureTrailerFormat,
+
        #[error("invalid public key in signature trailer")]
+
        PublicKey(#[from] PublicKeyError),
+
        #[error("invalid signature in trailer")]
+
        Signature(#[from] SignatureError),
+
    }
+

+
    pub fn parse_signatures(msg: &str) -> Result<Vec<(PublicKey, Signature)>, Error> {
+
        let trailers =
+
            git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
+
        let mut signatures = Vec::with_capacity(trailers.len());
+

+
        for (key, val) in trailers.iter() {
+
            if key == SIGNATURE_TRAILER {
+
                if let Some((pk, sig)) = val.split_once(' ') {
+
                    let pk = PublicKey::from_str(pk)?;
+
                    let sig = Signature::from_str(sig)?;
+

+
                    signatures.push((pk, sig));
+
                } else {
+
                    return Err(Error::SignatureTrailerFormat);
+
                }
+
            }
+
        }
+
        Ok(signatures)
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use crate::assert_matches;
+
    use crate::git;
+
    use crate::storage::refs::SIGNATURE_REF;
+
    use crate::storage::{ReadRepository, ReadStorage, RefUpdate, WriteRepository};
+
    use crate::test::arbitrary;
+
    use crate::test::fixtures;
+
    use crate::test::signer::MockSigner;
+

+
    #[test]
+
    fn test_remote_refs() {
+
        let dir = tempfile::tempdir().unwrap();
+
        let signer = MockSigner::default();
+
        let storage = fixtures::storage(dir.path(), &signer).unwrap();
+
        let inv = storage.inventory().unwrap();
+
        let proj = inv.first().unwrap();
+
        let mut refs = git::remote_refs(&git::Url {
+
            host: Some(storage.path().to_string_lossy().to_string()),
+
            scheme: git_url::Scheme::File,
+
            path: format!("/{}", proj).into(),
+
            ..git::Url::default()
+
        })
+
        .unwrap();
+

+
        let project = storage.repository(proj).unwrap();
+
        let remotes = project.remotes().unwrap();
+

+
        // Strip the remote refs of sigrefs so we can compare them.
+
        for remote in refs.values_mut() {
+
            remote.remove(&*SIGNATURE_REF).unwrap();
+
        }
+

+
        let remotes = remotes
+
            .map(|remote| remote.map(|(id, r): (RemoteId, Remote<Verified>)| (id, r.refs.into())))
+
            .collect::<Result<_, _>>()
+
            .unwrap();
+

+
        assert_eq!(refs, remotes);
+
    }
+

+
    #[test]
+
    fn test_fetch() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let alice_signer = MockSigner::default();
+
        let alice = fixtures::storage(tmp.path().join("alice"), alice_signer).unwrap();
+
        let bob = Storage::open(tmp.path().join("bob")).unwrap();
+
        let inventory = alice.inventory().unwrap();
+
        let proj = inventory.first().unwrap();
+
        let repo = alice.repository(proj).unwrap();
+
        let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
+
        let refname = git::refname!("heads/master");
+

+
        // Have Bob fetch Alice's refs.
+
        let updates = bob
+
            .repository(proj)
+
            .unwrap()
+
            .fetch(&git::Url {
+
                scheme: git_url::Scheme::File,
+
                path: alice
+
                    .path()
+
                    .join(proj.to_string())
+
                    .to_string_lossy()
+
                    .into_owned()
+
                    .into(),
+
                ..git::Url::default()
+
            })
+
            .unwrap();
+

+
        // Four refs are created for each remote.
+
        assert_eq!(updates.len(), remotes.len() * 3);
+

+
        for update in updates {
+
            assert_matches!(
+
                update,
+
                RefUpdate::Created { name, .. } if name.starts_with("refs/remotes")
+
            );
+
        }
+

+
        for remote in remotes {
+
            let (id, _) = remote.unwrap();
+
            let alice_repo = alice.repository(proj).unwrap();
+
            let alice_oid = alice_repo.reference(&id, &refname).unwrap().unwrap();
+

+
            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_fetch_update() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let alice = Storage::open(tmp.path().join("alice/storage")).unwrap();
+
        let bob = Storage::open(tmp.path().join("bob/storage")).unwrap();
+

+
        let alice_signer = MockSigner::new(&mut fastrand::Rng::new());
+
        let alice_id = alice_signer.public_key();
+
        let (proj_id, _, proj_repo, alice_head) =
+
            fixtures::project(tmp.path().join("alice/project"), &alice, &alice_signer).unwrap();
+

+
        let refname = git::refname!("refs/heads/master");
+
        let alice_url = git::Url {
+
            scheme: git_url::Scheme::File,
+
            path: alice
+
                .path()
+
                .join(proj_id.to_string())
+
                .to_string_lossy()
+
                .into_owned()
+
                .into(),
+
            ..git::Url::default()
+
        };
+

+
        // Have Bob fetch Alice's refs.
+
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
+
        // Three refs are created: the branch, the signature and the id.
+
        assert_eq!(updates.len(), 3);
+

+
        let alice_proj_storage = alice.repository(&proj_id).unwrap();
+
        let alice_head = proj_repo.find_commit(alice_head).unwrap();
+
        let alice_head = git::commit(&proj_repo, &alice_head, &refname, "Making changes", "Alice")
+
            .unwrap()
+
            .id();
+
        git::push(&proj_repo).unwrap();
+
        alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
+

+
        // Have Bob fetch Alice's new commit.
+
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
+
        // The branch and signature refs are updated.
+
        assert_matches!(
+
            updates.as_slice(),
+
            &[RefUpdate::Updated { .. }, RefUpdate::Updated { .. }]
+
        );
+

+
        // Bob's storage is updated.
+
        let bob_repo = bob.repository(&proj_id).unwrap();
+
        let bob_master = bob_repo.reference(alice_id, &refname).unwrap().unwrap();
+

+
        assert_eq!(bob_master.target().unwrap(), alice_head);
+
    }
+

+
    #[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()).unwrap();
+
        let proj_id = arbitrary::gen::<Id>(1);
+
        let alice = *signer.public_key();
+
        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();
+

+
        git::commit(
+
            backend,
+
            &head,
+
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
+
            "Second commit",
+
            &alice.to_string(),
+
        )
+
        .unwrap();
+

+
        let signed = storage.sign_refs(&project, &signer).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);
+
    }
+
}
added radicle/src/storage/refs.rs
@@ -0,0 +1,354 @@
+
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 once_cell::sync::Lazy;
+
use radicle_git_ext as git_ext;
+
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 static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("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)]
+
pub struct Refs(BTreeMap<git::RefString, 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(&msg, &signature) {
+
            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 = git::RefString::try_from(name)?;
+
            let oid = Oid::from_str(oid)?;
+

+
            if oid.is_zero() {
+
                continue;
+
            }
+
            refs.insert(name, oid);
+
        }
+
        Ok(Self(refs))
+
    }
+

+
    pub fn canonical(&self) -> Vec<u8> {
+
        let mut buf = String::new();
+
        let refs = self
+
            .iter()
+
            .filter(|(name, oid)| *name != &*SIGNATURE_REF && !oid.is_zero());
+

+
        for (name, oid) in refs {
+
            buf.push_str(&oid.to_string());
+
            buf.push(' ');
+
            buf.push_str(name);
+
            buf.push('\n');
+
        }
+
        buf.into_bytes()
+
    }
+
}
+

+
impl IntoIterator for Refs {
+
    type Item = (git::RefString, Oid);
+
    type IntoIter = std::collections::btree_map::IntoIter<git::RefString, Oid>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.0.into_iter()
+
    }
+
}
+

+
impl From<Refs> for BTreeMap<git::RefString, 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<git::RefString, Oid>> for Refs {
+
    fn from(refs: BTreeMap<git::RefString, Oid>) -> Self {
+
        Self(refs)
+
    }
+
}
+

+
impl Deref for Refs {
+
    type Target = BTreeMap<git::RefString, 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 key.
+
///
+
/// The type parameter keeps track of whether the signature was [`Verified`] or
+
/// [`Unverified`].
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct SignedRefs<V> {
+
    pub refs: Refs,
+
    pub signature: Signature,
+

+
    _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> {
+
        match self.verify(signer) {
+
            Ok(()) => Ok(SignedRefs {
+
                refs: self.refs,
+
                signature: self.signature,
+
                _verified: PhantomData,
+
            }),
+
            Err(e) => Err(e),
+
        }
+
    }
+

+
    pub fn verify(&self, signer: &PublicKey) -> Result<(), crypto::Error> {
+
        let canonical = self.refs.canonical();
+

+
        match signer.verify(&canonical, &self.signature) {
+
            Ok(()) => Ok(()),
+
            Err(e) => Err(e),
+
        }
+
    }
+
}
+

+
impl SignedRefs<Verified> {
+
    pub fn load<'r, S>(remote: &RemoteId, repo: &S) -> Result<Self, Error>
+
    where
+
        S: ReadRepository<'r>,
+
    {
+
        if let Some(oid) = repo.reference_oid(remote, &SIGNATURE_REF)? {
+
            Self::load_at(oid, remote, repo)
+
        } else {
+
            Err(Error::NotFound)
+
        }
+
    }
+

+
    pub fn load_at<'r, S>(oid: Oid, remote: &RemoteId, repo: &S) -> Result<Self, Error>
+
    where
+
        S: storage::ReadRepository<'r>,
+
    {
+
        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 = signature.content().try_into()?;
+

+
        match remote.verify(refs.content(), &signature) {
+
            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<'r, S: WriteRepository<'r>>(
+
        &self,
+
        // TODO: This should be part of the signed refs.
+
        remote: &RemoteId,
+
        repo: &S,
+
    ) -> Result<Updated, Error> {
+
        let sigref = &*SIGNATURE_REF;
+
        let parent: Option<git2::Commit> = repo
+
            .reference(remote, sigref)?
+
            .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.as_ref())?;
+

+
            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}/{sigref}");
+
        let author = repo.raw().signature()?;
+
        let commit = repo.raw().commit(
+
            Some(&sigref),
+
            &author,
+
            &author,
+
            &format!("Update {} for {}", sigref, 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(transparent)]
+
        InvalidRef(#[from] git_ref_format::Error),
+
        #[error("invalid canonical format")]
+
        InvalidFormat,
+
        #[error(transparent)]
+
        Io(#[from] io::Error),
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    #[quickcheck]
+
    fn prop_canonical_roundtrip(refs: Refs) {
+
        let encoded = refs.canonical();
+
        let decoded = Refs::from_canonical(&encoded).unwrap();
+

+
        assert_eq!(refs, decoded);
+
    }
+
}
added radicle/src/test.rs
@@ -0,0 +1,5 @@
+
pub mod arbitrary;
+
pub mod assert;
+
pub mod fixtures;
+
pub mod signer;
+
pub mod storage;
added radicle/src/test/arbitrary.rs
@@ -0,0 +1,234 @@
+
use std::collections::{BTreeMap, HashSet};
+
use std::hash::Hash;
+
use std::iter;
+
use std::ops::RangeBounds;
+
use std::path::PathBuf;
+

+
use nonempty::NonEmpty;
+
use quickcheck::Arbitrary;
+

+
use crate::collections::HashMap;
+
use crate::crypto;
+
use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified};
+
use crate::git;
+
use crate::hash;
+
use crate::identity::{doc::Delegate, doc::Doc, Did, Id, Project};
+
use crate::storage;
+
use crate::storage::refs::{Refs, SignedRefs};
+
use crate::test::signer::MockSigner;
+
use crate::test::storage::MockStorage;
+

+
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
+
    let size = fastrand::usize(range);
+
    let mut set = HashSet::with_capacity(size);
+
    let mut g = quickcheck::Gen::new(size);
+

+
    while set.len() < size {
+
        set.insert(T::arbitrary(&mut g));
+
    }
+
    set
+
}
+

+
pub fn gen<T: Arbitrary>(size: usize) -> T {
+
    let mut gen = quickcheck::Gen::new(size);
+

+
    T::arbitrary(&mut gen)
+
}
+

+
#[derive(Clone, Debug)]
+
pub struct ByteArray<const N: usize>([u8; N]);
+

+
impl<const N: usize> ByteArray<N> {
+
    pub fn into_inner(self) -> [u8; N] {
+
        self.0
+
    }
+

+
    pub fn as_slice(&self) -> &[u8] {
+
        self.0.as_slice()
+
    }
+
}
+

+
impl<const N: usize> Arbitrary for ByteArray<N> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut bytes: [u8; N] = [0; N];
+
        for byte in &mut bytes {
+
            *byte = u8::arbitrary(g);
+
        }
+
        Self(bytes)
+
    }
+
}
+

+
impl Arbitrary for storage::Remotes<crypto::Verified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
+
            Arbitrary::arbitrary(g);
+

+
        storage::Remotes::new(remotes)
+
    }
+
}
+

+
impl Arbitrary for Project {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let doc = Doc::<Verified>::arbitrary(g);
+
        let (oid, _) = doc.encode().unwrap();
+
        let id = Id::from(oid);
+
        let remotes = storage::Remotes::arbitrary(g);
+
        let path = PathBuf::arbitrary(g);
+

+
        Self {
+
            id,
+
            doc,
+
            remotes,
+
            path,
+
        }
+
    }
+
}
+

+
impl Arbitrary for Did {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        Self::from(PublicKey::arbitrary(g))
+
    }
+
}
+

+
impl Arbitrary for Delegate {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        Self {
+
            name: String::arbitrary(g),
+
            id: Did::arbitrary(g),
+
        }
+
    }
+
}
+

+
impl Arbitrary for Doc<Unverified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let name = String::arbitrary(g);
+
        let description = String::arbitrary(g);
+
        let default_branch = String::arbitrary(g);
+
        let delegate = Delegate::arbitrary(g);
+

+
        Self::initial(name, description, default_branch, delegate)
+
    }
+
}
+

+
impl Arbitrary for Doc<Verified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
+
        let name = iter::repeat_with(|| rng.alphanumeric())
+
            .take(rng.usize(1..16))
+
            .collect();
+
        let description = iter::repeat_with(|| rng.alphanumeric())
+
            .take(rng.usize(0..32))
+
            .collect();
+
        let default_branch = iter::repeat_with(|| rng.alphanumeric())
+
            .take(rng.usize(1..16))
+
            .collect();
+
        let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
+
            name: iter::repeat_with(|| rng.alphanumeric())
+
                .take(rng.usize(1..16))
+
                .collect(),
+
            id: Did::arbitrary(g),
+
        })
+
        .take(rng.usize(1..6))
+
        .collect::<Vec<_>>()
+
        .try_into()
+
        .unwrap();
+
        let threshold = delegates.len() / 2 + 1;
+
        let doc: Doc<Unverified> =
+
            Doc::new(name, description, default_branch, delegates, threshold);
+

+
        doc.verified().unwrap()
+
    }
+
}
+

+
impl Arbitrary for SignedRefs<Unverified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
+
        let signature = crypto::Signature::from(bytes.into_inner());
+
        let refs = Refs::arbitrary(g);
+

+
        Self::new(refs, signature)
+
    }
+
}
+

+
impl Arbitrary for Refs {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
+
        let mut bytes: [u8; 20] = [0; 20];
+
        let names = &[
+
            "heads/master",
+
            "heads/feature/1",
+
            "heads/feature/2",
+
            "heads/feature/3",
+
            "heads/radicle/id",
+
            "tags/v1.0",
+
            "tags/v2.0",
+
            "notes/1",
+
        ];
+

+
        for _ in 0..g.size().min(names.len()) {
+
            if let Some(name) = g.choose(names) {
+
                for byte in &mut bytes {
+
                    *byte = u8::arbitrary(g);
+
                }
+
                let oid = storage::Oid::try_from(&bytes[..]).unwrap();
+
                let name = git::RefString::try_from(*name).unwrap();
+

+
                refs.insert(name, oid);
+
            }
+
        }
+
        Self::from(refs)
+
    }
+
}
+

+
impl Arbitrary for MockSigner {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
+
        let seed = Seed::new(bytes.into_inner());
+
        let sk = KeyPair::from_seed(seed).sk;
+

+
        MockSigner::from(sk)
+
    }
+
}
+

+
impl Arbitrary for MockStorage {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let inventory = Arbitrary::arbitrary(g);
+
        MockStorage::new(inventory)
+
    }
+
}
+

+
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 Id {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes = ByteArray::<20>::arbitrary(g);
+
        let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
+

+
        Id::from(oid)
+
    }
+
}
+

+
impl Arbitrary for hash::Digest {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: Vec<u8> = Arbitrary::arbitrary(g);
+
        hash::Digest::new(&bytes)
+
    }
+
}
+

+
impl Arbitrary for PublicKey {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
+
        let seed = Seed::new(bytes.into_inner());
+
        let keypair = KeyPair::from_seed(seed);
+

+
        PublicKey(keypair.pk)
+
    }
+
}
added radicle/src/test/assert.rs
@@ -0,0 +1,296 @@
+
// Copyright (c) 2016 Murarth
+
//
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+
// and associated documentation files (the "Software"), to deal in the Software without
+
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
+
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+
// Software is furnished to do so, subject to the following conditions:
+
//
+
// The above copyright notice and this permission notice shall be included in all copies or
+
// substantial portions of the Software.
+
//
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+

+
//! Provides a macro, `assert_matches!`, which tests whether a value
+
//! matches a given pattern, causing a panic if the match fails.
+
//!
+
//! See the macro [`assert_matches!`] documentation for more information.
+
//!
+
//! Also provides a debug-only counterpart, [`debug_assert_matches!`].
+
//!
+
//! See the macro [`debug_assert_matches!`] documentation for more information
+
//! about this macro.
+
//!
+
//! [`assert_matches!`]: macro.assert_matches.html
+
//! [`debug_assert_matches!`]: macro.debug_assert_matches.html
+

+
#![deny(missing_docs)]
+

+
/// Asserts that an expression matches a given pattern.
+
///
+
/// A guard expression may be supplied to add further restrictions to the
+
/// expected value of the expression.
+
///
+
/// A `match` arm may be supplied to perform additional assertions or to yield
+
/// a value from the macro invocation.
+
///
+
#[macro_export]
+
macro_rules! assert_matches {
+
    ( $e:expr , $($pat:pat_param)|+ ) => {
+
        match $e {
+
            $($pat)|+ => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr ) => {
+
        match $e {
+
            $($pat)|+ if $cond => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+ if $cond))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr ) => {
+
        match $e {
+
            $($pat)|+ => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr ) => {
+
        match $e {
+
            $($pat)|+ if $cond => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+ if $cond))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ if $cond => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ if $cond => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
+
        }
+
    };
+
}
+

+
/// Asserts that an expression matches a given pattern.
+
///
+
/// Unlike [`assert_matches!`], `debug_assert_matches!` statements are only enabled
+
/// in non-optimized builds by default. An optimized build will omit all
+
/// `debug_assert_matches!` statements unless `-C debug-assertions` is passed
+
/// to the compiler.
+
///
+
/// See the macro [`assert_matches!`] documentation for more information.
+
///
+
/// [`assert_matches!`]: macro.assert_matches.html
+
#[macro_export(local_inner_macros)]
+
macro_rules! debug_assert_matches {
+
    ( $($tt:tt)* ) => { {
+
        if _assert_matches_cfg!(debug_assertions) {
+
            assert_matches!($($tt)*);
+
        }
+
    } }
+
}
+

+
#[doc(hidden)]
+
#[macro_export]
+
macro_rules! _assert_matches_cfg {
+
    ( $($tt:tt)* ) => { cfg!($($tt)*) }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use std::panic::{catch_unwind, UnwindSafe};
+

+
    #[derive(Debug)]
+
    enum Foo {
+
        A(i32),
+
        B(&'static str),
+
        C(&'static str),
+
    }
+

+
    #[test]
+
    fn test_assert_succeed() {
+
        let a = Foo::A(123);
+

+
        assert_matches!(a, Foo::A(_));
+
        assert_matches!(a, Foo::A(123));
+
        assert_matches!(a, Foo::A(i) if i == 123);
+
        assert_matches!(a, Foo::A(42) | Foo::A(123));
+

+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B(_));
+
        assert_matches!(b, Foo::B("foo"));
+
        assert_matches!(b, Foo::B(s) if s == "foo");
+
        assert_matches!(b, Foo::B(s) => assert_eq!(s, "foo"));
+
        assert_matches!(b, Foo::B(s) => { assert_eq!(s, "foo") });
+
        assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "foo"));
+
        assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo") });
+

+
        let c = Foo::C("foo");
+

+
        assert_matches!(c, Foo::B(_) | Foo::C(_));
+
        assert_matches!(c, Foo::B("foo") | Foo::C("foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo");
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) => assert_eq!(s, "foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) => { assert_eq!(s, "foo") });
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => assert_eq!(s, "foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => { assert_eq!(s, "foo") });
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_0() {
+
        let a = Foo::A(123);
+

+
        assert_matches!(a, Foo::B(_));
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_1() {
+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B("bar"));
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_2() {
+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B(s) if s == "bar");
+
    }
+

+
    #[test]
+
    fn test_assert_no_move() {
+
        let b = &mut Foo::A(0);
+
        assert_matches!(*b, Foo::A(0));
+
    }
+

+
    #[test]
+
    fn assert_with_message() {
+
        let a = Foo::A(0);
+

+
        assert_matches!(a, Foo::A(_), "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes");
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes");
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0 => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
+
        assert_matches!(a, Foo::A(_), "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(_), "o noes {value:?}", value = a);
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes {value:?}", value=a);
+
    }
+

+
    fn panic_message<F>(f: F) -> String
+
    where
+
        F: FnOnce() + UnwindSafe,
+
    {
+
        let err = catch_unwind(f).expect_err("function did not panic");
+

+
        *err.downcast::<String>()
+
            .expect("function panicked with non-String value")
+
    }
+

+
    #[test]
+
    fn test_panic_message() {
+
        let a = Foo::A(1);
+

+
        // expr, pat
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_));
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
+
        );
+

+
        // expr, pat if cond
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
+
        );
+

+
        // expr, pat => arm
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_) => {});
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
+
        );
+

+
        // expr, pat if cond => arm
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo" => {});
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
+
        );
+

+
        // expr, pat, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_), "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
+
        );
+

+
        // expr, pat if cond, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo", "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
+
        );
+

+
        // expr, pat => arm, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_) => {}, "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
+
        );
+

+
        // expr, pat if cond => arm, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo" => {}, "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
+
        );
+
    }
+
}
added radicle/src/test/fixtures.rs
@@ -0,0 +1,73 @@
+
use std::path::Path;
+

+
use crate::crypto::{Signer, Verified};
+
use crate::git;
+
use crate::identity::Id;
+
use crate::rad;
+
use crate::storage::git::Storage;
+
use crate::storage::refs::SignedRefs;
+
use crate::storage::{BranchName, WriteStorage};
+

+
/// Create a new storage with a project.
+
pub fn storage<P: AsRef<Path>, G: Signer>(path: P, signer: G) -> Result<Storage, rad::InitError> {
+
    let path = path.as_ref();
+
    let storage = Storage::open(path.join("storage"))?;
+

+
    for (name, desc) in [
+
        ("acme", "Acme's repository"),
+
        ("vim", "A text editor"),
+
        ("rx", "A pixel editor"),
+
    ] {
+
        let (repo, _) = repository(path.join("workdir").join(name));
+
        rad::init(
+
            &repo,
+
            name,
+
            desc,
+
            BranchName::from("master"),
+
            &signer,
+
            &storage,
+
        )?;
+
    }
+

+
    Ok(storage)
+
}
+

+
/// Create a new repository at the given path, and initialize it into a project.
+
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
+
    path: P,
+
    storage: &'r S,
+
    signer: G,
+
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
+
    let (repo, head) = repository(path);
+
    let (id, refs) = rad::init(
+
        &repo,
+
        "acme",
+
        "Acme's repository",
+
        BranchName::from("master"),
+
        signer,
+
        storage,
+
    )?;
+

+
    Ok((id, refs, repo, head))
+
}
+

+
/// Creates a regular repository at the given path with a couple of commits.
+
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
+
    let repo = git2::Repository::init(path).unwrap();
+
    let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
+
    let head = git::initial_commit(&repo, &sig).unwrap();
+
    let oid = git::commit(
+
        &repo,
+
        &head,
+
        git::refname!("refs/heads/master").as_refstr(),
+
        "Second commit",
+
        "anonymous",
+
    )
+
    .unwrap()
+
    .id();
+

+
    // Look, I don't really understand why we have to do this, but we do.
+
    drop(head);
+

+
    (repo, oid)
+
}
added radicle/src/test/signer.rs
@@ -0,0 +1,65 @@
+
use crate::crypto::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer};
+

+
#[derive(Debug, Clone)]
+
pub struct MockSigner {
+
    pk: PublicKey,
+
    sk: SecretKey,
+
}
+

+
impl MockSigner {
+
    pub fn new(rng: &mut fastrand::Rng) -> Self {
+
        let mut bytes: [u8; 32] = [0; 32];
+

+
        for byte in &mut bytes {
+
            *byte = rng.u8(..);
+
        }
+
        let seed = Seed::new(bytes);
+
        let keypair = KeyPair::from_seed(seed);
+

+
        Self::from(keypair.sk)
+
    }
+
}
+

+
impl From<SecretKey> for MockSigner {
+
    fn from(sk: SecretKey) -> Self {
+
        let pk = sk.public_key().into();
+
        Self { sk, pk }
+
    }
+
}
+

+
impl Default for MockSigner {
+
    fn default() -> Self {
+
        let seed = Seed::generate();
+
        let keypair = KeyPair::from_seed(seed);
+
        let sk = keypair.sk;
+

+
        Self {
+
            pk: sk.public_key().into(),
+
            sk,
+
        }
+
    }
+
}
+

+
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 {
+
    fn public_key(&self) -> &PublicKey {
+
        &self.pk
+
    }
+

+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.sk.sign(msg, None).into()
+
    }
+
}
added radicle/src/test/storage.rs
@@ -0,0 +1,138 @@
+
use git_url::Url;
+

+
use crate::crypto::{Signer, Verified};
+
use crate::identity::{Id, Project};
+

+
pub use crate::storage::*;
+

+
#[derive(Clone, Debug)]
+
pub struct MockStorage {
+
    pub inventory: Vec<Project>,
+
}
+

+
impl MockStorage {
+
    pub fn new(inventory: Vec<Project>) -> Self {
+
        Self { inventory }
+
    }
+

+
    pub fn empty() -> Self {
+
        Self {
+
            inventory: Vec::new(),
+
        }
+
    }
+
}
+

+
impl ReadStorage for MockStorage {
+
    fn url(&self) -> Url {
+
        Url {
+
            scheme: git_url::Scheme::Radicle,
+
            host: Some("mock".to_string()),
+
            ..Url::default()
+
        }
+
    }
+

+
    fn get(&self, _remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
+
        if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) {
+
            return Ok(Some(proj.clone()));
+
        }
+
        Ok(None)
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        let inventory = self
+
            .inventory
+
            .iter()
+
            .map(|proj| proj.id.clone())
+
            .collect::<Vec<_>>();
+

+
        Ok(inventory)
+
    }
+
}
+

+
impl WriteStorage<'_> for MockStorage {
+
    type Repository = MockRepository;
+

+
    fn repository(&self, _proj: &Id) -> Result<Self::Repository, Error> {
+
        Ok(MockRepository {})
+
    }
+

+
    fn sign_refs<G: Signer>(
+
        &self,
+
        _repository: &Self::Repository,
+
        _signer: G,
+
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
+
        todo!()
+
    }
+
}
+

+
pub struct MockRepository {}
+

+
impl ReadRepository<'_> for MockRepository {
+
    type Remotes = std::iter::Empty<Result<(RemoteId, Remote<Verified>), refs::Error>>;
+

+
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
        Ok(true)
+
    }
+

+
    fn path(&self) -> &std::path::Path {
+
        todo!()
+
    }
+

+
    fn remote(&self, _remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
+
        todo!()
+
    }
+

+
    fn remotes(&self) -> Result<Self::Remotes, git2::Error> {
+
        todo!()
+
    }
+

+
    fn commit(&self, _oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn revwalk(&self, _head: Oid) -> Result<git2::Revwalk, git2::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,
+
        _remote: &RemoteId,
+
        _reference: &git::RefStr,
+
    ) -> Result<Option<git2::Reference>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn reference_oid(
+
        &self,
+
        _remote: &RemoteId,
+
        _reference: &git::RefStr,
+
    ) -> Result<Option<radicle_git_ext::Oid>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> {
+
        todo!()
+
    }
+

+
    fn project(&self) -> Result<Project, Error> {
+
        todo!()
+
    }
+
}
+

+
impl WriteRepository<'_> for MockRepository {
+
    fn fetch(&mut self, _url: &Url) -> Result<Vec<RefUpdate>, FetchError> {
+
        Ok(vec![])
+
    }
+

+
    fn raw(&self) -> &git2::Repository {
+
        todo!()
+
    }
+
}