node: Properly validate signed refs on fetch
With this change, we not only verify the signed refs before the repository "transfer", but also validate that all refs in the repo are signed and there is no discrepancy with the signed refs file.
This commit is contained in:
parent
d9dd28b347
commit
0e7ffcc916
|
|
@ -22,7 +22,7 @@ use radicle::node::Handle as _;
|
||||||
use radicle::profile::Home;
|
use radicle::profile::Home;
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
use radicle::rad;
|
use radicle::rad;
|
||||||
use radicle::storage::ReadStorage as _;
|
use radicle::storage::{ReadStorage as _, WriteRepository};
|
||||||
use radicle::test::fixtures;
|
use radicle::test::fixtures;
|
||||||
use radicle::Storage;
|
use radicle::Storage;
|
||||||
|
|
||||||
|
|
@ -366,6 +366,12 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
|
||||||
}
|
}
|
||||||
git::push(repo, "rad", refs.iter().map(|(a, b)| (a, b))).unwrap();
|
git::push(repo, "rad", refs.iter().map(|(a, b)| (a, b))).unwrap();
|
||||||
|
|
||||||
|
self.storage
|
||||||
|
.repository(id)
|
||||||
|
.unwrap()
|
||||||
|
.sign_refs(&self.signer)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -699,7 +699,7 @@ fn fetch<W: WriteRepository>(
|
||||||
remote.fetch(&[refspec], Some(&mut opts), None)?;
|
remote.fetch(&[refspec], Some(&mut opts), None)?;
|
||||||
drop(opts);
|
drop(opts);
|
||||||
|
|
||||||
repo.verify()?;
|
repo.validate()?;
|
||||||
repo.set_head()?;
|
repo.set_head()?;
|
||||||
|
|
||||||
Ok(updates)
|
Ok(updates)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use std::{collections::HashSet, thread, time};
|
use std::{collections::HashSet, thread, time};
|
||||||
|
|
||||||
use radicle::crypto::{test::signer::MockSigner, Signer};
|
use radicle::crypto::{test::signer::MockSigner, Signer};
|
||||||
|
use radicle::git;
|
||||||
use radicle::node::{FetchResult, Handle as _};
|
use radicle::node::{FetchResult, Handle as _};
|
||||||
use radicle::storage::{ReadRepository, ReadStorage};
|
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository, WriteStorage};
|
||||||
use radicle::test::fixtures;
|
use radicle::test::fixtures;
|
||||||
use radicle::{assert_matches, rad};
|
use radicle::{assert_matches, rad};
|
||||||
|
|
||||||
|
|
@ -194,7 +195,90 @@ fn test_replication() {
|
||||||
|
|
||||||
assert_eq!(inventory.first(), Some(&acme));
|
assert_eq!(inventory.first(), Some(&acme));
|
||||||
assert_eq!(alice_refs, bob_refs);
|
assert_eq!(alice_refs, bob_refs);
|
||||||
assert_matches!(alice.storage.repository(acme).unwrap().verify(), Ok(()));
|
assert_matches!(alice.storage.repository(acme).unwrap().validate(), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replication_no_delegates() {
|
||||||
|
logger::init(log::Level::Debug);
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let alice = Node::init(tmp.path());
|
||||||
|
let mut bob = Node::init(tmp.path());
|
||||||
|
let (repo, _) = fixtures::repository(tmp.path().join("acme"));
|
||||||
|
|
||||||
|
let acme = bob.project_from("acme", "", &repo);
|
||||||
|
|
||||||
|
// Populate repo, but don't sign the refs.
|
||||||
|
let branches = fixtures::populate(&repo, 1);
|
||||||
|
git::push(&repo, "rad", branches.iter().map(|b| (b, b))).unwrap();
|
||||||
|
|
||||||
|
let mut alice = alice.spawn(service::Config::default());
|
||||||
|
let bob = bob.spawn(service::Config::default());
|
||||||
|
|
||||||
|
alice.connect(&bob);
|
||||||
|
converge([&alice, &bob]);
|
||||||
|
|
||||||
|
alice.handle.track_repo(acme, Scope::All).unwrap();
|
||||||
|
let result = alice.handle.fetch(acme, bob.id).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
result,
|
||||||
|
FetchResult::Failed {
|
||||||
|
reason
|
||||||
|
} if reason == "no delegates in transfer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replication_invalid() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let alice = Node::init(tmp.path());
|
||||||
|
let mut bob = Node::init(tmp.path());
|
||||||
|
let carol = MockSigner::default();
|
||||||
|
let acme = bob.project("acme", "");
|
||||||
|
let repo = bob.storage.repository_mut(acme).unwrap();
|
||||||
|
let (_, head) = repo.head().unwrap();
|
||||||
|
let id = repo.identity_head().unwrap();
|
||||||
|
|
||||||
|
// Create some unsigned refs for Carol in Bob's storage.
|
||||||
|
repo.raw()
|
||||||
|
.reference(
|
||||||
|
&git::qualified!("refs/heads/carol").with_namespace(carol.public_key().into()),
|
||||||
|
*head,
|
||||||
|
true,
|
||||||
|
&String::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
repo.raw()
|
||||||
|
.reference(
|
||||||
|
&git::refs::storage::id(carol.public_key()),
|
||||||
|
id.into(),
|
||||||
|
true,
|
||||||
|
&String::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut alice = alice.spawn(service::Config::default());
|
||||||
|
let bob = bob.spawn(service::Config::default());
|
||||||
|
|
||||||
|
alice.connect(&bob);
|
||||||
|
converge([&alice, &bob]);
|
||||||
|
|
||||||
|
alice.handle.track_node(*carol.public_key(), None).unwrap();
|
||||||
|
alice.handle.track_repo(acme, Scope::Trusted).unwrap();
|
||||||
|
let result = alice.handle.fetch(acme, bob.id).unwrap();
|
||||||
|
|
||||||
|
// Fetch is successful despite not fetching Carol's refs, since she isn't a delegate.
|
||||||
|
assert!(result.is_success());
|
||||||
|
|
||||||
|
let repo = alice.storage.repository(acme).unwrap();
|
||||||
|
let mut remotes = repo.remote_ids().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(remotes.next().unwrap().unwrap(), bob.id);
|
||||||
|
assert!(remotes.next().is_none());
|
||||||
|
|
||||||
|
repo.validate().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -244,7 +328,7 @@ fn test_migrated_clone() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(alice_refs, bob_refs);
|
assert_eq!(alice_refs, bob_refs);
|
||||||
assert_matches!(alice.storage.repository(acme).unwrap().verify(), Ok(()));
|
assert_matches!(alice.storage.repository(acme).unwrap().validate(), Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,8 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|(remote, verified)| match verified {
|
.flat_map(|(remote, verified)| match verified {
|
||||||
VerifiedRemote::Failed { reason } => {
|
VerifiedRemote::Failed { reason } => {
|
||||||
|
// TODO: We should include the skipped remotes in the fetch result,
|
||||||
|
// with the reason why they're skipped.
|
||||||
log::warn!(
|
log::warn!(
|
||||||
target: "worker",
|
target: "worker",
|
||||||
"{remote} failed to verify, will not fetch any further refs: {reason}",
|
"{remote} failed to verify, will not fetch any further refs: {reason}",
|
||||||
|
|
@ -276,21 +278,23 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
|
|
||||||
for refname in [heads, cobs, tags, notes] {
|
for refname in [heads, cobs, tags, notes] {
|
||||||
let pattern = refname.with_pattern(git::refspec::STAR);
|
let pattern = refname.with_pattern(git::refspec::STAR);
|
||||||
refspecs.push(
|
refspecs.push((
|
||||||
|
remote.id,
|
||||||
Refspec {
|
Refspec {
|
||||||
src: pattern.clone(),
|
src: pattern.clone(),
|
||||||
dst: pattern,
|
dst: pattern,
|
||||||
force: true,
|
force: true,
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then add the special refs.
|
// Then add the special refs.
|
||||||
let id = ns.join(&*radicle::git::refs::storage::IDENTITY_BRANCH);
|
let id = ns.join(&*radicle::git::refs::storage::IDENTITY_BRANCH);
|
||||||
let sigrefs = ns.join(&*radicle::git::refs::storage::SIGREFS_BRANCH);
|
let sigrefs = ns.join(&*radicle::git::refs::storage::SIGREFS_BRANCH);
|
||||||
|
|
||||||
refspecs.push(
|
refspecs.push((
|
||||||
|
remote.id,
|
||||||
Refspec {
|
Refspec {
|
||||||
src: id.clone(),
|
src: id.clone(),
|
||||||
dst: id,
|
dst: id,
|
||||||
|
|
@ -298,8 +302,9 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
force: true,
|
force: true,
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
));
|
||||||
refspecs.push(
|
refspecs.push((
|
||||||
|
remote.id,
|
||||||
Refspec {
|
Refspec {
|
||||||
src: sigrefs.clone(),
|
src: sigrefs.clone(),
|
||||||
dst: sigrefs,
|
dst: sigrefs,
|
||||||
|
|
@ -307,11 +312,22 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
force: false,
|
force: false,
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
));
|
||||||
refspecs
|
refspecs
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let (fetching, specs): (HashSet<_>, Vec<_>) = specs.into_iter().unzip();
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.repo
|
||||||
|
.delegates()?
|
||||||
|
.iter()
|
||||||
|
.all(|d| fetching.contains(d.as_key()))
|
||||||
|
{
|
||||||
|
return Err(error::Transfer::NoDelegates);
|
||||||
|
}
|
||||||
log::debug!(target: "worker", "Transferring staging to production {url}");
|
log::debug!(target: "worker", "Transferring staging to production {url}");
|
||||||
|
|
||||||
let mut opts = git::raw::FetchOptions::default();
|
let mut opts = git::raw::FetchOptions::default();
|
||||||
|
|
@ -349,13 +365,13 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
self.trusted
|
self.trusted
|
||||||
.iter()
|
.iter()
|
||||||
.map(|remote| {
|
.map(|remote| {
|
||||||
let verification = match (
|
let verification =
|
||||||
self.repo.identity_doc_of(remote),
|
match (self.repo.identity_doc_of(remote), self.repo.remote(remote)) {
|
||||||
SignedRefs::load(remote, self.repo.deref()),
|
(Ok(doc), Ok(remote)) => match self.repo.validate_remote(&remote) {
|
||||||
) {
|
Ok(()) => VerifiedRemote::Success { _doc: doc, remote },
|
||||||
(Ok(doc), Ok(refs)) => VerifiedRemote::Success {
|
Err(e) => VerifiedRemote::Failed {
|
||||||
_doc: doc,
|
reason: e.to_string(),
|
||||||
remote: Remote::new(*remote, refs),
|
},
|
||||||
},
|
},
|
||||||
(Err(e), _) => VerifiedRemote::Failed {
|
(Err(e), _) => VerifiedRemote::Failed {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ pub enum Transfer {
|
||||||
Identity(#[from] identity::IdentityError),
|
Identity(#[from] identity::IdentityError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Storage(#[from] storage::Error),
|
Storage(#[from] storage::Error),
|
||||||
|
#[error("no delegates in transfer")]
|
||||||
|
NoDelegates,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ impl Seeds {
|
||||||
#[serde(tag = "status", rename_all = "kebab-case")]
|
#[serde(tag = "status", rename_all = "kebab-case")]
|
||||||
pub enum FetchResult {
|
pub enum FetchResult {
|
||||||
Success { updated: Vec<RefUpdate> },
|
Success { updated: Vec<RefUpdate> },
|
||||||
|
// TODO: Create enum for reason.
|
||||||
Failed { reason: String },
|
Failed { reason: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -296,10 +296,18 @@ pub trait ReadRepository {
|
||||||
fn blob_at<'a>(&'a self, commit: Oid, path: &'a Path)
|
fn blob_at<'a>(&'a self, commit: Oid, path: &'a Path)
|
||||||
-> Result<git2::Blob<'a>, git_ext::Error>;
|
-> Result<git2::Blob<'a>, git_ext::Error>;
|
||||||
|
|
||||||
/// Verify all references in the repository, checking that they are signed
|
/// Validate all remotes with [`ReadStorage::validate`].
|
||||||
/// as part of 'sigrefs'. Also verify that no signed reference is missing
|
fn validate(&self) -> Result<(), VerifyError> {
|
||||||
/// from the repository.
|
for (_, remote) in self.remotes()? {
|
||||||
fn verify(&self) -> Result<(), VerifyError>;
|
self.validate_remote(&remote)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate all the remote's references in the repository, checking that they are signed as
|
||||||
|
/// part of the remote's signed refs. Also verify that no signed reference is missing from the
|
||||||
|
/// repository.
|
||||||
|
fn validate_remote(&self, remote: &Remote<Verified>) -> Result<(), VerifyError>;
|
||||||
|
|
||||||
/// Get the head of this repository.
|
/// Get the head of this repository.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,8 @@ pub enum VerifyError {
|
||||||
MissingRef(RemoteId, git::RefString),
|
MissingRef(RemoteId, git::RefString),
|
||||||
#[error("git: {0}")]
|
#[error("git: {0}")]
|
||||||
Git(#[from] git2::Error),
|
Git(#[from] git2::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Storage(#[from] Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
|
|
@ -346,33 +348,6 @@ impl Repository {
|
||||||
});
|
});
|
||||||
Ok(remotes)
|
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 {
|
impl ReadRepository for Repository {
|
||||||
|
|
@ -396,39 +371,32 @@ impl ReadRepository for Repository {
|
||||||
.get(&self.backend)
|
.get(&self.backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self) -> Result<(), VerifyError> {
|
fn validate_remote(&self, remote: &Remote<Verified>) -> Result<(), VerifyError> {
|
||||||
let mut remotes: HashMap<RemoteId, Refs> = self
|
// Contains a copy of the signed refs of this remote.
|
||||||
.remotes()?
|
let mut refs = BTreeMap::from((*remote.refs).clone());
|
||||||
.map(|remote| {
|
|
||||||
let (id, remote) = remote?;
|
|
||||||
Ok((id, remote.refs.into()))
|
|
||||||
})
|
|
||||||
.collect::<Result<_, VerifyError>>()?;
|
|
||||||
|
|
||||||
for entry in self.namespaced_references()? {
|
// Check all repository references, making sure they are present in the signed refs map.
|
||||||
let (remote_id, refname, oid) = entry?;
|
for (refname, oid) in self.references_of(&remote.id)? {
|
||||||
let remote = remotes
|
// Skip validation of the signed refs branch, as it is not part of `Remote`.
|
||||||
.get_mut(&remote_id)
|
if refname == refs::SIGREFS_BRANCH.to_ref_string() {
|
||||||
.ok_or(VerifyError::InvalidRemote(remote_id))?;
|
continue;
|
||||||
let refname = RefString::from(refname);
|
}
|
||||||
let signed_oid = remote
|
let signed_oid = refs
|
||||||
.remove(&refname)
|
.remove(&refname)
|
||||||
.ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
|
.ok_or_else(|| VerifyError::UnknownRef(remote.id, refname.clone()))?;
|
||||||
|
|
||||||
if oid != signed_oid {
|
if oid != signed_oid {
|
||||||
return Err(VerifyError::InvalidRefTarget(remote_id, refname, *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
|
// The refs that are left in the map, are ones that were signed, but are not
|
||||||
// in the repository.
|
// in the repository. If any are left, bail.
|
||||||
if let Some((name, _)) = refs.into_iter().next() {
|
if let Some((name, _)) = refs.into_iter().next() {
|
||||||
return Err(VerifyError::MissingRef(remote, name));
|
return Err(VerifyError::MissingRef(remote.id, name));
|
||||||
}
|
|
||||||
// Verify identity history of remote.
|
|
||||||
self.identity_of(&remote)?.verified(self.id)?;
|
|
||||||
}
|
}
|
||||||
|
// Finally, verify the identity history of remote.
|
||||||
|
self.identity_of(&remote.id)?.verified(self.id)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -727,7 +695,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_namespaced_references() {
|
fn test_references_of() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let signer = MockSigner::default();
|
let signer = MockSigner::default();
|
||||||
let storage = Storage::open(tmp.path().join("storage")).unwrap();
|
let storage = Storage::open(tmp.path().join("storage")).unwrap();
|
||||||
|
|
@ -739,14 +707,17 @@ mod tests {
|
||||||
let proj = storage.repository(id).unwrap();
|
let proj = storage.repository(id).unwrap();
|
||||||
|
|
||||||
let mut refs = proj
|
let mut refs = proj
|
||||||
.namespaced_references()
|
.references_of(signer.public_key())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|r| r.unwrap())
|
.iter()
|
||||||
.map(|(_, r, _)| r.to_string())
|
.map(|(r, _)| r.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
refs.sort();
|
refs.sort();
|
||||||
|
|
||||||
assert_eq!(refs, vec!["refs/heads/master", "refs/rad/id"]);
|
assert_eq!(
|
||||||
|
refs,
|
||||||
|
vec!["refs/heads/master", "refs/rad/id", "refs/rad/sigrefs"]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -86,24 +86,26 @@ pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate a repository with commits, branches and blobs.
|
/// Populate a repository with commits, branches and blobs.
|
||||||
pub fn populate(repo: &git2::Repository, scale: usize) {
|
pub fn populate(repo: &git2::Repository, scale: usize) -> Vec<git::Qualified> {
|
||||||
assert!(
|
assert!(
|
||||||
scale <= 8,
|
scale <= 8,
|
||||||
"Scale parameter must be less than or equal to 8"
|
"Scale parameter must be less than or equal to 8"
|
||||||
);
|
);
|
||||||
if scale == 0 {
|
if scale == 0 {
|
||||||
return;
|
return vec![];
|
||||||
}
|
}
|
||||||
let head = repo.head().unwrap().peel_to_commit().unwrap();
|
let head = repo.head().unwrap().peel_to_commit().unwrap();
|
||||||
let rng = fastrand::Rng::with_seed(42);
|
let rng = fastrand::Rng::with_seed(42);
|
||||||
let mut buffer = vec![0; 1024 * 1024 * scale];
|
let mut buffer = vec![0; 1024 * 1024 * scale];
|
||||||
|
let mut refs = Vec::new();
|
||||||
|
|
||||||
for _ in 0..scale {
|
for _ in 0..scale {
|
||||||
let random = std::iter::repeat_with(|| rng.alphanumeric())
|
let random = std::iter::repeat_with(|| rng.alphanumeric())
|
||||||
.take(7)
|
.take(7)
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
let name = format!("feature/{random}");
|
let name =
|
||||||
|
git::refname!("feature").join(git::RefString::try_from(random.as_str()).unwrap());
|
||||||
let signature = git2::Signature::now("Radicle", "radicle@radicle.xyz").unwrap();
|
let signature = git2::Signature::now("Radicle", "radicle@radicle.xyz").unwrap();
|
||||||
|
|
||||||
rng.fill(&mut buffer);
|
rng.fill(&mut buffer);
|
||||||
|
|
@ -113,17 +115,21 @@ pub fn populate(repo: &git2::Repository, scale: usize) {
|
||||||
builder.insert("random.txt", blob, 0o100_644).unwrap();
|
builder.insert("random.txt", blob, 0o100_644).unwrap();
|
||||||
let tree_oid = builder.write().unwrap();
|
let tree_oid = builder.write().unwrap();
|
||||||
let tree = repo.find_tree(tree_oid).unwrap();
|
let tree = repo.find_tree(tree_oid).unwrap();
|
||||||
|
let refstr = git::refs::workdir::branch(&name);
|
||||||
|
|
||||||
repo.commit(
|
repo.commit(
|
||||||
Some(&format!("refs/heads/{name}")),
|
Some(&refstr),
|
||||||
&signature,
|
&signature,
|
||||||
&signature,
|
&signature,
|
||||||
&format!("Initialize new branch feature/{random}"),
|
&format!("Initialize new branch {name}"),
|
||||||
&tree,
|
&tree,
|
||||||
&[&head],
|
&[&head],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
refs.push(git::Qualified::from_refstr(refstr).unwrap());
|
||||||
}
|
}
|
||||||
|
refs
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate random fixtures.
|
/// Generate random fixtures.
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ impl ReadRepository for MockRepository {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self) -> Result<(), VerifyError> {
|
fn validate_remote(&self, _remote: &Remote<Verified>) -> Result<(), VerifyError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue