cli: Unify all syncing code
We were using similar but slightly different syncing code in the remote-helper, issues/patches and `rad sync`. This commit unifies them.
This commit is contained in:
parent
8f89f088c6
commit
e6728db15a
|
|
@ -23,6 +23,7 @@ use radicle::storage::RepositoryError;
|
||||||
|
|
||||||
use crate::commands::rad_checkout as checkout;
|
use crate::commands::rad_checkout as checkout;
|
||||||
use crate::commands::rad_sync as sync;
|
use crate::commands::rad_sync as sync;
|
||||||
|
use crate::node::SyncSettings;
|
||||||
use crate::project;
|
use crate::project;
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
|
@ -62,7 +63,7 @@ pub struct Options {
|
||||||
/// The seeding scope of the repository.
|
/// The seeding scope of the repository.
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
/// Sync settings.
|
/// Sync settings.
|
||||||
sync: sync::RepoSync,
|
sync: SyncSettings,
|
||||||
/// Fetch timeout.
|
/// Fetch timeout.
|
||||||
timeout: time::Duration,
|
timeout: time::Duration,
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +75,7 @@ impl Args for Options {
|
||||||
let mut parser = lexopt::Parser::from_args(args);
|
let mut parser = lexopt::Parser::from_args(args);
|
||||||
let mut id: Option<RepoId> = None;
|
let mut id: Option<RepoId> = None;
|
||||||
let mut scope = Scope::All;
|
let mut scope = Scope::All;
|
||||||
let mut sync = sync::RepoSync::default();
|
let mut sync = SyncSettings::default();
|
||||||
let mut timeout = time::Duration::from_secs(9);
|
let mut timeout = time::Duration::from_secs(9);
|
||||||
let mut directory = None;
|
let mut directory = None;
|
||||||
|
|
||||||
|
|
@ -150,8 +151,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
options.id,
|
options.id,
|
||||||
options.directory.clone(),
|
options.directory.clone(),
|
||||||
options.scope,
|
options.scope,
|
||||||
options.sync.with_profile(&profile),
|
options.sync.with_profile(&profile).timeout(options.timeout),
|
||||||
options.timeout,
|
|
||||||
&mut node,
|
&mut node,
|
||||||
&signer,
|
&signer,
|
||||||
&profile.storage,
|
&profile.storage,
|
||||||
|
|
@ -236,8 +236,7 @@ pub fn clone<G: Signer>(
|
||||||
id: RepoId,
|
id: RepoId,
|
||||||
directory: Option<PathBuf>,
|
directory: Option<PathBuf>,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
settings: sync::RepoSync,
|
settings: SyncSettings,
|
||||||
timeout: time::Duration,
|
|
||||||
node: &mut Node,
|
node: &mut Node,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
|
|
@ -260,7 +259,7 @@ pub fn clone<G: Signer>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = sync::fetch(id, settings, timeout, node)?;
|
let results = sync::fetch(id, settings, node)?;
|
||||||
let Ok(repository) = storage.repository(id) else {
|
let Ok(repository) = storage.repository(id) else {
|
||||||
// If we don't have the repository locally, even after attempting to fetch,
|
// If we don't have the repository locally, even after attempting to fetch,
|
||||||
// there's nothing we can do.
|
// there's nothing we can do.
|
||||||
|
|
|
||||||
|
|
@ -593,7 +593,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
|
|
||||||
if announce {
|
if announce {
|
||||||
let mut node = Node::new(profile.socket());
|
let mut node = Node::new(profile.socket());
|
||||||
node::announce(rid, &mut node)?;
|
node::announce(
|
||||||
|
&repo,
|
||||||
|
node::SyncSettings::default(),
|
||||||
|
node::SyncReporting::default(),
|
||||||
|
&mut node,
|
||||||
|
&profile,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -847,7 +847,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
|
|
||||||
if announce {
|
if announce {
|
||||||
let mut node = Node::new(profile.socket());
|
let mut node = Node::new(profile.socket());
|
||||||
node::announce(rid, &mut node)?;
|
node::announce(
|
||||||
|
&repository,
|
||||||
|
node::SyncSettings::default(),
|
||||||
|
node::SyncReporting::default(),
|
||||||
|
&mut node,
|
||||||
|
&profile,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time;
|
|
||||||
|
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
use radicle::git::RefString;
|
use radicle::git::RefString;
|
||||||
|
|
@ -10,6 +9,7 @@ use radicle_crypto::PublicKey;
|
||||||
use crate::commands::rad_checkout as checkout;
|
use crate::commands::rad_checkout as checkout;
|
||||||
use crate::commands::rad_follow as follow;
|
use crate::commands::rad_follow as follow;
|
||||||
use crate::commands::rad_sync as sync;
|
use crate::commands::rad_sync as sync;
|
||||||
|
use crate::node::SyncSettings;
|
||||||
use crate::project::SetupRemote;
|
use crate::project::SetupRemote;
|
||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
|
|
@ -31,8 +31,7 @@ pub fn run(
|
||||||
follow::follow(*nid, alias, &mut node, profile)?;
|
follow::follow(*nid, alias, &mut node, profile)?;
|
||||||
sync::fetch(
|
sync::fetch(
|
||||||
rid,
|
rid,
|
||||||
sync::RepoSync::default().with_profile(profile),
|
SyncSettings::default().with_profile(profile),
|
||||||
time::Duration::from_secs(9),
|
|
||||||
&mut node,
|
&mut node,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::time;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
|
@ -10,6 +9,7 @@ use radicle::{prelude::*, storage, Node};
|
||||||
use radicle_term::Element as _;
|
use radicle_term::Element as _;
|
||||||
|
|
||||||
use crate::commands::rad_sync as sync;
|
use crate::commands::rad_sync as sync;
|
||||||
|
use crate::node::SyncSettings;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
use crate::{project, terminal as term};
|
use crate::{project, terminal as term};
|
||||||
|
|
||||||
|
|
@ -117,8 +117,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
if fetch && node.is_running() {
|
if fetch && node.is_running() {
|
||||||
sync::fetch(
|
sync::fetch(
|
||||||
rid,
|
rid,
|
||||||
sync::RepoSync::default().with_profile(&profile),
|
SyncSettings::default().with_profile(&profile),
|
||||||
time::Duration::from_secs(6),
|
|
||||||
&mut node,
|
&mut node,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::ops::ControlFlow;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
|
|
@ -12,9 +11,11 @@ use radicle::node::AliasStore;
|
||||||
use radicle::node::Seed;
|
use radicle::node::Seed;
|
||||||
use radicle::node::{FetchResult, FetchResults, Handle as _, Node, SyncStatus};
|
use radicle::node::{FetchResult, FetchResults, Handle as _, Node, SyncStatus};
|
||||||
use radicle::prelude::{NodeId, Profile, RepoId};
|
use radicle::prelude::{NodeId, Profile, RepoId};
|
||||||
use radicle::storage::{ReadRepository, ReadStorage};
|
use radicle::storage::ReadStorage;
|
||||||
use radicle_term::Element;
|
use radicle_term::Element;
|
||||||
|
|
||||||
|
use crate::node::SyncReporting;
|
||||||
|
use crate::node::SyncSettings;
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
use crate::terminal::format::Author;
|
use crate::terminal::format::Author;
|
||||||
|
|
@ -100,7 +101,7 @@ impl FromStr for SortBy {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum SyncMode {
|
pub enum SyncMode {
|
||||||
Repo {
|
Repo {
|
||||||
settings: RepoSync,
|
settings: SyncSettings,
|
||||||
direction: SyncDirection,
|
direction: SyncDirection,
|
||||||
},
|
},
|
||||||
Inventory,
|
Inventory,
|
||||||
|
|
@ -109,58 +110,12 @@ pub enum SyncMode {
|
||||||
impl Default for SyncMode {
|
impl Default for SyncMode {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Repo {
|
Self::Repo {
|
||||||
settings: RepoSync::default(),
|
settings: SyncSettings::default(),
|
||||||
direction: SyncDirection::default(),
|
direction: SyncDirection::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Repository sync settings.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct RepoSync {
|
|
||||||
/// Sync with at least N replicas.
|
|
||||||
pub replicas: usize,
|
|
||||||
/// Sync with the given list of seeds.
|
|
||||||
pub seeds: BTreeSet<NodeId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepoSync {
|
|
||||||
pub fn from_seeds(seeds: impl IntoIterator<Item = NodeId>) -> Self {
|
|
||||||
let seeds = BTreeSet::from_iter(seeds);
|
|
||||||
Self {
|
|
||||||
replicas: seeds.len(),
|
|
||||||
seeds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use profile to populate sync settings, by adding preferred seeds if no seeds are specified,
|
|
||||||
/// and removing the local node from the set.
|
|
||||||
pub fn with_profile(mut self, profile: &Profile) -> Self {
|
|
||||||
// If no seeds were specified, add up to `replica` seeds from the preferred seeds.
|
|
||||||
if self.seeds.is_empty() {
|
|
||||||
self.seeds = profile
|
|
||||||
.config
|
|
||||||
.preferred_seeds
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.id)
|
|
||||||
.take(self.replicas)
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
// Remove our local node from the seed set just in case it was added by mistake.
|
|
||||||
self.seeds.remove(profile.id());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RepoSync {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
replicas: 3,
|
|
||||||
seeds: BTreeSet::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
||||||
pub enum SyncDirection {
|
pub enum SyncDirection {
|
||||||
Fetch,
|
Fetch,
|
||||||
|
|
@ -174,7 +129,6 @@ pub struct Options {
|
||||||
pub rid: Option<RepoId>,
|
pub rid: Option<RepoId>,
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
pub timeout: time::Duration,
|
|
||||||
pub sort_by: SortBy,
|
pub sort_by: SortBy,
|
||||||
pub op: Operation,
|
pub op: Operation,
|
||||||
}
|
}
|
||||||
|
|
@ -266,16 +220,12 @@ impl Args for Options {
|
||||||
(false, true) => SyncDirection::Announce,
|
(false, true) => SyncDirection::Announce,
|
||||||
};
|
};
|
||||||
let settings = if seeds.is_empty() {
|
let settings = if seeds.is_empty() {
|
||||||
RepoSync {
|
SyncSettings::from_replicas(replicas.unwrap_or(3))
|
||||||
replicas: replicas.unwrap_or(3),
|
|
||||||
seeds,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
RepoSync {
|
SyncSettings::from_seeds(seeds)
|
||||||
replicas: replicas.unwrap_or(seeds.len()),
|
|
||||||
seeds,
|
|
||||||
}
|
}
|
||||||
};
|
.timeout(timeout);
|
||||||
|
|
||||||
SyncMode::Repo {
|
SyncMode::Repo {
|
||||||
settings,
|
settings,
|
||||||
direction,
|
direction,
|
||||||
|
|
@ -287,7 +237,6 @@ impl Args for Options {
|
||||||
rid,
|
rid,
|
||||||
debug,
|
debug,
|
||||||
verbose,
|
verbose,
|
||||||
timeout,
|
|
||||||
sort_by,
|
sort_by,
|
||||||
op: op.unwrap_or(Operation::Synchronize(sync)),
|
op: op.unwrap_or(Operation::Synchronize(sync)),
|
||||||
},
|
},
|
||||||
|
|
@ -335,7 +284,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
if !profile.policies()?.is_seeding(&rid)? {
|
if !profile.policies()?.is_seeding(&rid)? {
|
||||||
anyhow::bail!("repository {rid} is not seeded");
|
anyhow::bail!("repository {rid} is not seeded");
|
||||||
}
|
}
|
||||||
let results = fetch(rid, settings.clone(), options.timeout, &mut node)?;
|
let results = fetch(rid, settings.clone(), &mut node)?;
|
||||||
let success = results.success().count();
|
let success = results.success().count();
|
||||||
let failed = results.failed().count();
|
let failed = results.failed().count();
|
||||||
|
|
||||||
|
|
@ -348,14 +297,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if [SyncDirection::Announce, SyncDirection::Both].contains(&direction) {
|
if [SyncDirection::Announce, SyncDirection::Both].contains(&direction) {
|
||||||
announce_refs(
|
announce_refs(rid, settings, options.debug, &mut node, &profile)?;
|
||||||
rid,
|
|
||||||
settings,
|
|
||||||
options.timeout,
|
|
||||||
options.debug,
|
|
||||||
&mut node,
|
|
||||||
&profile,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Operation::Synchronize(SyncMode::Inventory) => {
|
Operation::Synchronize(SyncMode::Inventory) => {
|
||||||
|
|
@ -436,8 +378,7 @@ fn sync_status(
|
||||||
|
|
||||||
fn announce_refs(
|
fn announce_refs(
|
||||||
rid: RepoId,
|
rid: RepoId,
|
||||||
settings: RepoSync,
|
settings: SyncSettings,
|
||||||
timeout: time::Duration,
|
|
||||||
debug: bool,
|
debug: bool,
|
||||||
node: &mut Node,
|
node: &mut Node,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
|
|
@ -447,90 +388,18 @@ fn announce_refs(
|
||||||
"nothing to announce, repository {rid} is not available locally"
|
"nothing to announce, repository {rid} is not available locally"
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let doc = repo.identity_doc()?;
|
|
||||||
let unsynced: Vec<_> = if doc.visibility.is_public() {
|
|
||||||
// All seeds.
|
|
||||||
let all = node.seeds(rid)?;
|
|
||||||
// Seeds in sync with us.
|
|
||||||
let synced = all.iter().filter(|s| s.is_synced());
|
|
||||||
// Replicas not counting our local replica.
|
|
||||||
let replicas = all
|
|
||||||
.iter()
|
|
||||||
.filter(|s| s.is_synced() && &s.nid != profile.id())
|
|
||||||
.count();
|
|
||||||
// Maximum replication factor we can achieve.
|
|
||||||
let max_replicas = all.iter().filter(|s| &s.nid != profile.id()).count();
|
|
||||||
// If the seeds we specified in the sync settings are all synced.
|
|
||||||
let is_seeds_synced = {
|
|
||||||
let synced = synced.map(|s| s.nid).collect::<BTreeSet<_>>();
|
|
||||||
settings.seeds.iter().all(|s| synced.contains(s))
|
|
||||||
};
|
|
||||||
// If we met our desired replica count. Note that this can never exceed the maximum count.
|
|
||||||
let is_replicas_synced = replicas >= settings.replicas.min(max_replicas);
|
|
||||||
|
|
||||||
// Nothing to do if we've met our sync state.
|
crate::node::announce(
|
||||||
if is_seeds_synced && is_replicas_synced {
|
&repo,
|
||||||
term::success!("Nothing to announce, already in sync with {replicas} node(s) (see `rad sync status`)");
|
settings,
|
||||||
return Ok(());
|
SyncReporting {
|
||||||
}
|
debug,
|
||||||
// Return nodes we can announce to.
|
..SyncReporting::default()
|
||||||
all.connected()
|
},
|
||||||
.filter(|s| !s.is_synced())
|
node,
|
||||||
.map(|s| s.nid)
|
profile,
|
||||||
.collect()
|
)?;
|
||||||
} else {
|
|
||||||
node.sessions()?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid))
|
|
||||||
.map(|s| s.nid)
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if unsynced.is_empty() {
|
|
||||||
term::info!("Not connected to any seeds for {rid}.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut spinner = term::spinner(format!("Found {} seed(s)..", unsynced.len()));
|
|
||||||
let result = node.announce(rid, unsynced, timeout, |event, replicas| match event {
|
|
||||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
|
||||||
node::AnnounceEvent::RefsSynced { remote, time } => {
|
|
||||||
spinner.message(format!("Synced with {remote} in {time:?}.."));
|
|
||||||
|
|
||||||
// We're done syncing when both of these conditions are met:
|
|
||||||
//
|
|
||||||
// 1. We've matched or exceeded our target replica count.
|
|
||||||
// 2. We've synced with the seeds specified manually.
|
|
||||||
if replicas.len() >= settings.replicas
|
|
||||||
&& settings.seeds.iter().all(|s| replicas.contains_key(s))
|
|
||||||
{
|
|
||||||
ControlFlow::Break(())
|
|
||||||
} else {
|
|
||||||
ControlFlow::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if result.synced.is_empty() {
|
|
||||||
spinner.failed();
|
|
||||||
} else {
|
|
||||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
|
||||||
spinner.finish();
|
|
||||||
if debug {
|
|
||||||
for (seed, time) in &result.synced {
|
|
||||||
term::println(
|
|
||||||
" ",
|
|
||||||
term::format::dim(format!("Synced with {seed} in {time:?}")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for seed in result.timed_out {
|
|
||||||
term::notice!("Seed {seed} timed out..");
|
|
||||||
}
|
|
||||||
if result.synced.is_empty() {
|
|
||||||
anyhow::bail!("all seeds timed out");
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -546,8 +415,7 @@ pub fn announce_inventory(mut node: Node) -> anyhow::Result<()> {
|
||||||
|
|
||||||
pub fn fetch(
|
pub fn fetch(
|
||||||
rid: RepoId,
|
rid: RepoId,
|
||||||
settings: RepoSync,
|
settings: SyncSettings,
|
||||||
timeout: time::Duration,
|
|
||||||
node: &mut Node,
|
node: &mut Node,
|
||||||
) -> Result<FetchResults, node::Error> {
|
) -> Result<FetchResults, node::Error> {
|
||||||
let local = node.nid()?;
|
let local = node.nid()?;
|
||||||
|
|
@ -567,7 +435,7 @@ pub fn fetch(
|
||||||
term::warning(format!("node {nid} is not connected.. skipping"));
|
term::warning(format!("node {nid} is not connected.. skipping"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let result = fetch_from(rid, nid, timeout, node)?;
|
let result = fetch_from(rid, nid, settings.timeout, node)?;
|
||||||
results.push(*nid, result);
|
results.push(*nid, result);
|
||||||
}
|
}
|
||||||
if results.success().count() >= replicas {
|
if results.success().count() >= replicas {
|
||||||
|
|
@ -582,7 +450,7 @@ pub fn fetch(
|
||||||
.take(replicas)
|
.take(replicas)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for nid in connected {
|
for nid in connected {
|
||||||
let result = fetch_from(rid, &nid, timeout, node)?;
|
let result = fetch_from(rid, &nid, settings.timeout, node)?;
|
||||||
results.push(nid, result);
|
results.push(nid, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,10 +466,10 @@ pub fn fetch(
|
||||||
if connect(
|
if connect(
|
||||||
seed.nid,
|
seed.nid,
|
||||||
seed.addrs.into_iter().map(|ka| ka.addr),
|
seed.addrs.into_iter().map(|ka| ka.addr),
|
||||||
timeout,
|
settings.timeout,
|
||||||
node,
|
node,
|
||||||
)? {
|
)? {
|
||||||
let result = fetch_from(rid, &seed.nid, timeout, node)?;
|
let result = fetch_from(rid, &seed.nid, settings.timeout, node)?;
|
||||||
results.push(seed.nid, result);
|
results.push(seed.nid, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,268 @@
|
||||||
|
use core::time;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::io;
|
||||||
|
use std::io::Write;
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use radicle::identity::RepoId;
|
use radicle::node::{self, AnnounceResult};
|
||||||
use radicle::node;
|
use radicle::node::{Handle as _, NodeId};
|
||||||
use radicle::node::Handle as _;
|
use radicle::storage::{ReadRepository, RepositoryError};
|
||||||
use radicle::Node;
|
use radicle::{Node, Profile};
|
||||||
|
use radicle_term::format;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
|
||||||
/// Announce changes to the network.
|
/// Default time to wait for syncing to complete.
|
||||||
pub fn announce(rid: RepoId, node: &mut Node) -> anyhow::Result<()> {
|
pub const DEFAULT_SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(9);
|
||||||
match announce_(rid, node) {
|
|
||||||
Ok(()) => Ok(()),
|
/// Repository sync settings.
|
||||||
Err(e) if e.is_connection_err() => {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
term::hint("Node is stopped. To announce changes to the network, start it with `rad node start`.");
|
pub struct SyncSettings {
|
||||||
Ok(())
|
/// Sync with at least N replicas.
|
||||||
|
pub replicas: usize,
|
||||||
|
/// Sync with the given list of seeds.
|
||||||
|
pub seeds: BTreeSet<NodeId>,
|
||||||
|
/// How long to wait for syncing to complete.
|
||||||
|
pub timeout: time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncSettings {
|
||||||
|
/// Create a [`RepoSync`] from a list of seeds.
|
||||||
|
pub fn from_seeds(seeds: impl IntoIterator<Item = NodeId>) -> Self {
|
||||||
|
let seeds = BTreeSet::from_iter(seeds);
|
||||||
|
Self {
|
||||||
|
replicas: seeds.len(),
|
||||||
|
seeds,
|
||||||
|
timeout: DEFAULT_SYNC_TIMEOUT,
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.into()),
|
}
|
||||||
|
|
||||||
|
/// Create a [`RepoSync`] from a replica count.
|
||||||
|
pub fn from_replicas(replicas: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
replicas,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set sync timeout. Defaults to [`DEFAULT_SYNC_TIMEOUT`].
|
||||||
|
pub fn timeout(mut self, timeout: time::Duration) -> Self {
|
||||||
|
self.timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use profile to populate sync settings, by adding preferred seeds if no seeds are specified,
|
||||||
|
/// and removing the local node from the set.
|
||||||
|
pub fn with_profile(mut self, profile: &Profile) -> Self {
|
||||||
|
// If no seeds were specified, add up to `replica` seeds from the preferred seeds.
|
||||||
|
if self.seeds.is_empty() {
|
||||||
|
self.seeds = profile
|
||||||
|
.config
|
||||||
|
.preferred_seeds
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.id)
|
||||||
|
.take(self.replicas)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
// Remove our local node from the seed set just in case it was added by mistake.
|
||||||
|
self.seeds.remove(profile.id());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announce_(rid: RepoId, node: &mut Node) -> Result<(), radicle::node::Error> {
|
impl Default for SyncSettings {
|
||||||
let seeds = node.seeds(rid)?;
|
fn default() -> Self {
|
||||||
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
Self {
|
||||||
|
replicas: 3,
|
||||||
|
seeds: BTreeSet::new(),
|
||||||
|
timeout: DEFAULT_SYNC_TIMEOUT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if connected.is_empty() {
|
/// Error while syncing.
|
||||||
term::info!("Not connected to any seeds.");
|
#[derive(thiserror::Error, Debug)]
|
||||||
return Ok(());
|
pub enum SyncError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Repository(#[from] RepositoryError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Node(#[from] radicle::node::Error),
|
||||||
|
#[error("all seeds timed out")]
|
||||||
|
AllSeedsTimedOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncError {
|
||||||
|
fn is_connection_err(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Node(e) => e.is_connection_err(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes sync output.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SyncWriter {
|
||||||
|
/// Write to standard out.
|
||||||
|
Stdout(io::Stdout),
|
||||||
|
/// Write to standard error.
|
||||||
|
Stderr(io::Stderr),
|
||||||
|
/// Discard output, like [`std::io::sink`].
|
||||||
|
Sink,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for SyncWriter {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Stdout(_) => Self::Stdout(io::stdout()),
|
||||||
|
Self::Stderr(_) => Self::Stderr(io::stderr()),
|
||||||
|
Self::Sink => Self::Sink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for SyncWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Stdout(stdout) => stdout.write(buf),
|
||||||
|
Self::Stderr(stderr) => stderr.write(buf),
|
||||||
|
Self::Sink => Ok(buf.len()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut spinner = term::spinner(format!("Syncing with {} node(s)..", connected.len()));
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Stdout(stdout) => stdout.flush(),
|
||||||
|
Self::Stderr(stderr) => stderr.flush(),
|
||||||
|
Self::Sink => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configures how sync progress is reported.
|
||||||
|
pub struct SyncReporting {
|
||||||
|
/// Progress messages or animations.
|
||||||
|
pub progress: SyncWriter,
|
||||||
|
/// Completion messages.
|
||||||
|
pub completion: SyncWriter,
|
||||||
|
/// Debug output.
|
||||||
|
pub debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SyncReporting {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
progress: SyncWriter::Stderr(io::stderr()),
|
||||||
|
completion: SyncWriter::Stdout(io::stdout()),
|
||||||
|
debug: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Announce changes to the network.
|
||||||
|
pub fn announce<R: ReadRepository>(
|
||||||
|
repo: &R,
|
||||||
|
settings: SyncSettings,
|
||||||
|
reporting: SyncReporting,
|
||||||
|
node: &mut Node,
|
||||||
|
profile: &Profile,
|
||||||
|
) -> Result<AnnounceResult, SyncError> {
|
||||||
|
match announce_(repo, settings, reporting, node, profile) {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(e) if e.is_connection_err() => {
|
||||||
|
term::hint("Node is stopped. To announce changes to the network, start it with `rad node start`.");
|
||||||
|
Ok(AnnounceResult::default())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn announce_<R: ReadRepository>(
|
||||||
|
repo: &R,
|
||||||
|
settings: SyncSettings,
|
||||||
|
mut reporting: SyncReporting,
|
||||||
|
node: &mut Node,
|
||||||
|
profile: &Profile,
|
||||||
|
) -> Result<AnnounceResult, SyncError> {
|
||||||
|
let rid = repo.id();
|
||||||
|
let doc = repo.identity_doc()?;
|
||||||
|
let settings = settings.with_profile(profile);
|
||||||
|
let unsynced: Vec<_> = if doc.visibility.is_public() {
|
||||||
|
// All seeds.
|
||||||
|
let all = node.seeds(rid)?;
|
||||||
|
// Seeds in sync with us.
|
||||||
|
let synced = all.iter().filter(|s| s.is_synced());
|
||||||
|
// Replicas not counting our local replica.
|
||||||
|
let replicas = all
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.is_synced() && &s.nid != profile.id())
|
||||||
|
.count();
|
||||||
|
// Maximum replication factor we can achieve.
|
||||||
|
let max_replicas = all.iter().filter(|s| &s.nid != profile.id()).count();
|
||||||
|
// If the seeds we specified in the sync settings are all synced.
|
||||||
|
let is_seeds_synced = {
|
||||||
|
let synced = synced.map(|s| s.nid).collect::<BTreeSet<_>>();
|
||||||
|
settings.seeds.iter().all(|s| synced.contains(s))
|
||||||
|
};
|
||||||
|
// If we met our desired replica count. Note that this can never exceed the maximum count.
|
||||||
|
let is_replicas_synced = replicas >= settings.replicas.min(max_replicas);
|
||||||
|
|
||||||
|
// Nothing to do if we've met our sync state.
|
||||||
|
if is_seeds_synced && is_replicas_synced {
|
||||||
|
term::success!(
|
||||||
|
&mut reporting.completion;
|
||||||
|
"Nothing to announce, already in sync with {replicas} node(s) (see `rad sync status`)"
|
||||||
|
);
|
||||||
|
return Ok(AnnounceResult::default());
|
||||||
|
}
|
||||||
|
// Return nodes we can announce to.
|
||||||
|
all.connected()
|
||||||
|
.filter(|s| !s.is_synced())
|
||||||
|
.map(|s| s.nid)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
node.sessions()?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid))
|
||||||
|
.map(|s| s.nid)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
if unsynced.is_empty() {
|
||||||
|
term::info!(&mut reporting.completion; "Not connected to any seeds for {rid}.");
|
||||||
|
return Ok(AnnounceResult::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut spinner = term::spinner_to(
|
||||||
|
format!("Found {} seed(s)..", unsynced.len()),
|
||||||
|
reporting.completion.clone(),
|
||||||
|
reporting.progress.clone(),
|
||||||
|
);
|
||||||
let result = node.announce(
|
let result = node.announce(
|
||||||
rid,
|
rid,
|
||||||
connected,
|
unsynced,
|
||||||
Duration::from_secs(9),
|
settings.timeout,
|
||||||
|event, _| match event {
|
|event, replicas| match event {
|
||||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
||||||
node::AnnounceEvent::RefsSynced { remote, time } => {
|
node::AnnounceEvent::RefsSynced { remote, time } => {
|
||||||
spinner.message(format!("Synced with {remote} in {time:?}.."));
|
spinner.message(format!(
|
||||||
|
"Synced with {} in {}..",
|
||||||
|
format::dim(remote),
|
||||||
|
format::dim(format!("{time:?}"))
|
||||||
|
));
|
||||||
|
|
||||||
|
// We're done syncing when both of these conditions are met:
|
||||||
|
//
|
||||||
|
// 1. We've matched or exceeded our target replica count.
|
||||||
|
// 2. We've synced with the seeds specified manually.
|
||||||
|
if replicas.len() >= settings.replicas
|
||||||
|
&& settings.seeds.iter().all(|s| replicas.contains_key(s))
|
||||||
|
{
|
||||||
|
ControlFlow::Break(())
|
||||||
|
} else {
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -48,6 +271,25 @@ fn announce_(rid: RepoId, node: &mut Node) -> Result<(), radicle::node::Error> {
|
||||||
} else {
|
} else {
|
||||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
|
|
||||||
|
if reporting.debug {
|
||||||
|
for (seed, time) in &result.synced {
|
||||||
|
writeln!(
|
||||||
|
&mut reporting.completion,
|
||||||
|
" {}",
|
||||||
|
term::format::dim(format!("Synced with {seed} in {time:?}")),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
Ok(())
|
}
|
||||||
|
}
|
||||||
|
for seed in &result.timed_out {
|
||||||
|
if settings.seeds.contains(seed) {
|
||||||
|
term::notice!(&mut reporting.completion; "Seed {seed} timed out..");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.synced.is_empty() {
|
||||||
|
return Err(SyncError::AllSeedsTimedOut);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1054,7 +1054,7 @@ where
|
||||||
self.fetch_refs_at(rid, from, refs, FETCH_TIMEOUT, channel);
|
self.fetch_refs_at(rid, from, refs, FETCH_TIMEOUT, channel);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
debug!(target: "service", "Skipping dequeued fetch for {rid}, all refs are already in local storage");
|
trace!(target: "service", "Skipping dequeued fetch for {rid}, all refs are already in local storage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ pub struct Allow {
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
/// Don't sync after push.
|
/// Don't sync after push.
|
||||||
no_sync: bool,
|
no_sync: bool,
|
||||||
|
/// Sync debugging.
|
||||||
|
sync_debug: bool,
|
||||||
/// Enable hints.
|
/// Enable hints.
|
||||||
hints: bool,
|
hints: bool,
|
||||||
/// Open patch in draft mode.
|
/// Open patch in draft mode.
|
||||||
|
|
@ -210,6 +212,7 @@ fn push_option(args: &[&str], opts: &mut Options) -> Result<(), Error> {
|
||||||
match args {
|
match args {
|
||||||
["hints"] => opts.hints = true,
|
["hints"] => opts.hints = true,
|
||||||
["sync"] => opts.no_sync = false,
|
["sync"] => opts.no_sync = false,
|
||||||
|
["sync.debug"] => opts.sync_debug = true,
|
||||||
["no-sync"] => opts.no_sync = true,
|
["no-sync"] => opts.no_sync = true,
|
||||||
["patch.draft"] => opts.draft = true,
|
["patch.draft"] => opts.draft = true,
|
||||||
["allow.rollback"] => opts.allow.rollback = true,
|
["allow.rollback"] => opts.allow.rollback = true,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
use std::ops::ControlFlow;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time;
|
|
||||||
use std::{assert_eq, io};
|
use std::{assert_eq, io};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -18,19 +16,16 @@ use radicle::explorer::ExplorerResource;
|
||||||
use radicle::identity::Did;
|
use radicle::identity::Did;
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::{Handle, NodeId};
|
use radicle::node::{Handle, NodeId};
|
||||||
use radicle::prelude::RepoId;
|
|
||||||
use radicle::storage;
|
use radicle::storage;
|
||||||
use radicle::storage::git::transport::local::Url;
|
use radicle::storage::git::transport::local::Url;
|
||||||
use radicle::storage::{ReadRepository, SignRepository as _, WriteRepository};
|
use radicle::storage::{ReadRepository, SignRepository as _, WriteRepository};
|
||||||
use radicle::Profile;
|
use radicle::Profile;
|
||||||
use radicle::{git, rad};
|
use radicle::{git, rad};
|
||||||
use radicle_cli::terminal as cli;
|
use radicle_cli as cli;
|
||||||
|
use radicle_cli::terminal as term;
|
||||||
|
|
||||||
use crate::{hint, read_line, Options};
|
use crate::{hint, read_line, Options};
|
||||||
|
|
||||||
/// Default timeout for syncing to the network after a push.
|
|
||||||
const DEFAULT_SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(9);
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Public key doesn't match the remote namespace we're pushing to.
|
/// Public key doesn't match the remote namespace we're pushing to.
|
||||||
|
|
@ -89,7 +84,7 @@ pub enum Error {
|
||||||
PatchCache(#[from] patch::cache::Error),
|
PatchCache(#[from] patch::cache::Error),
|
||||||
/// Patch edit message error.
|
/// Patch edit message error.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
PatchEdit(#[from] cli::patch::Error),
|
PatchEdit(#[from] term::patch::Error),
|
||||||
/// Policy config error.
|
/// Policy config error.
|
||||||
#[error("node policy: {0}")]
|
#[error("node policy: {0}")]
|
||||||
Policy(#[from] node::policy::config::Error),
|
Policy(#[from] node::policy::config::Error),
|
||||||
|
|
@ -329,8 +324,8 @@ pub fn run(
|
||||||
if head.is_updated() {
|
if head.is_updated() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Canonical head updated to {}",
|
"{} Canonical head updated to {}",
|
||||||
cli::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
cli::format::secondary(head.new),
|
term::format::secondary(head.new),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,7 +337,7 @@ pub fn run(
|
||||||
let node = radicle::Node::new(profile.socket());
|
let node = radicle::Node::new(profile.socket());
|
||||||
if node.is_running() {
|
if node.is_running() {
|
||||||
// Nb. allow this to fail. The push to local storage was still successful.
|
// Nb. allow this to fail. The push to local storage was still successful.
|
||||||
sync(stored.id, ok.into_values().flatten(), node, profile).ok();
|
sync(stored, ok.into_values().flatten(), opts, node, profile).ok();
|
||||||
} else if hints {
|
} else if hints {
|
||||||
hint("offline push, your node is not running");
|
hint("offline push, your node is not running");
|
||||||
hint("to sync with the network, run `rad node start`");
|
hint("to sync with the network, run `rad node start`");
|
||||||
|
|
@ -397,7 +392,7 @@ fn patch_open<G: Signer>(
|
||||||
return Err(Error::EmptyPatch);
|
return Err(Error::EmptyPatch);
|
||||||
}
|
}
|
||||||
let (title, description) =
|
let (title, description) =
|
||||||
cli::patch::get_create_message(opts.message, &stored.backend, &base, &head)?;
|
term::patch::get_create_message(opts.message, &stored.backend, &base, &head)?;
|
||||||
|
|
||||||
let patch = if opts.draft {
|
let patch = if opts.draft {
|
||||||
patches.draft(
|
patches.draft(
|
||||||
|
|
@ -431,8 +426,8 @@ fn patch_open<G: Signer>(
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Patch {} {action}",
|
"{} Patch {} {action}",
|
||||||
cli::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
cli::format::tertiary(patch),
|
term::format::tertiary(patch),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create long-lived patch head reference, now that we know the Patch ID.
|
// Create long-lived patch head reference, now that we know the Patch ID.
|
||||||
|
|
@ -509,7 +504,7 @@ fn patch_update<G: Signer>(
|
||||||
if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
|
if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let message = cli::patch::get_update_message(
|
let message = term::patch::get_update_message(
|
||||||
opts.message,
|
opts.message,
|
||||||
&stored.backend,
|
&stored.backend,
|
||||||
patch.latest().1,
|
patch.latest().1,
|
||||||
|
|
@ -527,9 +522,9 @@ fn patch_update<G: Signer>(
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Patch {} updated to revision {}",
|
"{} Patch {} updated to revision {}",
|
||||||
cli::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
cli::format::tertiary(cli::format::cob(&patch_id)),
|
term::format::tertiary(term::format::cob(&patch_id)),
|
||||||
cli::format::dim(revision)
|
term::format::dim(revision)
|
||||||
);
|
);
|
||||||
|
|
||||||
// In this case, the patch was already merged via git, and pushed to storage.
|
// In this case, the patch was already merged via git, and pushed to storage.
|
||||||
|
|
@ -641,15 +636,15 @@ fn patch_merge<C: cob::cache::Update<patch::Patch>, G: Signer>(
|
||||||
if revision == latest {
|
if revision == latest {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Patch {} merged",
|
"{} Patch {} merged",
|
||||||
cli::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
cli::format::tertiary(merged.patch)
|
term::format::tertiary(merged.patch)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} Patch {} merged at revision {}",
|
"{} Patch {} merged at revision {}",
|
||||||
cli::format::positive("✓"),
|
term::format::positive("✓"),
|
||||||
cli::format::tertiary(merged.patch),
|
term::format::tertiary(merged.patch),
|
||||||
cli::format::dim(cli::format::oid(revision)),
|
term::format::dim(term::format::oid(revision)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,57 +676,38 @@ fn push_ref(
|
||||||
|
|
||||||
/// Sync with the network.
|
/// Sync with the network.
|
||||||
fn sync(
|
fn sync(
|
||||||
rid: RepoId,
|
repo: &storage::git::Repository,
|
||||||
updated: impl Iterator<Item = ExplorerResource>,
|
updated: impl Iterator<Item = ExplorerResource>,
|
||||||
|
opts: Options,
|
||||||
mut node: radicle::Node,
|
mut node: radicle::Node,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
) -> Result<(), radicle::node::Error> {
|
) -> Result<(), cli::node::SyncError> {
|
||||||
let seeds = node.seeds(rid)?;
|
let progress = if io::stderr().is_terminal() {
|
||||||
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
cli::node::SyncWriter::Stderr(io::stderr())
|
||||||
let mut replicated = HashSet::new();
|
|
||||||
|
|
||||||
if connected.is_empty() {
|
|
||||||
eprintln!("Not connected to any seeds.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let message = format!("Syncing with {} node(s)..", connected.len());
|
|
||||||
let mut spinner = if io::stderr().is_terminal() {
|
|
||||||
cli::spinner_to(message, io::stderr(), io::stderr())
|
|
||||||
} else {
|
} else {
|
||||||
cli::spinner_to(message, io::stderr(), io::sink())
|
cli::node::SyncWriter::Sink
|
||||||
};
|
};
|
||||||
let result = node.announce(
|
let result = cli::node::announce(
|
||||||
rid,
|
repo,
|
||||||
connected,
|
cli::node::SyncSettings::default().with_profile(profile),
|
||||||
DEFAULT_SYNC_TIMEOUT,
|
cli::node::SyncReporting {
|
||||||
|event, _| match event {
|
progress,
|
||||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
completion: cli::node::SyncWriter::Stderr(io::stderr()),
|
||||||
node::AnnounceEvent::RefsSynced { remote, time } => {
|
debug: opts.sync_debug,
|
||||||
replicated.insert(remote);
|
|
||||||
spinner.message(format!(
|
|
||||||
"Synced with {} in {time:?}..",
|
|
||||||
cli::format::dim(remote)
|
|
||||||
));
|
|
||||||
ControlFlow::Continue(())
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
&mut node,
|
||||||
|
profile,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if result.synced.is_empty() {
|
|
||||||
spinner.failed();
|
|
||||||
} else {
|
|
||||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
|
||||||
spinner.finish();
|
|
||||||
}
|
|
||||||
let mut urls = Vec::new();
|
let mut urls = Vec::new();
|
||||||
|
|
||||||
for seed in profile.config.preferred_seeds.iter() {
|
for seed in profile.config.preferred_seeds.iter() {
|
||||||
if replicated.contains(&seed.id) {
|
if result.synced(&seed.id).is_some() {
|
||||||
for resource in updated {
|
for resource in updated {
|
||||||
let url = profile
|
let url = profile
|
||||||
.config
|
.config
|
||||||
.public_explorer
|
.public_explorer
|
||||||
.url(seed.addr.host.clone(), rid)
|
.url(seed.addr.host.clone(), repo.id)
|
||||||
.resource(resource);
|
.resource(resource);
|
||||||
|
|
||||||
urls.push(url);
|
urls.push(url);
|
||||||
|
|
@ -744,7 +720,7 @@ fn sync(
|
||||||
if !urls.is_empty() {
|
if !urls.is_empty() {
|
||||||
eprintln!();
|
eprintln!();
|
||||||
for url in urls {
|
for url in urls {
|
||||||
eprintln!(" {}", cli::format::dim(url));
|
eprintln!(" {}", term::format::dim(url));
|
||||||
}
|
}
|
||||||
eprintln!();
|
eprintln!();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! info {
|
macro_rules! info {
|
||||||
|
($writer:expr; $($arg:tt)*) => ({
|
||||||
|
writeln!($writer, $($arg)*).ok();
|
||||||
|
});
|
||||||
($($arg:tt)*) => ({
|
($($arg:tt)*) => ({
|
||||||
println!("{}", format_args!($($arg)*));
|
println!("{}", format_args!($($arg)*));
|
||||||
})
|
})
|
||||||
|
|
@ -50,9 +53,14 @@ macro_rules! info {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! success {
|
macro_rules! success {
|
||||||
|
// Pattern when a writer is provided.
|
||||||
|
($writer:expr; $($arg:tt)*) => ({
|
||||||
|
$crate::io::success_args($writer, format_args!($($arg)*));
|
||||||
|
});
|
||||||
|
// Pattern without writer.
|
||||||
($($arg:tt)*) => ({
|
($($arg:tt)*) => ({
|
||||||
$crate::io::success_args(format_args!($($arg)*));
|
$crate::io::success_args(&mut std::io::stdout(), format_args!($($arg)*));
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
@ -64,8 +72,12 @@ macro_rules! tip {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! notice {
|
macro_rules! notice {
|
||||||
|
// Pattern when a writer is provided.
|
||||||
|
($writer:expr; $($arg:tt)*) => ({
|
||||||
|
$crate::io::notice_args($writer, format_args!($($arg)*));
|
||||||
|
});
|
||||||
($($arg:tt)*) => ({
|
($($arg:tt)*) => ({
|
||||||
$crate::io::notice_args(format_args!($($arg)*));
|
$crate::io::notice_args(&mut std::io::stdout(), format_args!($($arg)*));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,8 +86,8 @@ pub use notice;
|
||||||
pub use success;
|
pub use success;
|
||||||
pub use tip;
|
pub use tip;
|
||||||
|
|
||||||
pub fn success_args(args: fmt::Arguments) {
|
pub fn success_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
|
||||||
println!("{} {args}", Paint::green("✓"));
|
writeln!(w, "{} {args}", Paint::green("✓")).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tip_args(args: fmt::Arguments) {
|
pub fn tip_args(args: fmt::Arguments) {
|
||||||
|
|
@ -86,8 +98,8 @@ pub fn tip_args(args: fmt::Arguments) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notice_args(args: fmt::Arguments) {
|
pub fn notice_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
|
||||||
println!("{} {args}", Paint::new("!").dim());
|
writeln!(w, "{} {args}", Paint::new("!").dim()).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn columns() -> Option<usize> {
|
pub fn columns() -> Option<usize> {
|
||||||
|
|
|
||||||
|
|
@ -637,7 +637,7 @@ impl From<Vec<Seed>> for Seeds {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Announcement result returned by [`Node::announce`].
|
/// Announcement result returned by [`Node::announce`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct AnnounceResult {
|
pub struct AnnounceResult {
|
||||||
/// Nodes that timed out.
|
/// Nodes that timed out.
|
||||||
pub timed_out: Vec<NodeId>,
|
pub timed_out: Vec<NodeId>,
|
||||||
|
|
@ -645,6 +645,16 @@ pub struct AnnounceResult {
|
||||||
pub synced: Vec<(NodeId, time::Duration)>,
|
pub synced: Vec<(NodeId, time::Duration)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AnnounceResult {
|
||||||
|
/// Check if a node synced successfully.
|
||||||
|
pub fn synced(&self, nid: &NodeId) -> Option<time::Duration> {
|
||||||
|
self.synced
|
||||||
|
.iter()
|
||||||
|
.find(|(id, _)| id == nid)
|
||||||
|
.map(|(_, time)| *time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A sync event, emitted by [`Node::announce`].
|
/// A sync event, emitted by [`Node::announce`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AnnounceEvent {
|
pub enum AnnounceEvent {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue