#![allow(clippy::unwrap_used)]
pub mod arbitrary;
pub mod assert;
pub mod fixtures;
pub mod storage;
use super::storage::{Namespaces, RefUpdate};
use crate::git;
use crate::prelude::NodeId;
use crate::storage::WriteRepository;
/// Perform a fetch between two local repositories.
/// This has the same outcome as doing a "real" fetch, but suffices for the simulation, and
/// doesn't require running nodes.
pub fn fetch<W: WriteRepository>(
repo: &W,
node: &NodeId,
namespaces: impl Into<Namespaces>,
) -> Result<Vec<RefUpdate>, crate::storage::FetchError> {
let namespace = match namespaces.into() {
Namespaces::All => None,
Namespaces::Followed(followed) => followed.into_iter().next(),
};
let mut updates = Vec::new();
let mut callbacks = git::raw::RemoteCallbacks::new();
let mut opts = git::raw::FetchOptions::default();
let refspec = if let Some(namespace) = namespace {
opts.prune(git::raw::FetchPrune::On);
format!("refs/namespaces/{namespace}/refs/*:refs/namespaces/{namespace}/refs/*")
} else {
opts.prune(git::raw::FetchPrune::Off);
"refs/namespaces/*:refs/namespaces/*".to_owned()
};
callbacks.update_tips(|name, old, new| {
if let Ok(name) = git::fmt::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;
}
}
false
});
opts.remote_callbacks(callbacks);
let mut remote = repo.raw().remote_anonymous(
crate::storage::git::transport::remote::Url {
node: *node,
repo: repo.id(),
namespace,
}
.to_string()
.as_str(),
)?;
remote.fetch(&[refspec], Some(&mut opts), None)?;
drop(opts);
repo.set_identity_head()?;
repo.set_head_to_default_branch()?;
repo.set_default_branch_to_canonical_head()?;
let validations = repo.validate()?;
if !validations.is_empty() {
return Err(crate::storage::FetchError::Validation { validations });
}
Ok(updates)
}
pub mod setup {
use std::path::{Path, PathBuf};
use tempfile::{TempDir, tempdir};
use super::storage::{Namespaces, RefUpdate};
use crate::crypto::test::signer::MockSigner;
use crate::node::device::Device;
use crate::storage::git::Repository;
use crate::storage::git::transport::remote;
use crate::{Storage, git, profile::Home, rad::REMOTE_NAME, test::fixtures};
use crate::{prelude::*, rad};
/// A node.
///
/// Note that this isn't a real node; only a profile with storage and a signing key.
pub struct Node {
pub tmp: TempDir,
pub root: PathBuf,
pub storage: Storage,
pub signer: Device<MockSigner>,
}
impl Default for Node {
fn default() -> Self {
let root = tempdir().unwrap();
Self::new(root, MockSigner::default(), "Radcliff")
}
}
impl Node {
pub fn new(tmp: TempDir, signer: MockSigner, alias: &str) -> Self {
let signer = Device::from(signer);
let root = tmp.path().to_path_buf();
let home = root.join("home");
let paths = Home::new(home.as_path()).unwrap();
let storage = Storage::open(
paths.storage(),
git::UserInfo {
alias: Alias::new(alias),
key: *signer.public_key(),
},
)
.unwrap();
remote::mock::register(signer.public_key(), storage.path());
Self {
tmp,
root,
storage,
signer,
}
}
pub fn clone(&mut self, rid: RepoId, other: &Self) {
let repo = self.storage.create(rid).unwrap();
super::fetch(&repo, other.signer.public_key(), Namespaces::All).unwrap();
rad::fork(rid, &self.signer, &self.storage).unwrap();
}
pub fn project(&self) -> NodeRepo {
let (id, _, checkout, _) =
fixtures::project(self.root.join("working"), &self.storage, &self.signer).unwrap();
let repo = self.storage.repository(id).unwrap();
let checkout = Some(NodeRepoCheckout { checkout });
NodeRepo { repo, checkout }
}
}
/// A node repository with an optional checkout.
pub struct NodeRepo {
pub repo: Repository,
pub checkout: Option<NodeRepoCheckout>,
}
impl NodeRepo {
#[track_caller]
pub fn fetch(&self, from: &Node) -> Vec<RefUpdate> {
super::fetch(&self.repo, from.signer.public_key(), Namespaces::All).unwrap()
}
#[track_caller]
pub fn checkout(&self) -> &NodeRepoCheckout {
self.checkout.as_ref().unwrap()
}
}
impl std::ops::Deref for NodeRepo {
type Target = Repository;
fn deref(&self) -> &Self::Target {
&self.repo
}
}
impl std::ops::DerefMut for NodeRepo {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.repo
}
}
/// A repository checkout.
pub struct NodeRepoCheckout {
checkout: git::raw::Repository,
}
impl NodeRepoCheckout {
pub fn branch_with<S: AsRef<Path>, T: AsRef<[u8]>>(
&self,
blobs: impl IntoIterator<Item = (S, T)>,
) -> BranchWith {
let refname =
git::fmt::Qualified::from(git::fmt::lit::refs_heads(git::fmt::refname!("master")));
let base = self.checkout.refname_to_id(refname.as_str()).unwrap();
let parent = self.checkout.find_commit(base).unwrap();
let oid = commit(&self.checkout, &refname, blobs, &[&parent]);
git::push(&self.checkout, &REMOTE_NAME, [(&refname, &refname)]).unwrap();
BranchWith {
base: base.into(),
oid,
}
}
}
impl std::ops::Deref for NodeRepoCheckout {
type Target = git::raw::Repository;
fn deref(&self) -> &Self::Target {
&self.checkout
}
}
/// A node with a repository.
pub struct NodeWithRepo {
pub node: Node,
pub repo: NodeRepo,
}
impl std::ops::Deref for NodeWithRepo {
type Target = Node;
fn deref(&self) -> &Self::Target {
&self.node
}
}
impl std::ops::DerefMut for NodeWithRepo {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.node
}
}
impl Default for NodeWithRepo {
fn default() -> Self {
let node = Node::default();
let repo = node.project();
Self { node, repo }
}
}
/// A network of three nodes.
///
/// Note that these are not actually running nodes in the sense of `radicle-node`.
/// These are simply profiles with their own storage, and the ability to fetch between
/// them.
pub struct Network {
pub alice: NodeWithRepo,
pub bob: NodeWithRepo,
pub eve: NodeWithRepo,
pub rid: RepoId,
}
impl Default for Network {
fn default() -> Self {
let alice = Node::new(tempdir().unwrap(), MockSigner::from_seed([!0; 32]), "alice");
let mut bob = Node::new(tempdir().unwrap(), MockSigner::from_seed([!1; 32]), "bob");
let mut eve = Node::new(tempdir().unwrap(), MockSigner::from_seed([!2; 32]), "eve");
let repo = alice.project();
let rid = repo.id;
bob.clone(repo.id, &alice);
eve.clone(repo.id, &alice);
let alice = NodeWithRepo { node: alice, repo };
let repo = bob.storage.repository(rid).unwrap();
let bob = NodeWithRepo {
node: bob,
repo: NodeRepo {
repo,
checkout: None,
},
};
let repo = eve.storage.repository(rid).unwrap();
let eve = NodeWithRepo {
node: eve,
repo: NodeRepo {
repo,
checkout: None,
},
};
Self {
alice,
bob,
eve,
rid,
}
}
}
#[derive(Debug)]
pub struct BranchWith {
pub base: git::Oid,
pub oid: git::Oid,
}
pub fn commit<S: AsRef<Path>, T: AsRef<[u8]>>(
repo: &git::raw::Repository,
refname: &git::fmt::Qualified,
blobs: impl IntoIterator<Item = (S, T)>,
parents: &[&git::raw::Commit<'_>],
) -> crate::git::Oid {
let tree = {
let mut tb = repo.treebuilder(None).unwrap();
for (name, blob) in blobs.into_iter() {
let oid = repo.blob(blob.as_ref()).unwrap();
tb.insert(name.as_ref(), oid, git::raw::FileMode::Blob.into())
.unwrap();
}
tb.write().unwrap()
};
let tree = repo.find_tree(tree).unwrap();
let author = git::raw::Signature::now("anonymous", "anonymous@example.com").unwrap();
repo.commit(
Some(refname.as_str()),
&author,
&author,
"Making changes",
&tree,
parents,
)
.unwrap()
.into()
}
}