radicle-heartwood-lfs/radicle/src/storage/git.rs

936 lines
31 KiB
Rust

pub mod cob;
pub mod transport;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::{fs, io};
use crypto::{Signer, Unverified, Verified};
use git_ref_format::refspec;
use once_cell::sync::Lazy;
use crate::git;
use crate::identity;
use crate::identity::{doc, Doc, Id};
use crate::identity::{Identity, IdentityError, Project};
use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs};
use crate::storage::{
Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, Remotes, WriteRepository,
WriteStorage,
};
pub use crate::git::*;
use super::{Namespaces, RefUpdate, RemoteId};
use transport::remote;
pub static NAMESPACES_GLOB: Lazy<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
pub static SIGREFS_GLOB: Lazy<refspec::PatternString> =
Lazy::new(|| refspec::pattern!("refs/namespaces/*/rad/sigrefs"));
// TODO: Is this is the wrong place for this type?
#[derive(Error, Debug)]
pub enum ProjectError {
#[error("identity branches diverge from each other")]
BranchesDiverge,
#[error("identity branches are in an invalid state")]
InvalidState,
#[error("storage error: {0}")]
Storage(#[from] Error),
#[error("identity document error: {0}")]
Doc(#[from] doc::DocError),
#[error("payload error: {0}")]
Payload(#[from] doc::PayloadError),
#[error("git: {0}")]
Git(#[from] git2::Error),
#[error("git: {0}")]
GitExt(#[from] git::Error),
#[error("refs: {0}")]
Refs(#[from] refs::Error),
}
impl ProjectError {
/// Whether this error is caused by the project not being found.
pub fn is_not_found(&self) -> bool {
match self {
Self::Doc(doc) => doc.is_not_found(),
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct Storage {
path: PathBuf,
}
impl ReadStorage for Storage {
fn path(&self) -> &Path {
self.path.as_path()
}
fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, ProjectError> {
// TODO: Don't create a repo here if it doesn't exist?
// Perhaps for checking we could have a `contains` method?
match self.repository(proj)?.identity_of(remote) {
Ok(doc) => Ok(Some(doc)),
Err(err) if err.is_not_found() => Ok(None),
Err(err) => Err(err),
}
}
fn inventory(&self) -> Result<Inventory, Error> {
self.projects()
}
}
impl WriteStorage for Storage {
type Repository = Repository;
fn repository(&self, proj: Id) -> Result<Self::Repository, Error> {
Repository::open(paths::repository(self, &proj), proj)
}
}
impl Storage {
// TODO: Return a better error when not found.
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.urn(), oid, name);
}
}
Ok(())
}
}
pub struct Repository {
pub id: Id,
pub(crate) backend: git2::Repository,
}
#[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 identity: {0}")]
InvalidIdentity(#[from] IdentityError),
#[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, id: Id) -> 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 { id, backend })
}
/// Create the repository's identity branch.
pub fn init<G: Signer>(
doc: &Doc<Verified>,
remote: &RemoteId,
storage: &Storage,
signer: &G,
) -> Result<(Self, git::Oid), Error> {
let (doc_oid, doc) = doc.encode()?;
let id = Id::from(doc_oid);
let repo = Self::open(paths::repository(storage, &id), id)?;
let oid = Doc::init(
doc.as_slice(),
remote,
&[(signer.public_key(), signer.sign(&doc))],
repo.raw(),
)?;
Ok((repo, oid))
}
/// Verify all references in the repository, checking that they are signed
/// as part of 'sigrefs'. Also verify that no signed reference is missing
/// from the repository.
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 entry in self.namespaced_references()? {
let (remote_id, refname, oid) = entry?;
let remote = remotes
.get_mut(&remote_id)
.ok_or(VerifyError::InvalidRemote(remote_id))?;
let refname = RefString::from(refname);
let signed_oid = remote
.remove(&refname)
.ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
if oid != signed_oid {
return Err(VerifyError::InvalidRefTarget(remote_id, refname, *oid));
}
}
for (remote, refs) in remotes.into_iter() {
// The refs that are left in the map, are ones that were signed, but are not
// in the repository.
if let Some((name, _)) = refs.into_iter().next() {
return Err(VerifyError::MissingRef(remote, name));
}
// Verify identity history of remote.
self.identity(&remote)?.verified(self.id)?;
}
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(&self, remote: &RemoteId) -> Result<Identity<Oid>, IdentityError> {
Identity::load(remote, self)
}
pub fn project_of(&self, remote: &RemoteId) -> Result<Project, ProjectError> {
let doc = self.identity_of(remote)?;
let proj = doc.project()?;
Ok(proj)
}
pub fn identity_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, ProjectError> {
let (doc, _) = identity::Doc::load(remote, self)?;
let verified = doc.verified()?;
Ok(verified)
}
/// Return the canonical identity [`git::Oid`] and document.
pub fn identity_doc(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
let mut heads = Vec::new();
for remote in self.remote_ids()? {
let remote = remote?;
let oid = Doc::<Unverified>::head(&remote, self)?;
heads.push(oid.into());
}
// Keep track of the longest identity branch.
let mut longest = heads.pop().ok_or(ProjectError::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(ProjectError::BranchesDiverge);
}
}
Doc::<Unverified>::load_at(longest.into(), self)
.map(|(doc, _)| (longest.into(), doc))
.map_err(ProjectError::from)
}
pub fn remote_ids(
&self,
) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git2::Error> {
let iter = self.backend.references_glob(SIGREFS_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_namespaced::<RemoteId>(name)?;
Ok(id)
},
);
Ok(iter)
}
pub fn remotes(
&self,
) -> Result<
impl Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + '_,
git2::Error,
> {
let remotes =
self.backend
.references_glob(SIGREFS_GLOB.as_str())?
.map(|reference| -> Result<_, _> {
let r = reference?;
let name = r.name().ok_or(refs::Error::InvalidRef)?;
let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
let remote = self.remote(&id)?;
Ok((id, remote))
});
Ok(remotes)
}
/// Return all references that are namespaced, ie. that are signed by a node and verified.
fn namespaced_references(
&self,
) -> Result<impl Iterator<Item = Result<(RemoteId, Qualified, Oid), refs::Error>>, git2::Error>
{
let refs = self.backend.references_glob("refs/namespaces/*")?;
let refs = refs
.map(|reference| {
let r = reference?;
let name = r.name().ok_or(refs::Error::InvalidRef)?;
let (namespace, refname) = git::parse_ref_namespaced::<RemoteId>(name)?;
let Some(oid) = r.target() else {
// Ignore symbolic refs, eg. `HEAD`.
return Ok(None);
};
if refname == *refs::SIGREFS_BRANCH {
// Ignore the signed-refs reference, as this is what we're verifying.
return Ok(None);
}
Ok(Some((namespace, refname.to_owned(), oid.into())))
})
.filter_map(Result::transpose);
Ok(refs)
}
}
impl ReadRepository for Repository {
fn is_empty(&self) -> Result<bool, git2::Error> {
Ok(self.remotes()?.next().is_none())
}
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::Qualified,
) -> Result<git2::Reference, git::Error> {
let name = name.with_namespace(remote.into());
self.backend.find_reference(&name).map_err(git::Error::from)
}
fn reference_oid(
&self,
remote: &RemoteId,
reference: &git::Qualified,
) -> Result<Oid, git::Error> {
let name = reference.with_namespace(remote.into());
let oid = self.backend.refname_to_id(&name)?;
Ok(oid.into())
}
fn commit(&self, oid: Oid) -> Result<git2::Commit, git::Error> {
self.backend
.find_commit(oid.into())
.map_err(git::Error::from)
}
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
let mut revwalk = self.backend.revwalk()?;
revwalk.push(head.into())?;
Ok(revwalk)
}
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/namespaces/{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.into(), oid.into());
}
Ok(refs.into())
}
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
let mut remotes = Vec::new();
for remote in Repository::remotes(self)? {
remotes.push(remote?);
}
Ok(Remotes::from_iter(remotes))
}
fn project(&self) -> Result<Doc<Verified>, Error> {
todo!()
}
fn project_identity(&self) -> Result<(Oid, identity::Doc<Unverified>), ProjectError> {
Repository::identity_doc(self)
}
fn head(&self) -> Result<(Qualified, Oid), ProjectError> {
// If `HEAD` is already set locally, just return that.
if let Ok(head) = self.backend.head() {
if let Ok((name, oid)) = git::refs::qualified_from(&head) {
return Ok((name.to_owned(), oid));
}
}
self.canonical_head()
}
fn canonical_head(&self) -> Result<(Qualified, Oid), ProjectError> {
// TODO: In the `fork` function for example, we call Repository::project_identity again,
// This should only be necessary once.
let (_, doc) = self.project_identity()?;
let doc = doc.verified()?;
let project = doc.project()?;
let branch_ref = Qualified::from(lit::refs_heads(&project.default_branch()));
let raw = self.raw();
let mut heads = Vec::new();
for delegate in doc.delegates.iter() {
let r = self.reference_oid(delegate, &branch_ref)?.into();
heads.push(r);
}
let oid = match heads.as_slice() {
[head] => Ok(*head),
// FIXME: This branch is not tested.
heads => raw.merge_base_many(heads),
}?;
Ok((branch_ref, oid.into()))
}
}
impl WriteRepository for Repository {
/// Fetch all remotes of a project from the given URL.
/// This is the primary way in which projects are updated on the network.
///
/// Since we're operating in an untrusted network, we have to be take some precautions
/// when fetching from a remote. We don't want to fetch straight into a public facing
/// repository because if the updates were to be invalid, we'd be allowing others to
/// read this invalid state. We also don't want to lock our repositories during the fetch
/// or verification, as this will make the repositories unavailable. Therefore, we choose
/// to perform the fetch into a "staging" copy of the given repository we're fetching, and
/// then transfer the changes to the canonical, public copy of the repository.
///
/// To do this, we first create a temporary directory, and clone the canonical repo into it.
/// This local clone takes advantage of the fact that both repositories live on the same
/// host (or even file-system). We now have a "staging" copy and the canonical copy.
///
/// We then fetch the *remote* repo into the *staging* copy. We turn off pruning because we
/// don't want to accidentally delete any objects before verification is complete.
///
/// We proceed to verify the staging copy through the usual verification process.
///
/// If verification succeeds, we fetch from the staging copy into the canonical repo,
/// with pruning *on*, and discard the staging copy. If it fails, we just discard the
/// staging copy.
///
fn fetch(
&mut self,
node: &RemoteId,
namespaces: impl Into<Namespaces>,
) -> Result<Vec<RefUpdate>, FetchError> {
// The steps are summarized in the following diagram:
//
// staging <- git-clone -- local (canonical) # create staging copy
// staging <- git-fetch -- remote # fetch from remote
//
// ... verify ...
//
// local <- git-fetch -- staging # fetch from staging copy
//
let namespace = match namespaces.into() {
Namespaces::All => None,
Namespaces::One(ns) => Some(ns),
};
let mut updates = Vec::new();
let mut callbacks = git2::RemoteCallbacks::new();
let tempdir = tempfile::tempdir()?;
// Create staging copy.
let staging = {
let mut builder = git2::build::RepoBuilder::new();
let path = tempdir.path().join("git");
let staging_repo = builder
.bare(true)
// Using `clone_local` will try to hard-link the ODBs for better performance.
// 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::File::new(self.backend.path().to_path_buf())
.to_string()
.as_str(),
&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);
// Fetch from the remote into the staging copy.
staging_repo
.remote_anonymous(
remote::Url {
node: *node,
repo: self.id,
namespace,
}
.to_string()
.as_str(),
)?
.fetch(&["refs/*:refs/*"], Some(&mut opts), None)?;
// Verify the staging copy as if it was the canonical copy.
Repository {
id: self.id,
backend: staging_repo,
}
.verify()?;
path
};
callbacks.update_tips(|name, old, new| {
if let Ok(name) = git::RefString::try_from(name) {
if name.to_namespaced().is_some() {
updates.push(RefUpdate::from(name, old, new));
// Returning `true` ensures the process is not aborted.
return true;
}
}
log::warn!("Invalid ref `{}` detected; aborting fetch", name);
false
});
{
let mut remote = self
.backend
.remote_anonymous(git::url::File::new(staging).to_string().as_str())?;
let mut opts = git2::FetchOptions::default();
opts.remote_callbacks(callbacks);
let refspec = if let Some(namespace) = namespace {
format!("refs/namespaces/{namespace}/refs/*:refs/namespaces/{namespace}/refs/*")
} else {
"refs/namespaces/*:refs/namespaces/*".to_owned()
};
// 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);
// Fetch from the staging copy into the canonical repo.
remote.fetch(&[refspec], Some(&mut opts), None)?;
}
// Set repository HEAD for git cloning support.
self.set_head()?;
Ok(updates)
}
fn set_head(&self) -> Result<Oid, ProjectError> {
let head_ref = refname!("HEAD");
let (branch_ref, head) = self.canonical_head()?;
log::debug!("Setting ref: {} -> {}", &branch_ref, head);
self.raw()
.reference(&branch_ref, *head, true, "set-local-branch (radicle)")?;
log::debug!("Setting ref: {} -> {}", head_ref, branch_ref);
self.raw()
.reference_symbolic(&head_ref, &branch_ref, true, "set-head (radicle)")?;
Ok(head)
}
fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, Error> {
let remote = signer.public_key();
let refs = self.references(remote)?;
let signed = refs.signed(signer)?;
signed.save(remote, self)?;
Ok(signed)
}
fn raw(&self) -> &git2::Repository {
&self.backend
}
}
pub mod trailers {
use std::str::FromStr;
use thiserror::Error;
use super::*;
use crypto::{PublicKey, PublicKeyError};
use 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<HashMap<PublicKey, Signature>, Error> {
let trailers =
git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
let mut signatures = HashMap::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.insert(pk, sig);
} else {
return Err(Error::SignatureTrailerFormat);
}
}
}
Ok(signatures)
}
}
pub mod paths {
use std::path::PathBuf;
use super::Id;
use super::ReadStorage;
pub fn repository<S: ReadStorage>(storage: &S, proj: &Id) -> PathBuf {
storage.path().join(proj.canonical())
}
}
#[cfg(test)]
mod tests {
use crypto::test::signer::MockSigner;
use super::*;
use crate::assert_matches;
use crate::git;
use crate::storage::refs::SIGREFS_BRANCH;
use crate::storage::{ReadRepository, ReadStorage, RefUpdate, WriteRepository};
use crate::test::arbitrary;
use crate::test::fixtures;
#[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::from(*proj)).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() {
let sigref = (*SIGREFS_BRANCH).to_ref_string();
remote.remove(&sigref).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_pk = *alice_signer.public_key();
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 = Qualified::from_refstr(git::refname!("refs/heads/master")).unwrap();
// Have Bob fetch Alice's refs.
let updates = bob
.repository(proj)
.unwrap()
.fetch(&alice_pk, alice_pk)
.unwrap();
// Three 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/namespaces")
);
}
for remote in remotes {
let (id, _) = remote.unwrap();
let alice_repo = alice.repository(proj).unwrap();
let alice_oid = alice_repo.reference(&id, &refname).unwrap();
let bob_repo = bob.repository(proj).unwrap();
let bob_oid = bob_repo.reference(&id, &refname).unwrap();
assert_eq!(alice_oid.target(), bob_oid.target());
}
// Canonical HEAD is set correctly.
let alice_repo = alice.repository(proj).unwrap();
let bob_repo = bob.repository(proj).unwrap();
assert_eq!(
bob_repo.backend.head().unwrap().target().unwrap(),
alice_repo.backend.head().unwrap().target().unwrap()
);
}
#[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 = Qualified::from_refstr(git::refname!("refs/heads/master")).unwrap();
transport::remote::mock::register(alice_id, alice.path());
// Have Bob fetch Alice's refs.
let updates = bob
.repository(proj_id)
.unwrap()
.fetch(alice_signer.public_key(), *alice_signer.public_key())
.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_sig = git2::Signature::now("Alice", "alice@radicle.xyz").unwrap();
let alice_head = git::commit(
&proj_repo,
&alice_head,
&refname,
"Making changes",
&alice_sig,
)
.unwrap()
.id();
git::push(&proj_repo, "rad", [(&refname, &refname)]).unwrap();
alice_proj_storage.sign_refs(&alice_signer).unwrap();
alice_proj_storage.set_head().unwrap();
// Have Bob fetch Alice's new commit.
let updates = bob
.repository(proj_id)
.unwrap()
.fetch(alice_signer.public_key(), *alice_signer.public_key())
.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();
assert_eq!(bob_master.target().unwrap(), alice_head);
}
#[test]
fn test_namespaced_references() {
let tmp = tempfile::tempdir().unwrap();
let signer = MockSigner::default();
let storage = Storage::open(tmp.path().join("storage")).unwrap();
transport::local::register(storage.clone());
let (id, _, _, _) =
fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
let proj = storage.repository(id).unwrap();
let mut refs = proj
.namespaced_references()
.unwrap()
.map(|r| r.unwrap())
.map(|(_, r, _)| r.to_string())
.collect::<Vec<_>>();
refs.sort();
assert_eq!(refs, vec!["refs/heads/master", "refs/rad/id"]);
}
#[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",
&sig,
)
.unwrap();
let signed = project.sign_refs(&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.
let sigref = (*SIGREFS_BRANCH).to_ref_string();
unsigned.remove(&sigref).unwrap();
assert_eq!(remote.refs, signed);
assert_eq!(*remote.refs, unsigned);
}
}