radicle: Teach `rad sync` and `rad clone` to accept feature levels

Teach the `rad sync` and `rad clone` commands to accept the
`--signed-refs-feature-level` option. This option configures that
fetch to use the minimum feature level provided when fetching from
other nodes. This overrides the value of
`node.fetch.signedReferences.featureLevel.minimum`, and should only be
used in scenarios where it is necessary to downgrade the security of a
fetch for backwards compatibility.


To achieve this, the `Handle::fetch` method introduces an optional
parameter for threading through the `FeatureLevel`.
This commit is contained in:
Fintan Halpenny 2026-03-24 10:38:47 +00:00
parent ef4ddf06c3
commit 33db6637b4
19 changed files with 403 additions and 82 deletions

View File

@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## New Features ## New Features
- Teach the `rad sync` and `rad clone` commands to accept the
`--signed-refs-feature-level` option. This option configures that fetch to use
the minimum feature level provided when fetching from other nodes. This
overrides the value of `node.fetch.signedReferences.featureLevel.minimum`, and
should only be used in scenarios where it is necessary to tolerate a lower
minimum feature level for a fetch to achieve backwards compatibility.
- Fix the signed references reading process by correctly choosing the first, - Fix the signed references reading process by correctly choosing the first,
non-replayed commit. This only occurs if duplicate signatures are found and non-replayed commit. This only occurs if duplicate signatures are found and
the process needs to find the first legitimate commit of the namespace. the process needs to find the first legitimate commit of the namespace.

View File

@ -2,12 +2,17 @@ use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use crate::node::SyncSettings;
use radicle::identity::doc::RepoId; use radicle::identity::doc::RepoId;
use radicle::identity::IdError; use radicle::identity::IdError;
use radicle::node::policy::Scope; use radicle::node::policy::Scope;
use radicle::prelude::*; use radicle::prelude::*;
use radicle::storage::refs;
use crate::common_args::{
SignedReferencesFeatureLevel, SignedReferencesFeatureLevelParser,
ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM,
};
use crate::node::SyncSettings;
use crate::terminal; use crate::terminal;
const ABOUT: &str = "Clone a Radicle repository"; const ABOUT: &str = "Clone a Radicle repository";
@ -35,6 +40,13 @@ pub(super) struct SyncArgs {
/// Valid arguments are for example "10s", "5min" or "2h 37min" /// Valid arguments are for example "10s", "5min" or "2h 37min"
#[arg(long, value_parser = humantime::parse_duration, default_value = "9s")] #[arg(long, value_parser = humantime::parse_duration, default_value = "9s")]
timeout: std::time::Duration, timeout: std::time::Duration,
#[arg(
long,
value_parser = SignedReferencesFeatureLevelParser,
help = ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM
)]
signed_refs_feature_level: Option<SignedReferencesFeatureLevel>,
} }
impl From<SyncArgs> for SyncSettings { impl From<SyncArgs> for SyncSettings {
@ -42,6 +54,9 @@ impl From<SyncArgs> for SyncSettings {
SyncSettings { SyncSettings {
timeout: args.timeout, timeout: args.timeout,
seeds: args.seeds.into_iter().collect(), seeds: args.seeds.into_iter().collect(),
signed_references_minimum_feature_level: args
.signed_refs_feature_level
.map(refs::FeatureLevel::from),
..SyncSettings::default() ..SyncSettings::default()
} }
} }

View File

@ -324,7 +324,12 @@ pub fn fetch(
} }
if let Some((nid, addr)) = fetcher.next_fetch() { if let Some((nid, addr)) = fetcher.next_fetch() {
spinner.emit_fetching(&nid, &addr, &progress); spinner.emit_fetching(&nid, &addr, &progress);
let result = node.fetch(rid, nid, settings.timeout)?; let result = node.fetch(
rid,
nid,
settings.timeout,
settings.signed_references_minimum_feature_level,
)?;
match fetcher.fetch_complete(nid, result) { match fetcher.fetch_complete(nid, result) {
std::ops::ControlFlow::Continue(update) => { std::ops::ControlFlow::Continue(update) => {
spinner.emit_progress(&update); spinner.emit_progress(&update);

View File

@ -6,8 +6,13 @@ use clap::{Parser, Subcommand, ValueEnum};
use radicle::{ use radicle::{
node::{sync, NodeId}, node::{sync, NodeId},
prelude::RepoId, prelude::RepoId,
storage::refs,
}; };
use crate::common_args::{
SignedReferencesFeatureLevel, SignedReferencesFeatureLevelParser,
ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM,
};
use crate::node::SyncSettings; use crate::node::SyncSettings;
const ABOUT: &str = "Sync repositories to the network"; const ABOUT: &str = "Sync repositories to the network";
@ -136,6 +141,14 @@ pub(super) struct SyncArgs {
/// <RID> is ignored with `--inventory` /// <RID> is ignored with `--inventory`
#[arg(long, short)] #[arg(long, short)]
inventory: bool, inventory: bool,
#[arg(
long,
requires = "fetch",
value_parser = SignedReferencesFeatureLevelParser,
help = ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM
)]
signed_refs_feature_level: Option<SignedReferencesFeatureLevel>,
} }
impl SyncArgs { impl SyncArgs {
@ -220,9 +233,13 @@ impl From<SyncArgs> for SyncMode {
} else { } else {
assert!(!args.inventory); assert!(!args.inventory);
let direction = args.direction(); let direction = args.direction();
let timeout = args.timeout();
let replicas = args.replication();
let feature_level = args.signed_refs_feature_level.map(refs::FeatureLevel::from);
let mut settings = SyncSettings::default() let mut settings = SyncSettings::default()
.timeout(args.timeout()) .timeout(timeout)
.replicas(args.replication()); .replicas(replicas)
.minimum_feature_level(feature_level);
if !args.seeds.is_empty() { if !args.seeds.is_empty() {
settings.seeds = args.seeds.into_iter().collect(); settings.seeds = args.seeds.into_iter().collect();
} }

View File

@ -0,0 +1,79 @@
use std::str::FromStr;
use radicle::storage::refs;
pub const ABOUT_FETCH_SIGNED_REFERENCES_FEATURE_LEVEL_MINIMUM: &str = r#"
Perform the fetch using the provided Signed References minimum feature
level.
The options for this value provide the following behavior:
`parent`: Only the namespaces that use the parent feature level will be
accepted. This prevents graft attacks and replay attacks from
occurring. This provides the most security.
`root`: Only the namespaces that use the root feature level will be
accepted. This prevents graft attacks from occurring. This option
should be only used if you trust the node you are fetching from, and
want to bypass security for backwards compatibility.
`none`: All namespaces will be fetched regardless of feature level
detected, and provides no security against graft attacks or replay
attacks. This option should be only used if you trust the node you are
fetching from, and want to bypass security for backwards compatibility.
"#;
#[derive(Clone, Copy, Debug)]
pub struct SignedReferencesFeatureLevel {
inner: refs::FeatureLevel,
}
impl From<SignedReferencesFeatureLevel> for refs::FeatureLevel {
fn from(SignedReferencesFeatureLevel { inner }: SignedReferencesFeatureLevel) -> Self {
inner
}
}
impl FromStr for SignedReferencesFeatureLevel {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner = match s {
"none" => refs::FeatureLevel::None,
"root" => refs::FeatureLevel::Root,
"parent" => refs::FeatureLevel::Parent,
_ => return Err("invalid feature level"),
};
Ok(Self { inner })
}
}
#[derive(Clone, Debug)]
pub struct SignedReferencesFeatureLevelParser;
impl clap::builder::TypedValueParser for SignedReferencesFeatureLevelParser {
type Value = SignedReferencesFeatureLevel;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
<SignedReferencesFeatureLevel as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
use clap::builder::PossibleValue;
Some(Box::new(
[
PossibleValue::new("parent"),
PossibleValue::new("root"),
PossibleValue::new("none"),
]
.into_iter(),
))
}
}

View File

@ -8,6 +8,7 @@ pub mod pager;
pub mod project; pub mod project;
pub mod terminal; pub mod terminal;
mod common_args;
mod warning; mod warning;
extern crate radicle_localtime as localtime; extern crate radicle_localtime as localtime;

View File

@ -4,7 +4,7 @@ use std::io::Write;
use radicle::node::sync; use radicle::node::sync;
use radicle::node::{Handle as _, NodeId}; use radicle::node::{Handle as _, NodeId};
use radicle::storage::{ReadRepository, RepositoryError}; use radicle::storage::{refs, ReadRepository, RepositoryError};
use radicle::{Node, Profile}; use radicle::{Node, Profile};
use crate::terminal as term; use crate::terminal as term;
@ -21,6 +21,8 @@ pub struct SyncSettings {
pub seeds: BTreeSet<NodeId>, pub seeds: BTreeSet<NodeId>,
/// How long to wait for syncing to complete. /// How long to wait for syncing to complete.
pub timeout: time::Duration, pub timeout: time::Duration,
/// The minimum feature level to accept when fetching signed references.
pub signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
} }
impl SyncSettings { impl SyncSettings {
@ -31,6 +33,13 @@ impl SyncSettings {
self self
} }
/// Set minimum feature level for fetching signed references.
#[must_use]
pub fn minimum_feature_level(mut self, feature_level: Option<refs::FeatureLevel>) -> Self {
self.signed_references_minimum_feature_level = feature_level;
self
}
/// Set replicas. /// Set replicas.
#[must_use] #[must_use]
pub fn replicas(mut self, replicas: sync::ReplicationFactor) -> Self { pub fn replicas(mut self, replicas: sync::ReplicationFactor) -> Self {
@ -69,6 +78,7 @@ impl Default for SyncSettings {
replicas: sync::ReplicationFactor::default(), replicas: sync::ReplicationFactor::default(),
seeds: BTreeSet::new(), seeds: BTreeSet::new(),
timeout: DEFAULT_SYNC_TIMEOUT, timeout: DEFAULT_SYNC_TIMEOUT,
signed_references_minimum_feature_level: None,
} }
} }
} }

View File

@ -220,7 +220,7 @@ fn test_cob_deletion() {
radicle::assert_matches!( radicle::assert_matches!(
bob.handle bob.handle
.fetch(rid, alice.id, radicle::node::DEFAULT_TIMEOUT) .fetch(rid, alice.id, radicle::node::DEFAULT_TIMEOUT, None)
.unwrap(), .unwrap(),
radicle::node::FetchResult::Success { .. } radicle::node::FetchResult::Success { .. }
); );

View File

@ -87,7 +87,9 @@ fn rad_id_threshold() {
alice.connect(&seed).connect(&bob); alice.connect(&seed).connect(&bob);
bob.connect(&seed); bob.connect(&seed);
alice.routes_to(&[(acme, seed.id)]); alice.routes_to(&[(acme, seed.id)]);
seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); seed.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
formula(&environment.tempdir(), "examples/rad-id-threshold.md") formula(&environment.tempdir(), "examples/rad-id-threshold.md")
.unwrap() .unwrap()
@ -323,10 +325,12 @@ fn rad_id_collaboration() {
.connect(&distrustful) .connect(&distrustful)
.converge([&seed, &distrustful]); .converge([&seed, &distrustful]);
seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); seed.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
distrustful distrustful
.handle .handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT) .fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap(); .unwrap();
formula(&environment.tempdir(), "examples/rad-id-collaboration.md") formula(&environment.tempdir(), "examples/rad-id-collaboration.md")

View File

@ -87,7 +87,9 @@ fn rad_clean() {
bob.connect(&alice).converge([&alice]); bob.connect(&alice).converge([&alice]);
eve.connect(&alice).converge([&alice]); eve.connect(&alice).converge([&alice]);
eve.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); eve.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
bob.fork(acme, bob.home.path()).unwrap(); bob.fork(acme, bob.home.path()).unwrap();
bob.announce(acme, 1, bob.home.path()).unwrap(); bob.announce(acme, 1, bob.home.path()).unwrap();

View File

@ -5,6 +5,7 @@ use std::io::LineWriter;
use std::path::PathBuf; use std::path::PathBuf;
use std::{io, net, time}; use std::{io, net, time};
use radicle::storage::refs;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::net::{UnixListener, UnixStream}; use std::os::unix::net::{UnixListener, UnixStream};
#[cfg(windows)] #[cfg(windows)]
@ -112,8 +113,20 @@ where
CommandResult::ok().to_writer(writer).ok(); CommandResult::ok().to_writer(writer).ok();
} }
}, },
Command::Fetch { rid, nid, timeout } => { Command::Fetch {
fetch(rid, nid, timeout, writer, &mut handle)?; rid,
nid,
timeout,
signed_references_minimum_feature_level,
} => {
fetch(
rid,
nid,
timeout,
signed_references_minimum_feature_level,
writer,
&mut handle,
)?;
} }
Command::Config => { Command::Config => {
let config = handle.config()?; let config = handle.config()?;
@ -248,10 +261,11 @@ fn fetch<W: Write, H: Handle<Error = runtime::HandleError>>(
id: RepoId, id: RepoId,
node: NodeId, node: NodeId,
timeout: time::Duration, timeout: time::Duration,
signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
mut writer: W, mut writer: W,
handle: &mut H, handle: &mut H,
) -> Result<(), CommandError> { ) -> Result<(), CommandError> {
match handle.fetch(id, node, timeout) { match handle.fetch(id, node, timeout, signed_references_minimum_feature_level) {
Ok(result) => { Ok(result) => {
json::to_writer(&mut writer, &result)?; json::to_writer(&mut writer, &result)?;
} }

View File

@ -15,6 +15,7 @@ use radicle::node::events::{Event, Events};
use radicle::node::policy; use radicle::node::policy;
use radicle::node::{Config, NodeId}; use radicle::node::{Config, NodeId};
use radicle::node::{ConnectOptions, ConnectResult, Seeds}; use radicle::node::{ConnectOptions, ConnectResult, Seeds};
use radicle::storage::refs;
use serde_json::json; use serde_json::json;
use thiserror::Error; use thiserror::Error;
@ -229,9 +230,16 @@ impl radicle::node::Handle for Handle {
id: RepoId, id: RepoId,
from: NodeId, from: NodeId,
timeout: time::Duration, timeout: time::Duration,
signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
) -> Result<FetchResult, Error> { ) -> Result<FetchResult, Error> {
let (responder, receiver) = service::command::Responder::oneshot(); let (responder, receiver) = service::command::Responder::oneshot();
self.command(service::Command::Fetch(id, from, timeout, responder))?; self.command(service::Command::Fetch(
id,
from,
timeout,
signed_references_minimum_feature_level,
responder,
))?;
Ok(receiver.recv()??) Ok(receiver.recv()??)
} }

View File

@ -5,7 +5,7 @@ use std::time;
use radicle::crypto::PublicKey; use radicle::crypto::PublicKey;
use radicle::git::Oid; use radicle::git::Oid;
use radicle::storage::refs::RefsAt; use radicle::storage::refs::{self, RefsAt};
use crate::identity::RepoId; use crate::identity::RepoId;
use crate::node::{Alias, Config, ConnectOptions, ConnectResult, Event, FetchResult, Seeds}; use crate::node::{Alias, Config, ConnectOptions, ConnectResult, Event, FetchResult, Seeds};
@ -69,6 +69,7 @@ impl radicle::node::Handle for Handle {
_id: RepoId, _id: RepoId,
_from: NodeId, _from: NodeId,
_timeout: time::Duration, _timeout: time::Duration,
_signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
) -> Result<FetchResult, Self::Error> { ) -> Result<FetchResult, Self::Error> {
Ok(FetchResult::Success { Ok(FetchResult::Success {
updated: vec![], updated: vec![],

View File

@ -1496,15 +1496,15 @@ fn test_queued_fetch_max_capacity() {
alice.connect_to(&bob); alice.connect_to(&bob);
// Send the first fetch. // Send the first fetch.
let (cmd, _recv1) = Command::fetch(rid1, bob.id, DEFAULT_TIMEOUT); let (cmd, _recv1) = Command::fetch(rid1, bob.id, DEFAULT_TIMEOUT, None);
alice.command(cmd); alice.command(cmd);
// Send the 2nd fetch that will be queued. // Send the 2nd fetch that will be queued.
let (cmd, _recv2) = Command::fetch(rid2, bob.id, DEFAULT_TIMEOUT); let (cmd, _recv2) = Command::fetch(rid2, bob.id, DEFAULT_TIMEOUT, None);
alice.command(cmd); alice.command(cmd);
// Send the 3rd fetch that will be queued. // Send the 3rd fetch that will be queued.
let (cmd, _recv3) = Command::fetch(rid3, bob.id, DEFAULT_TIMEOUT); let (cmd, _recv3) = Command::fetch(rid3, bob.id, DEFAULT_TIMEOUT, None);
alice.command(cmd); alice.command(cmd);
// The first fetch is initiated. // The first fetch is initiated.
@ -1618,15 +1618,15 @@ fn test_queued_fetch_from_command_same_rid() {
alice.connect_to(&carol); alice.connect_to(&carol);
// Send the first fetch. // Send the first fetch.
let (cmd, _recv1) = Command::fetch(rid1, bob.id, DEFAULT_TIMEOUT); let (cmd, _recv1) = Command::fetch(rid1, bob.id, DEFAULT_TIMEOUT, None);
alice.command(cmd); alice.command(cmd);
// Send the 2nd fetch that will be queued. // Send the 2nd fetch that will be queued.
let (cmd, _recv2) = Command::fetch(rid1, eve.id, DEFAULT_TIMEOUT); let (cmd, _recv2) = Command::fetch(rid1, eve.id, DEFAULT_TIMEOUT, None);
alice.command(cmd); alice.command(cmd);
// Send the 3rd fetch that will be queued. // Send the 3rd fetch that will be queued.
let (cmd, _recv3) = Command::fetch(rid1, carol.id, DEFAULT_TIMEOUT); let (cmd, _recv3) = Command::fetch(rid1, carol.id, DEFAULT_TIMEOUT, None);
alice.command(cmd); alice.command(cmd);
// Peers Alice will fetch from. // Peers Alice will fetch from.

View File

@ -189,7 +189,10 @@ fn test_replication() {
let seeds = alice.handle.seeds_for(acme, None).unwrap(); let seeds = alice.handle.seeds_for(acme, None).unwrap();
assert!(seeds.is_connected(&bob.id)); assert!(seeds.is_connected(&bob.id));
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
let updated = match result { let updated = match result {
@ -266,7 +269,10 @@ fn test_replication_ref_in_sigrefs() {
converge([&alice, &bob]); converge([&alice, &bob]);
alice.handle.seed(acme, Scope::All).unwrap(); alice.handle.seed(acme, Scope::All).unwrap();
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert_matches!(result, FetchResult::Success { .. }); assert_matches!(result, FetchResult::Success { .. });
@ -325,7 +331,10 @@ fn test_replication_invalid() {
alice.handle.follow(*carol.public_key(), None).unwrap(); alice.handle.follow(*carol.public_key(), None).unwrap();
alice.handle.seed(acme, Scope::Followed).unwrap(); alice.handle.seed(acme, Scope::Followed).unwrap();
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
// Fetch is successful despite not fetching Carol's refs, since she isn't a delegate. // Fetch is successful despite not fetching Carol's refs, since she isn't a delegate.
assert!(result.is_success()); assert!(result.is_success());
@ -355,7 +364,10 @@ fn test_migrated_clone() {
let updated = bob.handle.seed(acme, Scope::All).unwrap(); let updated = bob.handle.seed(acme, Scope::All).unwrap();
assert!(updated); assert!(updated);
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
log::debug!(target: "test", "Fetch complete with {}", alice.id); log::debug!(target: "test", "Fetch complete with {}", alice.id);
@ -366,7 +378,10 @@ fn test_migrated_clone() {
std::fs::remove_dir_all(path).unwrap(); std::fs::remove_dir_all(path).unwrap();
} }
assert!(!alice.storage.contains(&acme).unwrap()); assert!(!alice.storage.contains(&acme).unwrap());
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
let alice_repo = alice.storage.repository(acme).unwrap(); let alice_repo = alice.storage.repository(acme).unwrap();
@ -405,13 +420,19 @@ fn test_dont_fetch_owned_refs() {
assert!(bob.handle.seed(acme, Scope::Followed).unwrap()); assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
log::debug!(target: "test", "Fetch complete with {}", bob.id); log::debug!(target: "test", "Fetch complete with {}", bob.id);
alice.issue(acme, Title::new("Don't fetch self").unwrap(), "Use ^"); alice.issue(acme, Title::new("Don't fetch self").unwrap(), "Use ^");
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()) assert!(result.is_success())
} }
@ -451,7 +472,10 @@ fn test_fetch_followed_remotes() {
assert!(bob.handle.follow(*nid, None).unwrap()); assert!(bob.handle.follow(*nid, None).unwrap());
} }
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
log::debug!(target: "test", "Fetch complete with {}", bob.id); log::debug!(target: "test", "Fetch complete with {}", bob.id);
@ -484,7 +508,10 @@ fn test_missing_remote() {
assert!(bob.handle.seed(acme, Scope::Followed).unwrap()); assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
assert!(bob.handle.follow(*carol.public_key(), None).unwrap()); assert!(bob.handle.follow(*carol.public_key(), None).unwrap());
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
log::debug!(target: "test", "Fetch complete with {}", bob.id); log::debug!(target: "test", "Fetch complete with {}", bob.id);
rad::fork_remote(acme, &alice.id, &carol, &bob.storage).unwrap(); rad::fork_remote(acme, &alice.id, &carol, &bob.storage).unwrap();
@ -494,7 +521,10 @@ fn test_missing_remote() {
Title::new("Missing Remote").unwrap(), Title::new("Missing Remote").unwrap(),
"Fixing the missing remote issue", "Fixing the missing remote issue",
); );
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
log::debug!(target: "test", "Fetch complete with {}", bob.id); log::debug!(target: "test", "Fetch complete with {}", bob.id);
} }
@ -514,7 +544,10 @@ fn test_fetch_preserve_owned_refs() {
assert!(bob.handle.seed(acme, Scope::Followed).unwrap()); assert!(bob.handle.seed(acme, Scope::Followed).unwrap());
assert!(bob.handle.follow(alice.id, None).unwrap()); assert!(bob.handle.follow(alice.id, None).unwrap());
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
log::debug!(target: "test", "Fetch complete with {}", bob.id); log::debug!(target: "test", "Fetch complete with {}", bob.id);
@ -529,7 +562,10 @@ fn test_fetch_preserve_owned_refs() {
.unwrap(); .unwrap();
// Fetch shouldn't prune any of our own refs. // Fetch shouldn't prune any of our own refs.
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
let (updated, _) = result.success().unwrap(); let (updated, _) = result.success().unwrap();
assert_eq!(updated, vec![]); assert_eq!(updated, vec![]);
@ -562,7 +598,10 @@ fn test_clone() {
let seeds = alice.handle.seeds_for(acme, None).unwrap(); let seeds = alice.handle.seeds_for(acme, None).unwrap();
assert!(seeds.is_connected(&bob.id)); assert!(seeds.is_connected(&bob.id));
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
rad::fork(acme, &alice.signer, &alice.storage).unwrap(); rad::fork(acme, &alice.signer, &alice.storage).unwrap();
@ -616,11 +655,17 @@ fn test_fetch_up_to_date() {
transport::local::register(alice.storage.clone()); transport::local::register(alice.storage.clone());
let _ = alice.handle.seed(acme, Scope::All).unwrap(); let _ = alice.handle.seed(acme, Scope::All).unwrap();
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
// Fetch again! This time, everything's up to date. // Fetch again! This time, everything's up to date.
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert_matches!( assert_matches!(
result.success(), result.success(),
Some((updates, _fetched)) if updates.iter().all(|update| matches!(update, RefUpdate::Skipped { .. })) Some((updates, _fetched)) if updates.iter().all(|update| matches!(update, RefUpdate::Skipped { .. }))
@ -643,14 +688,20 @@ fn test_fetch_unseeded() {
transport::local::register(alice.storage.clone()); transport::local::register(alice.storage.clone());
let _ = alice.handle.seed(acme, Scope::All).unwrap(); let _ = alice.handle.seed(acme, Scope::All).unwrap();
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
// Bob stops seeding the repository // Bob stops seeding the repository
assert!(bob.handle.unseed(acme).unwrap()); assert!(bob.handle.unseed(acme).unwrap());
// Alice attempts to fetch but is unauthorized // Alice attempts to fetch but is unauthorized
let result = alice.handle.fetch(acme, bob.id, DEFAULT_TIMEOUT).unwrap(); let result = alice
.handle
.fetch(acme, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert_matches!(result, FetchResult::Failed { .. }); assert_matches!(result, FetchResult::Failed { .. });
} }
@ -896,9 +947,14 @@ fn test_non_fastforward_sigrefs() {
converge([&alice, &bob, &eve]); converge([&alice, &bob, &eve]);
// Eve fetches the initial project from Bob. // Eve fetches the initial project from Bob.
eve.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(); eve.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
// Alice fetches it too. // Alice fetches it too.
let old_bob = alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(); let old_bob = alice
.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
let bob_sigrefs = bob let bob_sigrefs = bob
.storage .storage
.repository(rid) .repository(rid)
@ -940,7 +996,10 @@ fn test_non_fastforward_sigrefs() {
"Updated sigrefs are harshing my vibes", "Updated sigrefs are harshing my vibes",
); );
// Alice fetches from Bob. // Alice fetches from Bob.
let new_bob = alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(); let new_bob = alice
.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
let bob_sigrefs = bob let bob_sigrefs = bob
.storage .storage
.repository(rid) .repository(rid)
@ -972,7 +1031,7 @@ fn test_non_fastforward_sigrefs() {
} }
assert_matches!( assert_matches!(
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(), alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT, None).unwrap(),
FetchResult::Success { updated, .. } FetchResult::Success { updated, .. }
if updated.iter().all(|u| u.is_skipped()) if updated.iter().all(|u| u.is_skipped())
); );
@ -999,11 +1058,15 @@ fn test_outdated_sigrefs() {
eve.connect(&alice); eve.connect(&alice);
converge([&alice, &bob, &eve]); converge([&alice, &bob, &eve]);
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(bob.storage.contains(&rid).unwrap()); assert!(bob.storage.contains(&rid).unwrap());
rad::fork(rid, &bob.signer, &bob.storage).unwrap(); rad::fork(rid, &bob.signer, &bob.storage).unwrap();
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); eve.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(eve.storage.contains(&rid).unwrap()); assert!(eve.storage.contains(&rid).unwrap());
rad::fork(rid, &eve.signer, &eve.storage).unwrap(); rad::fork(rid, &eve.signer, &eve.storage).unwrap();
@ -1011,13 +1074,18 @@ fn test_outdated_sigrefs() {
.handle .handle
.follow(eve.id, Some(Alias::new("eve"))) .follow(eve.id, Some(Alias::new("eve")))
.unwrap(); .unwrap();
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(); alice
.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap();
let repo = alice.storage.repository(rid).unwrap(); let repo = alice.storage.repository(rid).unwrap();
assert!(repo.remote(&eve.id).is_ok()); assert!(repo.remote(&eve.id).is_ok());
log::debug!(target: "test", "Bob fetches from Eve.."); log::debug!(target: "test", "Bob fetches from Eve..");
assert_matches!( assert_matches!(
bob.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(), bob.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();
@ -1038,7 +1106,10 @@ fn test_outdated_sigrefs() {
// Get the current state of eve's refs in alice's storage // Get the current state of eve's refs in alice's storage
log::debug!(target: "test", "Alice fetches from Eve.."); log::debug!(target: "test", "Alice fetches from Eve..");
assert_matches!( assert_matches!(
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(), alice
.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = alice.storage.repository(rid).unwrap(); let repo = alice.storage.repository(rid).unwrap();
@ -1059,7 +1130,10 @@ fn test_outdated_sigrefs() {
.follow(bob.id, Some(Alias::new("bob"))) .follow(bob.id, Some(Alias::new("bob")))
.unwrap(); .unwrap();
assert_matches!( assert_matches!(
alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(), alice
.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
@ -1093,11 +1167,15 @@ fn test_outdated_delegate_sigrefs() {
eve.connect(&alice); eve.connect(&alice);
converge([&alice, &bob, &eve]); converge([&alice, &bob, &eve]);
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(bob.storage.contains(&rid).unwrap()); assert!(bob.storage.contains(&rid).unwrap());
rad::fork(rid, &bob.signer, &bob.storage).unwrap(); rad::fork(rid, &bob.signer, &bob.storage).unwrap();
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); eve.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(eve.storage.contains(&rid).unwrap()); assert!(eve.storage.contains(&rid).unwrap());
rad::fork(rid, &eve.signer, &eve.storage).unwrap(); rad::fork(rid, &eve.signer, &eve.storage).unwrap();
@ -1105,13 +1183,18 @@ fn test_outdated_delegate_sigrefs() {
.handle .handle
.follow(eve.id, Some(Alias::new("eve"))) .follow(eve.id, Some(Alias::new("eve")))
.unwrap(); .unwrap();
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(); alice
.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap();
let repo = alice.storage.repository(rid).unwrap(); let repo = alice.storage.repository(rid).unwrap();
assert!(repo.remote(&eve.id).is_ok()); assert!(repo.remote(&eve.id).is_ok());
log::debug!(target: "test", "Bob fetches from Eve.."); log::debug!(target: "test", "Bob fetches from Eve..");
assert_matches!( assert_matches!(
bob.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(), bob.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();
@ -1132,7 +1215,9 @@ fn test_outdated_delegate_sigrefs() {
// Get the current state of eve's refs in alice's storage // Get the current state of eve's refs in alice's storage
log::debug!(target: "test", "Alice fetches from Eve.."); log::debug!(target: "test", "Alice fetches from Eve..");
assert_matches!( assert_matches!(
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(), eve.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = eve.storage.repository(rid).unwrap(); let repo = eve.storage.repository(rid).unwrap();
@ -1145,7 +1230,9 @@ fn test_outdated_delegate_sigrefs() {
eve.handle.follow(bob.id, Some(Alias::new("bob"))).unwrap(); eve.handle.follow(bob.id, Some(Alias::new("bob"))).unwrap();
assert_matches!( assert_matches!(
eve.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(), eve.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
@ -1175,7 +1262,9 @@ fn missing_default_branch() {
alice.connect(&bob); alice.connect(&bob);
converge([&alice, &bob]); converge([&alice, &bob]);
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(bob.storage.contains(&rid).unwrap()); assert!(bob.storage.contains(&rid).unwrap());
// Fetching from still works despite not having // Fetching from still works despite not having
@ -1185,7 +1274,10 @@ fn missing_default_branch() {
Title::new("Hello, Acme").unwrap(), Title::new("Hello, Acme").unwrap(),
"Popping in to say hello", "Popping in to say hello",
); );
alice.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(); alice
.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap();
{ {
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();
@ -1207,7 +1299,9 @@ fn missing_default_branch() {
// Fetching from her will still succeed. // Fetching from her will still succeed.
assert_matches!( assert_matches!(
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(), bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();
@ -1240,7 +1334,9 @@ fn missing_delegate_default_branch() {
converge([&seed]); converge([&seed]);
bob.connect(&seed); bob.connect(&seed);
bob.handle.fetch(rid, seed.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, seed.id, DEFAULT_TIMEOUT, None)
.unwrap();
bob_events bob_events
.wait( .wait(
|e| { |e| {
@ -1312,7 +1408,9 @@ fn missing_delegate_default_branch() {
// a) Bob's default branch is still missing // a) Bob's default branch is still missing
// b) Bob's issue is there // b) Bob's issue is there
assert_matches!( assert_matches!(
seed.handle.fetch(rid, bob.id, DEFAULT_TIMEOUT).unwrap(), seed.handle
.fetch(rid, bob.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
{ {
@ -1323,7 +1421,10 @@ fn missing_delegate_default_branch() {
// Do the same for Alice // Do the same for Alice
assert_matches!( assert_matches!(
alice.handle.fetch(rid, seed.id, DEFAULT_TIMEOUT).unwrap(), alice
.handle
.fetch(rid, seed.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
{ {
@ -1334,7 +1435,9 @@ fn missing_delegate_default_branch() {
// Check that Bob can still fetch from the seed // Check that Bob can still fetch from the seed
assert_matches!( assert_matches!(
bob.handle.fetch(rid, seed.id, DEFAULT_TIMEOUT).unwrap(), bob.handle
.fetch(rid, seed.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
} }
@ -1360,11 +1463,15 @@ fn test_background_foreground_fetch() {
alice.connect(&eve); alice.connect(&eve);
converge([&alice, &bob, &eve]); converge([&alice, &bob, &eve]);
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(bob.storage.contains(&rid).unwrap()); assert!(bob.storage.contains(&rid).unwrap());
rad::fork(rid, &bob.signer, &bob.storage).unwrap(); rad::fork(rid, &bob.signer, &bob.storage).unwrap();
eve.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); eve.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(eve.storage.contains(&rid).unwrap()); assert!(eve.storage.contains(&rid).unwrap());
rad::fork(rid, &eve.signer, &eve.storage).unwrap(); rad::fork(rid, &eve.signer, &eve.storage).unwrap();
@ -1373,7 +1480,10 @@ fn test_background_foreground_fetch() {
.handle .handle
.follow(eve.id, Some(Alias::new("eve"))) .follow(eve.id, Some(Alias::new("eve")))
.unwrap(); .unwrap();
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(); alice
.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap();
let repo = alice.storage.repository(rid).unwrap(); let repo = alice.storage.repository(rid).unwrap();
assert!(repo.remote(&eve.id).is_ok()); assert!(repo.remote(&eve.id).is_ok());
let repo = alice.storage.repository(rid).unwrap(); let repo = alice.storage.repository(rid).unwrap();
@ -1414,7 +1524,10 @@ fn test_background_foreground_fetch() {
// interfere // interfere
log::debug!(target: "test", "Alice fetches from Eve.."); log::debug!(target: "test", "Alice fetches from Eve..");
assert_matches!( assert_matches!(
alice.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(), alice
.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = alice.storage.repository(rid).unwrap(); let repo = alice.storage.repository(rid).unwrap();
@ -1514,7 +1627,10 @@ fn test_channel_reader_limit() {
let updated = bob.handle.seed(acme, Scope::All).unwrap(); let updated = bob.handle.seed(acme, Scope::All).unwrap();
assert!(updated); assert!(updated);
let result = bob.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(acme, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(!result.is_success()); assert!(!result.is_success());
let FetchResult::Failed { reason } = result else { let FetchResult::Failed { reason } = result else {
@ -1548,7 +1664,10 @@ fn test_fetch_emits_canonical_ref_update() {
bob.handle.seed(rid, Scope::All).unwrap(); bob.handle.seed(rid, Scope::All).unwrap();
alice.connect(&bob); alice.connect(&bob);
let result = bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(result.is_success()); assert!(result.is_success());
let default_branch: git::fmt::Qualified = { let default_branch: git::fmt::Qualified = {
@ -1606,12 +1725,14 @@ fn test_non_fastforward_identity_doc() {
// Bob and Eve have the same state for the repository // Bob and Eve have the same state for the repository
bob.handle.seed(rid, Scope::Followed).unwrap(); bob.handle.seed(rid, Scope::Followed).unwrap();
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
alice_laptop.handle.seed(rid, Scope::All).unwrap(); alice_laptop.handle.seed(rid, Scope::All).unwrap();
alice_laptop alice_laptop
.handle .handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT) .fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap(); .unwrap();
// Alice pushes new references to her laptop // Alice pushes new references to her laptop
let issue = alice_laptop.issue( let issue = alice_laptop.issue(
@ -1623,7 +1744,7 @@ fn test_non_fastforward_identity_doc() {
// Eve will fetch these references since her scope is "all" // Eve will fetch these references since her scope is "all"
eve.handle.seed(rid, Scope::All).unwrap(); eve.handle.seed(rid, Scope::All).unwrap();
eve.handle eve.handle
.fetch(rid, alice_laptop.id, DEFAULT_TIMEOUT) .fetch(rid, alice_laptop.id, DEFAULT_TIMEOUT, None)
.unwrap(); .unwrap();
assert!(has_issue(&eve, &issue)); assert!(has_issue(&eve, &issue));
@ -1651,7 +1772,10 @@ fn test_non_fastforward_identity_doc() {
// //
// Bob does not have the issue because Alice does not have the updates from // Bob does not have the issue because Alice does not have the updates from
// Alice's Laptop. // Alice's Laptop.
let result = bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); let result = bob
.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(matches!(result, FetchResult::Success { .. })); assert!(matches!(result, FetchResult::Success { .. }));
assert!(!has_issue(&bob, &issue)); assert!(!has_issue(&bob, &issue));
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();
@ -1662,7 +1786,9 @@ fn test_non_fastforward_identity_doc() {
// Bob fetches from Eve, the identity document should remain the same, but // Bob fetches from Eve, the identity document should remain the same, but
// since Bob now knows that Alice's Laptop is a delegate, the issue should // since Bob now knows that Alice's Laptop is a delegate, the issue should
// be fetched. // be fetched.
bob.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, eve.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(matches!(result, FetchResult::Success { .. })); assert!(matches!(result, FetchResult::Success { .. }));
assert!(has_issue(&bob, &issue)); assert!(has_issue(&bob, &issue));
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();
@ -1744,7 +1870,7 @@ fn test_block_prevents_fetch() {
let result = alice let result = alice
.handle .handle
.fetch(rid, bob.id, time::Duration::from_secs(5)) .fetch(rid, bob.id, time::Duration::from_secs(5), None)
.unwrap(); .unwrap();
assert_matches!(result, FetchResult::Failed { .. }); assert_matches!(result, FetchResult::Failed { .. });
@ -1768,7 +1894,9 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
alice.connect(&bob); alice.connect(&bob);
converge([&alice, &bob]); converge([&alice, &bob]);
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(); bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap();
assert!(bob.storage.contains(&rid).unwrap()); assert!(bob.storage.contains(&rid).unwrap());
rad::fork(rid, &bob.signer, &bob.storage).unwrap(); rad::fork(rid, &bob.signer, &bob.storage).unwrap();
@ -1782,7 +1910,9 @@ fn fetch_does_not_contain_rad_sigrefs_parent() {
log::debug!(target: "test", "Bob fetches from Alice.."); log::debug!(target: "test", "Bob fetches from Alice..");
assert_matches!( assert_matches!(
bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap(), bob.handle
.fetch(rid, alice.id, DEFAULT_TIMEOUT, None)
.unwrap(),
FetchResult::Success { .. } FetchResult::Success { .. }
); );
let repo = bob.storage.repository(rid).unwrap(); let repo = bob.storage.repository(rid).unwrap();

View File

@ -863,9 +863,13 @@ where
resp.err(e).ok(); resp.err(e).ok();
} }
}, },
Command::Fetch(rid, seed, timeout, resp) => { Command::Fetch(rid, seed, timeout, signed_references_minimum_feature_level, resp) => {
// TODO(finto): pass through feature-level let feature_level = signed_references_minimum_feature_level
let config = self.fetch_config().with_timeout(timeout); .unwrap_or(self.config.fetch.feature_level_min());
let config = self
.fetch_config()
.with_timeout(timeout)
.with_minimum_feature_level(feature_level);
self.fetch(rid, seed, vec![], config, Some(resp)); self.fetch(rid, seed, vec![], config, Some(resp));
} }
Command::Seed(rid, scope, resp) => { Command::Seed(rid, scope, resp) => {

View File

@ -8,6 +8,7 @@ use radicle::node::policy::Scope;
use radicle::node::FetchResult; use radicle::node::FetchResult;
use radicle::node::Seeds; use radicle::node::Seeds;
use radicle::node::{Address, Alias, Config, ConnectOptions}; use radicle::node::{Address, Alias, Config, ConnectOptions};
use radicle::storage::refs;
use radicle::storage::refs::RefsAt; use radicle::storage::refs::RefsAt;
use radicle_core::{NodeId, RepoId}; use radicle_core::{NodeId, RepoId};
use thiserror::Error; use thiserror::Error;
@ -90,7 +91,13 @@ pub enum Command {
/// sync status for given namespaces. /// sync status for given namespaces.
Seeds(RepoId, HashSet<PublicKey>, Responder<Seeds>), Seeds(RepoId, HashSet<PublicKey>, Responder<Seeds>),
/// Fetch the given repository from the network. /// Fetch the given repository from the network.
Fetch(RepoId, NodeId, time::Duration, Responder<FetchResult>), Fetch(
RepoId,
NodeId,
time::Duration,
Option<refs::FeatureLevel>,
Responder<FetchResult>,
),
/// Seed the given repository. /// Seed the given repository.
Seed(RepoId, Scope, Responder<bool>), Seed(RepoId, Scope, Responder<bool>),
/// Unseed the given repository. /// Unseed the given repository.
@ -150,9 +157,19 @@ impl Command {
rid: RepoId, rid: RepoId,
node_id: NodeId, node_id: NodeId,
duration: time::Duration, duration: time::Duration,
signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
) -> (Self, Receiver<Result<FetchResult>>) { ) -> (Self, Receiver<Result<FetchResult>>) {
let (responder, receiver) = Responder::oneshot(); let (responder, receiver) = Responder::oneshot();
(Self::Fetch(rid, node_id, duration, responder), receiver) (
Self::Fetch(
rid,
node_id,
duration,
signed_references_minimum_feature_level,
responder,
),
receiver,
)
} }
pub fn seed(rid: RepoId, scope: Scope) -> (Self, Receiver<Result<bool>>) { pub fn seed(rid: RepoId, scope: Scope) -> (Self, Receiver<Result<bool>>) {
@ -191,7 +208,10 @@ impl fmt::Debug for Command {
Self::Config(_) => write!(f, "Config"), Self::Config(_) => write!(f, "Config"),
Self::ListenAddrs(_) => write!(f, "ListenAddrs"), Self::ListenAddrs(_) => write!(f, "ListenAddrs"),
Self::Seeds(id, _, _) => write!(f, "Seeds({id})"), Self::Seeds(id, _, _) => write!(f, "Seeds({id})"),
Self::Fetch(id, node, _, _) => write!(f, "Fetch({id}, {node})"), Self::Fetch(id, node, _, feature_level, _) => match feature_level {
Some(feature_level) => write!(f, "Fetch({id}, {node} {feature_level})"),
None => write!(f, "Fetch({id}, {node})"),
},
Self::Seed(id, scope, _) => write!(f, "Seed({id}, {scope})"), Self::Seed(id, scope, _) => write!(f, "Seed({id}, {scope})"),
Self::Unseed(id, _) => write!(f, "Unseed({id})"), Self::Unseed(id, _) => write!(f, "Unseed({id})"),
Self::Follow(id, _, _) => write!(f, "Follow({id})"), Self::Follow(id, _, _) => write!(f, "Follow({id})"),

View File

@ -43,7 +43,7 @@ use crate::crypto::PublicKey;
use crate::git; use crate::git;
use crate::identity::RepoId; use crate::identity::RepoId;
use crate::profile; use crate::profile;
use crate::storage::refs::RefsAt; use crate::storage::refs::{FeatureLevel, RefsAt};
use crate::storage::RefUpdate; use crate::storage::RefUpdate;
pub use address::KnownAddress; pub use address::KnownAddress;
@ -975,6 +975,7 @@ pub trait Handle: Clone + Sync + Send {
id: RepoId, id: RepoId,
from: NodeId, from: NodeId,
timeout: time::Duration, timeout: time::Duration,
signed_references_minimum_feature_level: Option<FeatureLevel>,
) -> Result<FetchResult, Self::Error>; ) -> Result<FetchResult, Self::Error>;
/// Start seeding the given repo. May update the scope. Does nothing if the /// Start seeding the given repo. May update the scope. Does nothing if the
/// repo is already seeded. /// repo is already seeded.
@ -1244,6 +1245,7 @@ impl Handle for Node {
rid: RepoId, rid: RepoId,
from: NodeId, from: NodeId,
timeout: time::Duration, timeout: time::Duration,
signed_references_minimum_feature_level: Option<FeatureLevel>,
) -> Result<FetchResult, Error> { ) -> Result<FetchResult, Error> {
let result = self let result = self
.call( .call(
@ -1251,6 +1253,7 @@ impl Handle for Node {
rid, rid,
nid: from, nid: from,
timeout, timeout,
signed_references_minimum_feature_level,
}, },
DEFAULT_TIMEOUT.max(timeout), DEFAULT_TIMEOUT.max(timeout),
)? )?

View File

@ -15,6 +15,7 @@ use serde_json as json;
use crate::crypto::PublicKey; use crate::crypto::PublicKey;
use crate::identity::RepoId; use crate::identity::RepoId;
use crate::storage::refs;
use super::events::Event; use super::events::Event;
use super::NodeId; use super::NodeId;
@ -96,6 +97,7 @@ pub enum Command {
rid: RepoId, rid: RepoId,
nid: NodeId, nid: NodeId,
timeout: time::Duration, timeout: time::Duration,
signed_references_minimum_feature_level: Option<refs::FeatureLevel>,
}, },
/// Seed the given repository. /// Seed the given repository.