node: Get the failing fetch tests passing
We fix the failing fetch tests by: 1. Not verifying our own refs, unless we're cloning, since we're otherwise not fetching our own refs. 2. Always force-fetching `sigrefs` from remotes into the staging copy. 3. Making sure that sigref updates are fast-forward before transfering the remote into the production copy.
This commit is contained in:
parent
1326774d4f
commit
0886ab0a3c
|
|
@ -612,7 +612,12 @@ fn test_cob_deletion() {
|
|||
let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
|
||||
alice_issues.remove(issue_id, &alice.signer).unwrap();
|
||||
|
||||
bob.handle.fetch(rid, alice.id).unwrap();
|
||||
log::debug!(target: "test", "Removing issue..");
|
||||
|
||||
radicle::assert_matches!(
|
||||
bob.handle.fetch(rid, alice.id).unwrap(),
|
||||
radicle::node::FetchResult::Success { .. }
|
||||
);
|
||||
let bob_repo = bob.storage.repository(rid).unwrap();
|
||||
let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
|
||||
assert!(bob_issues.get(issue_id).unwrap().is_none());
|
||||
|
|
|
|||
|
|
@ -364,6 +364,13 @@ impl PublicKey {
|
|||
pub fn to_namespace(&self) -> git_ref_format::RefString {
|
||||
git_ref_format::refname!("refs/namespaces").join(git_ref_format::Component::from(self))
|
||||
}
|
||||
|
||||
#[cfg(feature = "git-ref-format")]
|
||||
pub fn from_namespaced(refstr: &git_ref_format::Namespaced) -> Result<Self, PublicKeyError> {
|
||||
let name = refstr.namespace().into_inner();
|
||||
|
||||
Self::from_str(name.deref().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PublicKey {
|
||||
|
|
|
|||
|
|
@ -736,7 +736,6 @@ fn test_connection_crossing() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
/// Alice is going to try to fetch outdated refs of Bob, from Eve. This is a non-fastfoward fetch
|
||||
/// on the sigrefs branch.
|
||||
fn test_non_fastforward_sigrefs() {
|
||||
|
|
@ -791,7 +790,6 @@ fn test_non_fastforward_sigrefs() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_outdated_sigrefs() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
|
|
@ -850,6 +848,7 @@ fn test_outdated_sigrefs() {
|
|||
let eves_refs = repo.remote(&eve.id).unwrap().refs;
|
||||
|
||||
// Get the current state of eve's refs in alice's storage
|
||||
log::debug!(target: "test", "Alice fetches from Eve..");
|
||||
assert_matches!(
|
||||
alice.handle.fetch(rid, eve.id).unwrap(),
|
||||
FetchResult::Success { .. }
|
||||
|
|
@ -860,6 +859,8 @@ fn test_outdated_sigrefs() {
|
|||
assert_ne!(eves_refs_expected, old_refs);
|
||||
assert_eq!(eves_refs_expected, eves_refs);
|
||||
|
||||
log::debug!(target: "test", "Alice fetches from Bob..");
|
||||
|
||||
alice
|
||||
.handle
|
||||
.track_node(bob.id, Some("bob".to_string()))
|
||||
|
|
@ -869,7 +870,7 @@ fn test_outdated_sigrefs() {
|
|||
FetchResult::Success { .. }
|
||||
);
|
||||
|
||||
// Ensure that bob's refs have not changed
|
||||
// Ensure that Eve's refs have not changed after fetching the old refs from Bob.
|
||||
let repo = alice.storage.repository(rid).unwrap();
|
||||
let eve_remote = repo.remote(&eve.id).unwrap();
|
||||
let eves_refs = eve_remote.refs;
|
||||
|
|
|
|||
|
|
@ -229,7 +229,8 @@ impl Worker {
|
|||
namespaces: &Namespaces,
|
||||
mut channels: Channels,
|
||||
) -> Result<(Vec<RefUpdate>, HashSet<NodeId>), FetchError> {
|
||||
let staging = fetch::StagingPhaseInitial::new(&self.storage, rid, namespaces.clone())?;
|
||||
let staging =
|
||||
fetch::StagingPhaseInitial::new(&self.storage, rid, self.nid, namespaces.clone())?;
|
||||
let refs = if staging.repo.is_cloning() {
|
||||
match self._fetch(
|
||||
&staging.repo,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ pub struct StagingPhaseInitial<'a> {
|
|||
pub(super) repo: StagedRepository,
|
||||
/// The original [`Storage`] we are finalising changes into.
|
||||
production: &'a Storage,
|
||||
/// The local Node ID.
|
||||
nid: NodeId,
|
||||
/// The `Namespaces` passed by the fetching caller.
|
||||
pub(super) namespaces: Namespaces,
|
||||
_tmp: tempfile::TempDir,
|
||||
|
|
@ -101,6 +103,8 @@ pub struct StagingPhaseFinal<'a> {
|
|||
pub(super) repo: FinalStagedRepository,
|
||||
/// The original [`Storage`] we are finalising changes into.
|
||||
production: &'a Storage,
|
||||
/// The local Node ID.
|
||||
nid: NodeId,
|
||||
_tmp: tempfile::TempDir,
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +119,7 @@ enum VerifiedRemote {
|
|||
/// Unsigned refs.
|
||||
unsigned: Vec<git::RefString>,
|
||||
},
|
||||
UpToDate,
|
||||
}
|
||||
|
||||
impl<'a> StagingPhaseInitial<'a> {
|
||||
|
|
@ -123,6 +128,7 @@ impl<'a> StagingPhaseInitial<'a> {
|
|||
pub fn new(
|
||||
production: &'a Storage,
|
||||
rid: Id,
|
||||
nid: NodeId,
|
||||
namespaces: Namespaces,
|
||||
) -> Result<Self, error::Init> {
|
||||
let tmp = tempfile::TempDir::new()?;
|
||||
|
|
@ -131,6 +137,7 @@ impl<'a> StagingPhaseInitial<'a> {
|
|||
let repo = Self::repository(&staging, production, rid)?;
|
||||
Ok(Self {
|
||||
repo,
|
||||
nid,
|
||||
production,
|
||||
namespaces,
|
||||
_tmp: tmp,
|
||||
|
|
@ -194,6 +201,7 @@ impl<'a> StagingPhaseInitial<'a> {
|
|||
|
||||
Ok(StagingPhaseFinal {
|
||||
repo,
|
||||
nid: self.nid,
|
||||
production: self.production,
|
||||
_tmp: self._tmp,
|
||||
})
|
||||
|
|
@ -273,27 +281,45 @@ impl<'a> StagingPhaseFinal<'a> {
|
|||
/// All references that were updated are returned as a
|
||||
/// [`RefUpdate`].
|
||||
pub fn transfer(self) -> Result<(Vec<RefUpdate>, HashSet<NodeId>), error::Transfer> {
|
||||
let verifications = self.verify()?;
|
||||
let production = match &self.repo {
|
||||
FinalStagedRepository::Cloning { repo, .. } => self.production.create(repo.id)?,
|
||||
FinalStagedRepository::Fetching { repo, .. } => self.production.repository(repo.id)?,
|
||||
// Nb. we have to verify in a different order when fetching vs. cloning, due to needing
|
||||
// access to the existing repository in the fetching case.
|
||||
let (production, verifications) = match &self.repo {
|
||||
FinalStagedRepository::Cloning { repo, .. } => {
|
||||
let verifications = self.verify::<Repository>(None)?;
|
||||
let prod = self.production.create(repo.id)?;
|
||||
|
||||
(prod, verifications)
|
||||
}
|
||||
FinalStagedRepository::Fetching { repo, .. } => {
|
||||
let prod = self.production.repository(repo.id)?;
|
||||
let verifications = self.verify(Some(&prod))?;
|
||||
|
||||
(prod, verifications)
|
||||
}
|
||||
};
|
||||
let url = url::File::new(self.repo.path().to_path_buf()).to_string();
|
||||
let mut remote = production.backend.remote_anonymous(&url)?;
|
||||
let mut updates = Vec::new();
|
||||
let mut delete = HashSet::new();
|
||||
let mut skipped = HashSet::new();
|
||||
|
||||
let callbacks = ref_updates(&mut updates);
|
||||
let remotes = {
|
||||
let mut remotes = {
|
||||
let specs = verifications
|
||||
.into_iter()
|
||||
.flat_map(|(remote, verified)| match verified {
|
||||
VerifiedRemote::UpToDate => {
|
||||
log::debug!(target: "worker", "{remote} is up-to-date");
|
||||
skipped.insert(remote);
|
||||
|
||||
vec![]
|
||||
}
|
||||
VerifiedRemote::Failed { reason } => {
|
||||
// TODO: We should include the skipped remotes in the fetch result,
|
||||
// with the reason why they're skipped.
|
||||
log::warn!(
|
||||
target: "worker",
|
||||
"{remote} failed to verify, will not fetch any further refs: {reason}",
|
||||
"{remote} failed to verify, ignoring ref updates: {reason}",
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
|
|
@ -356,11 +382,12 @@ impl<'a> StagingPhaseFinal<'a> {
|
|||
|
||||
let (fetching, specs): (HashSet<_>, Vec<_>) = specs.into_iter().unzip();
|
||||
|
||||
if !self
|
||||
.repo
|
||||
.delegates()?
|
||||
.iter()
|
||||
.all(|d| fetching.contains(d.as_key()))
|
||||
if self.repo.is_cloning()
|
||||
&& !self
|
||||
.repo
|
||||
.delegates()?
|
||||
.iter()
|
||||
.all(|d| fetching.contains(d.as_key()))
|
||||
{
|
||||
return Err(error::Transfer::NoDelegates);
|
||||
}
|
||||
|
|
@ -408,6 +435,10 @@ impl<'a> StagingPhaseFinal<'a> {
|
|||
production.id,
|
||||
);
|
||||
|
||||
// Extend the list of remotes we attempted to fetch from with the skipped remotes.
|
||||
// This confirms to the user that the remote was indeed tried.
|
||||
remotes.extend(skipped);
|
||||
|
||||
Ok((updates, remotes))
|
||||
}
|
||||
|
||||
|
|
@ -418,17 +449,68 @@ impl<'a> StagingPhaseFinal<'a> {
|
|||
.iter()
|
||||
.filter_map(|remote| self.repo.remote(remote).ok()),
|
||||
)),
|
||||
FinalStagedRepository::Fetching { repo, .. } => Ok(Box::new(
|
||||
repo.remotes()?.filter_map(|r| r.ok().map(|(_, r)| r)),
|
||||
)),
|
||||
FinalStagedRepository::Fetching { repo, refs } => {
|
||||
// Only verify remotes we're fetching refs from.
|
||||
let remotes = refs
|
||||
.iter()
|
||||
.filter_map(|r| NodeId::from_namespaced(r).ok())
|
||||
.collect::<HashSet<_>>();
|
||||
let remotes = remotes.into_iter().filter_map(|r| repo.remote(&r).ok());
|
||||
|
||||
Ok(Box::new(remotes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(&self) -> Result<BTreeMap<RemoteId, VerifiedRemote>, git::raw::Error> {
|
||||
Ok(self
|
||||
fn verify<R: ReadRepository>(
|
||||
&self,
|
||||
local: Option<&R>,
|
||||
) -> Result<BTreeMap<RemoteId, VerifiedRemote>, git::raw::Error> {
|
||||
let result = self
|
||||
.remotes()?
|
||||
.filter(|remote| remote.id != self.nid || self.repo.is_cloning())
|
||||
.map(|remote| {
|
||||
let remote_id = remote.id;
|
||||
|
||||
log::debug!(target: "worker", "Verifying remote {remote_id}..");
|
||||
|
||||
// If we have a local copy, ie. we're not cloning, we check that the signed refs
|
||||
// are being fast-forwarded.
|
||||
if let Some(local) = local {
|
||||
if let (Ok(local), Ok(staging)) = (
|
||||
local.reference_oid(&remote_id, &git::refs::storage::SIGREFS_BRANCH),
|
||||
self.repo.reference_oid(&remote_id, &git::refs::storage::SIGREFS_BRANCH),
|
||||
) {
|
||||
if local != staging {
|
||||
match self
|
||||
.repo
|
||||
.backend
|
||||
.graph_descendant_of(staging.into(), local.into())
|
||||
{
|
||||
Ok(true) => {
|
||||
log::debug!(target: "worker", "Signed refs for {remote_id} fast-foward: {local} -> {staging}");
|
||||
}
|
||||
Ok(false) => {
|
||||
return (
|
||||
remote_id,
|
||||
VerifiedRemote::Failed {
|
||||
reason: "signed refs have diverged".to_owned()
|
||||
}
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
return (
|
||||
remote_id,
|
||||
VerifiedRemote::Failed { reason: e.to_string() },
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (remote_id, VerifiedRemote::UpToDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let verification = match self.repo.identity_doc_of(&remote_id) {
|
||||
Ok(doc) => match self.repo.validate_remote(&remote) {
|
||||
Ok(unsigned) => VerifiedRemote::Success {
|
||||
|
|
@ -446,7 +528,9 @@ impl<'a> StagingPhaseFinal<'a> {
|
|||
};
|
||||
(remote_id, verification)
|
||||
})
|
||||
.collect())
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,18 +42,15 @@ impl AsRefspecs for SpecialRefs {
|
|||
Namespaces::All => {
|
||||
let id = NAMESPACES_GLOB.join(&*IDENTITY_BRANCH);
|
||||
let sigrefs = NAMESPACES_GLOB.join(&*SIGREFS_BRANCH);
|
||||
vec![
|
||||
Refspec {
|
||||
src: id.clone(),
|
||||
dst: id,
|
||||
force: false,
|
||||
},
|
||||
Refspec {
|
||||
src: sigrefs.clone(),
|
||||
dst: sigrefs,
|
||||
force: false,
|
||||
},
|
||||
]
|
||||
|
||||
[id, sigrefs]
|
||||
.into_iter()
|
||||
.map(|spec| Refspec {
|
||||
src: spec.clone(),
|
||||
dst: spec,
|
||||
force: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Namespaces::Trusted(pks) => pks.iter().flat_map(rad_refs).collect(),
|
||||
}
|
||||
|
|
@ -70,13 +67,13 @@ fn rad_refs(pk: &PublicKey) -> Vec<Refspec<git::PatternString, git::PatternStrin
|
|||
let id = Refspec {
|
||||
src: id.clone(),
|
||||
dst: id,
|
||||
force: false,
|
||||
force: true,
|
||||
};
|
||||
let sigrefs = git::PatternString::from(ns.join(&*SIGREFS_BRANCH));
|
||||
let sigrefs = Refspec {
|
||||
src: sigrefs.clone(),
|
||||
dst: sigrefs,
|
||||
force: false,
|
||||
force: true,
|
||||
};
|
||||
vec![id, sigrefs]
|
||||
}
|
||||
|
|
@ -104,7 +101,7 @@ impl AsRefspecs for Namespaces {
|
|||
Namespaces::All => vec![Refspec {
|
||||
src: (*storage::git::NAMESPACES_GLOB).clone(),
|
||||
dst: (*storage::git::NAMESPACES_GLOB).clone(),
|
||||
force: false,
|
||||
force: true,
|
||||
}],
|
||||
Namespaces::Trusted(pks) => pks
|
||||
.iter()
|
||||
|
|
@ -113,7 +110,7 @@ impl AsRefspecs for Namespaces {
|
|||
Refspec {
|
||||
src: ns.clone(),
|
||||
dst: ns,
|
||||
force: false,
|
||||
force: true,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
|
|
@ -163,19 +160,11 @@ impl AsRefspecs for Remote {
|
|||
|
||||
impl<'a> AsRefspecs for BTreeSet<git::Namespaced<'a>> {
|
||||
fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
|
||||
let reserved = [(*IDENTITY_BRANCH).clone(), (*SIGREFS_BRANCH).clone()]
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>();
|
||||
self.iter()
|
||||
.map(|r| {
|
||||
// Only force ordinary refs.
|
||||
let suffix = r.strip_namespace();
|
||||
let force = !reserved.contains(&suffix);
|
||||
Refspec {
|
||||
src: r.clone().to_ref_string().into(),
|
||||
dst: r.clone().to_ref_string().into(),
|
||||
force,
|
||||
}
|
||||
.map(|r| Refspec {
|
||||
src: r.clone().to_ref_string().into(),
|
||||
dst: r.clone().to_ref_string().into(),
|
||||
force: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue