1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::collections::BTreeSet;
4use std::collections::HashSet;
5use std::ffi::OsString;
6use std::str::FromStr;
7use std::time;
8
9use anyhow::{anyhow, Context as _};
10
11use radicle::node;
12use radicle::node::address::Store;
13use radicle::node::sync;
14use radicle::node::sync::fetch::SuccessfulOutcome;
15use radicle::node::SyncedAt;
16use radicle::node::{AliasStore, Handle as _, Node, Seed, SyncStatus};
17use radicle::prelude::{NodeId, Profile, RepoId};
18use radicle::storage::ReadRepository;
19use radicle::storage::RefUpdate;
20use radicle::storage::{ReadStorage, RemoteRepository};
21use radicle_term::Element;
22
23use crate::node::SyncReporting;
24use crate::node::SyncSettings;
25use crate::terminal as term;
26use crate::terminal::args::{Args, Error, Help};
27use crate::terminal::format::Author;
28use crate::terminal::{Table, TableOptions};
29
30pub const HELP: Help = Help {
31 name: "sync",
32 description: "Sync repositories to the network",
33 version: env!("RADICLE_VERSION"),
34 usage: r#"
35Usage
36
37 rad sync [--fetch | --announce] [<rid>] [<option>...]
38 rad sync --inventory [<option>...]
39 rad sync status [<rid>] [<option>...]
40
41 By default, the current repository is synchronized both ways.
42 If an <rid> is specified, that repository is synced instead.
43
44 The process begins by fetching changes from connected seeds,
45 followed by announcing local refs to peers, thereby prompting
46 them to fetch from us.
47
48 When `--fetch` is specified, any number of seeds may be given
49 using the `--seed` option, eg. `--seed <nid>@<addr>:<port>`.
50
51 When `--replicas` is specified, the given replication factor will try
52 to be matched. For example, `--replicas 5` will sync with 5 seeds.
53
54 The synchronization process can be configured using `--replicas <min>` and
55 `--replicas-max <max>`. If these options are used independently, then the
56 replication factor is taken as the given `<min>`/`<max>` value. If the
57 options are used together, then the replication factor has a minimum and
58 maximum bound.
59
60 For fetching, the synchronization process will be considered successful if
61 at least `<min>` seeds were fetched from *or* all preferred seeds were
62 fetched from. If `<max>` is specified then the process will continue and
63 attempt to sync with `<max>` seeds.
64
65 For reference announcing, the synchronization process will be considered
66 successful if at least `<min>` seeds were pushed to *and* all preferred
67 seeds were pushed to.
68
69 When `--fetch` or `--announce` are specified on their own, this command
70 will only fetch or announce.
71
72 If `--inventory` is specified, the node's inventory is announced to
73 the network. This mode does not take an `<rid>`.
74
75Commands
76
77 status Display the sync status of a repository
78
79Options
80
81 --sort-by <field> Sort the table by column (options: nid, alias, status)
82 -f, --fetch Turn on fetching (default: true)
83 -a, --announce Turn on ref announcing (default: true)
84 -i, --inventory Turn on inventory announcing (default: false)
85 --timeout <secs> How many seconds to wait while syncing
86 --seed <nid> Sync with the given node (may be specified multiple times)
87 -r, --replicas <count> Sync with a specific number of seeds
88 --replicas-max <count> Sync with an upper bound number of seeds
89 -v, --verbose Verbose output
90 --debug Print debug information afer sync
91 --help Print help
92"#,
93};
94
95#[derive(Debug, Clone, PartialEq, Eq, Default)]
96pub enum Operation {
97 Synchronize(SyncMode),
98 #[default]
99 Status,
100}
101
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
103pub enum SortBy {
104 Nid,
105 Alias,
106 #[default]
107 Status,
108}
109
110impl FromStr for SortBy {
111 type Err = &'static str;
112
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
114 match s {
115 "nid" => Ok(Self::Nid),
116 "alias" => Ok(Self::Alias),
117 "status" => Ok(Self::Status),
118 _ => Err("invalid `--sort-by` field"),
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum SyncMode {
125 Repo {
126 settings: SyncSettings,
127 direction: SyncDirection,
128 },
129 Inventory,
130}
131
132impl Default for SyncMode {
133 fn default() -> Self {
134 Self::Repo {
135 settings: SyncSettings::default(),
136 direction: SyncDirection::default(),
137 }
138 }
139}
140
141#[derive(Debug, Default, PartialEq, Eq, Clone)]
142pub enum SyncDirection {
143 Fetch,
144 Announce,
145 #[default]
146 Both,
147}
148
149#[derive(Default, Debug)]
150pub struct Options {
151 pub rid: Option<RepoId>,
152 pub debug: bool,
153 pub verbose: bool,
154 pub sort_by: SortBy,
155 pub op: Operation,
156}
157
158impl Args for Options {
159 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
160 use lexopt::prelude::*;
161
162 let mut parser = lexopt::Parser::from_args(args);
163 let mut verbose = false;
164 let mut timeout = time::Duration::from_secs(9);
165 let mut rid = None;
166 let mut fetch = false;
167 let mut announce = false;
168 let mut inventory = false;
169 let mut debug = false;
170 let mut replicas = None;
171 let mut max_replicas = None;
172 let mut seeds = BTreeSet::new();
173 let mut sort_by = SortBy::default();
174 let mut op: Option<Operation> = None;
175
176 while let Some(arg) = parser.next()? {
177 match arg {
178 Long("debug") => {
179 debug = true;
180 }
181 Long("verbose") | Short('v') => {
182 verbose = true;
183 }
184 Long("fetch") | Short('f') => {
185 fetch = true;
186 }
187 Long("replicas") | Short('r') => {
188 let val = parser.value()?;
189 let count = term::args::number(&val)?;
190
191 if count == 0 {
192 anyhow::bail!("value for `--replicas` must be greater than zero");
193 }
194 replicas = Some(count);
195 }
196 Long("replicas-max") => {
197 let val = parser.value()?;
198 let count = term::args::number(&val)?;
199
200 if count == 0 {
201 anyhow::bail!("value for `--replicas-max` must be greater than zero");
202 }
203 max_replicas = Some(count);
204 }
205 Long("seed") => {
206 let val = parser.value()?;
207 let nid = term::args::nid(&val)?;
208
209 seeds.insert(nid);
210 }
211 Long("announce") | Short('a') => {
212 announce = true;
213 }
214 Long("inventory") | Short('i') => {
215 inventory = true;
216 }
217 Long("sort-by") if matches!(op, Some(Operation::Status)) => {
218 let value = parser.value()?;
219 sort_by = value.parse()?;
220 }
221 Long("timeout") | Short('t') => {
222 let value = parser.value()?;
223 let secs = term::args::parse_value("timeout", value)?;
224
225 timeout = time::Duration::from_secs(secs);
226 }
227 Long("help") | Short('h') => {
228 return Err(Error::Help.into());
229 }
230 Value(val) if rid.is_none() => match val.to_string_lossy().as_ref() {
231 "s" | "status" => {
232 op = Some(Operation::Status);
233 }
234 _ => {
235 rid = Some(term::args::rid(&val)?);
236 }
237 },
238 arg => {
239 return Err(anyhow!(arg.unexpected()));
240 }
241 }
242 }
243
244 let sync = if inventory && fetch {
245 anyhow::bail!("`--inventory` cannot be used with `--fetch`");
246 } else if inventory {
247 SyncMode::Inventory
248 } else {
249 let direction = match (fetch, announce) {
250 (true, true) | (false, false) => SyncDirection::Both,
251 (true, false) => SyncDirection::Fetch,
252 (false, true) => SyncDirection::Announce,
253 };
254 let mut settings = SyncSettings::default().timeout(timeout);
255
256 let replicas = match (replicas, max_replicas) {
257 (None, None) => sync::ReplicationFactor::default(),
258 (None, Some(min)) => sync::ReplicationFactor::must_reach(min),
259 (Some(min), None) => sync::ReplicationFactor::must_reach(min),
260 (Some(min), Some(max)) => sync::ReplicationFactor::range(min, max),
261 };
262 settings.replicas = replicas;
263 if !seeds.is_empty() {
264 settings.seeds = seeds;
265 }
266 SyncMode::Repo {
267 settings,
268 direction,
269 }
270 };
271
272 Ok((
273 Options {
274 rid,
275 debug,
276 verbose,
277 sort_by,
278 op: op.unwrap_or(Operation::Synchronize(sync)),
279 },
280 vec![],
281 ))
282 }
283}
284
285pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
286 let profile = ctx.profile()?;
287 let mut node = radicle::Node::new(profile.socket());
288 if !node.is_running() {
289 anyhow::bail!(
290 "to sync a repository, your node must be running. To start it, run `rad node start`"
291 );
292 }
293
294 match &options.op {
295 Operation::Status => {
296 let rid = match options.rid {
297 Some(rid) => rid,
298 None => {
299 let (_, rid) = radicle::rad::cwd()
300 .context("Current directory is not a Radicle repository")?;
301 rid
302 }
303 };
304 sync_status(rid, &mut node, &profile, &options)?;
305 }
306 Operation::Synchronize(SyncMode::Repo {
307 settings,
308 direction,
309 }) => {
310 let rid = match options.rid {
311 Some(rid) => rid,
312 None => {
313 let (_, rid) = radicle::rad::cwd()
314 .context("Current directory is not a Radicle repository")?;
315 rid
316 }
317 };
318 let settings = settings.clone().with_profile(&profile);
319
320 if [SyncDirection::Fetch, SyncDirection::Both].contains(direction) {
321 if !profile.policies()?.is_seeding(&rid)? {
322 anyhow::bail!("repository {rid} is not seeded");
323 }
324 let result = fetch(rid, settings.clone(), &mut node, &profile)?;
325 display_fetch_result(&result, options.verbose)
326 }
327 if [SyncDirection::Announce, SyncDirection::Both].contains(direction) {
328 announce_refs(rid, settings, &mut node, &profile, &options)?;
329 }
330 }
331 Operation::Synchronize(SyncMode::Inventory) => {
332 announce_inventory(node)?;
333 }
334 }
335 Ok(())
336}
337
338fn sync_status(
339 rid: RepoId,
340 node: &mut Node,
341 profile: &Profile,
342 options: &Options,
343) -> anyhow::Result<()> {
344 const SYMBOL_STATE: &str = "?";
345 const SYMBOL_STATE_UNKNOWN: &str = "•";
346
347 let mut table = Table::<5, term::Label>::new(TableOptions::bordered());
348 let mut seeds: Vec<_> = node.seeds(rid)?.into();
349 let local_nid = node.nid()?;
350 let aliases = profile.aliases();
351
352 table.header([
353 term::format::bold("Node ID").into(),
354 term::format::bold("Alias").into(),
355 term::format::bold(SYMBOL_STATE).into(),
356 term::format::bold("SigRefs").into(),
357 term::format::bold("Timestamp").into(),
358 ]);
359 table.divider();
360
361 sort_seeds_by(local_nid, &mut seeds, &aliases, &options.sort_by);
362
363 for seed in seeds {
364 let (status, head, time) = match seed.sync {
365 Some(SyncStatus::Synced {
366 at: SyncedAt { oid, timestamp },
367 }) => (
368 term::PREFIX_SUCCESS,
369 term::format::oid(oid),
370 term::format::timestamp(timestamp),
371 ),
372 Some(SyncStatus::OutOfSync {
373 remote: SyncedAt { timestamp, .. },
374 local,
375 ..
376 }) if seed.nid == local_nid => (
377 term::PREFIX_WARNING,
378 term::format::oid(local.oid),
379 term::format::timestamp(timestamp),
380 ),
381 Some(SyncStatus::OutOfSync {
382 remote: SyncedAt { oid, timestamp },
383 ..
384 }) => (
385 term::PREFIX_ERROR,
386 term::format::oid(oid),
387 term::format::timestamp(timestamp),
388 ),
389 None if options.verbose => (
390 term::format::dim(SYMBOL_STATE_UNKNOWN),
391 term::paint(String::new()),
392 term::paint(String::new()),
393 ),
394 None => continue,
395 };
396
397 let (alias, nid) = Author::new(&seed.nid, profile, options.verbose).labels();
398
399 table.push([
400 nid,
401 alias,
402 status.into(),
403 term::format::secondary(head).into(),
404 time.dim().italic().into(),
405 ]);
406 }
407 table.print();
408
409 if profile.hints() {
410 const COLUMN_WIDTH: usize = 16;
411 let status = format!(
412 "\n{:>4} … {}\n {} {}\n {} {}",
413 term::Paint::from(SYMBOL_STATE.to_string()).fg(radicle_term::Color::White),
414 term::format::dim("Status:"),
415 format_args!(
416 "{} {:width$}",
417 term::PREFIX_SUCCESS,
418 term::format::dim("… in sync"),
419 width = COLUMN_WIDTH,
420 ),
421 format_args!(
422 "{} {}",
423 term::PREFIX_ERROR,
424 term::format::dim("… out of sync")
425 ),
426 format_args!(
427 "{} {:width$}",
428 term::PREFIX_WARNING,
429 term::format::dim("… not announced"),
430 width = COLUMN_WIDTH,
431 ),
432 format_args!(
433 "{} {}",
434 term::format::dim(SYMBOL_STATE_UNKNOWN),
435 term::format::dim("… unknown")
436 ),
437 );
438 term::hint(status);
439 }
440
441 Ok(())
442}
443
444fn announce_refs(
445 rid: RepoId,
446 settings: SyncSettings,
447 node: &mut Node,
448 profile: &Profile,
449 options: &Options,
450) -> anyhow::Result<()> {
451 let Ok(repo) = profile.storage.repository(rid) else {
452 return Err(anyhow!(
453 "nothing to announce, repository {rid} is not available locally"
454 ));
455 };
456 if let Err(e) = repo.remote(&profile.public_key) {
457 if e.is_not_found() {
458 term::print(term::format::italic(
459 "Nothing to announce, you don't have a fork of this repository.",
460 ));
461 return Ok(());
462 } else {
463 return Err(anyhow!("failed to load local fork of {rid}: {e}"));
464 }
465 }
466
467 let result = crate::node::announce(
468 &repo,
469 settings,
470 SyncReporting {
471 debug: options.debug,
472 ..SyncReporting::default()
473 },
474 node,
475 profile,
476 )?;
477 if let Some(result) = result {
478 print_announcer_result(&result, options.verbose)
479 }
480
481 Ok(())
482}
483
484pub fn announce_inventory(mut node: Node) -> anyhow::Result<()> {
485 let peers = node.sessions()?.iter().filter(|s| s.is_connected()).count();
486 let spinner = term::spinner(format!("Announcing inventory to {peers} peers.."));
487
488 node.announce_inventory()?;
489 spinner.finish();
490
491 Ok(())
492}
493
494#[derive(Debug, thiserror::Error)]
495pub enum FetchError {
496 #[error(transparent)]
497 Node(#[from] node::Error),
498 #[error(transparent)]
499 Db(#[from] node::db::Error),
500 #[error(transparent)]
501 Address(#[from] node::address::Error),
502 #[error(transparent)]
503 Fetcher(#[from] sync::FetcherError),
504}
505
506pub fn fetch(
507 rid: RepoId,
508 settings: SyncSettings,
509 node: &mut Node,
510 profile: &Profile,
511) -> Result<sync::FetcherResult, FetchError> {
512 let db = profile.database()?;
513 let local = profile.id();
514 let is_private = profile.storage.repository(rid).ok().and_then(|repo| {
515 let doc = repo.identity_doc().ok()?.doc;
516 sync::PrivateNetwork::private_repo(&doc)
517 });
518 let config = match is_private {
519 Some(private) => sync::FetcherConfig::private(private, settings.replicas, *local),
520 None => {
521 let seeds = node.seeds(rid)?;
524 let (connected, disconnected) = seeds.partition();
525 let candidates = connected
526 .into_iter()
527 .map(|seed| seed.nid)
528 .chain(disconnected.into_iter().map(|seed| seed.nid))
529 .map(sync::fetch::Candidate::new);
530 sync::FetcherConfig::public(settings.seeds.clone(), settings.replicas, *local)
531 .with_candidates(candidates)
532 }
533 };
534 let mut fetcher = sync::Fetcher::new(config)?;
535
536 let mut progress = fetcher.progress();
537 term::info!(
538 "Fetching {} from the network, found {} potential seed(s).",
539 term::format::tertiary(rid),
540 term::format::tertiary(progress.candidate())
541 );
542 let mut spinner = FetcherSpinner::new(fetcher.target(), &progress);
543
544 while let Some(nid) = fetcher.next_node() {
545 match node.session(nid)? {
546 Some(session) if session.is_connected() => fetcher.ready_to_fetch(nid, session.addr),
547 _ => {
548 let addrs = db.addresses_of(&nid)?;
549 if addrs.is_empty() {
550 fetcher.fetch_failed(nid, "Could not connect. No addresses known.");
551 } else if let Some(addr) = connect(
552 nid,
553 addrs.into_iter().map(|ka| ka.addr),
554 settings.timeout,
555 node,
556 &mut spinner,
557 &fetcher.progress(),
558 ) {
559 fetcher.ready_to_fetch(nid, addr)
560 } else {
561 fetcher
562 .fetch_failed(nid, "Could not connect. At least one address is known but all attempts timed out.");
563 }
564 }
565 }
566 if let Some((nid, addr)) = fetcher.next_fetch() {
567 spinner.emit_fetching(&nid, &addr, &progress);
568 let result = node.fetch(rid, nid, settings.timeout)?;
569 match fetcher.fetch_complete(nid, result) {
570 std::ops::ControlFlow::Continue(update) => {
571 spinner.emit_progress(&update);
572 progress = update
573 }
574 std::ops::ControlFlow::Break(success) => {
575 spinner.finished(success.outcome());
576 return Ok(sync::FetcherResult::TargetReached(success));
577 }
578 }
579 }
580 }
581 let result = fetcher.finish();
582 match &result {
583 sync::FetcherResult::TargetReached(success) => {
584 spinner.finished(success.outcome());
585 }
586 sync::FetcherResult::TargetError(missed) => spinner.failed(missed),
587 }
588 Ok(result)
589}
590
591fn connect(
595 nid: NodeId,
596 addrs: impl Iterator<Item = node::Address>,
597 timeout: time::Duration,
598 node: &mut Node,
599 spinner: &mut FetcherSpinner,
600 progress: &sync::fetch::Progress,
601) -> Option<node::Address> {
602 for addr in addrs {
603 spinner.emit_dialing(&nid, &addr, progress);
604 let cr = node.connect(
605 nid,
606 addr.clone(),
607 node::ConnectOptions {
608 persistent: false,
609 timeout,
610 },
611 );
612
613 match cr {
614 Ok(node::ConnectResult::Connected) => {
615 return Some(addr);
616 }
617 Ok(node::ConnectResult::Disconnected { .. }) => {
618 continue;
619 }
620 Err(e) => {
621 log::warn!(target: "cli", "Failed to connect to {nid}@{addr}: {e}");
622 continue;
623 }
624 }
625 }
626 None
627}
628
629fn sort_seeds_by(local: NodeId, seeds: &mut [Seed], aliases: &impl AliasStore, sort_by: &SortBy) {
630 let compare = |a: &Seed, b: &Seed| match sort_by {
631 SortBy::Nid => a.nid.cmp(&b.nid),
632 SortBy::Alias => {
633 let a = aliases.alias(&a.nid);
634 let b = aliases.alias(&b.nid);
635 a.cmp(&b)
636 }
637 SortBy::Status => match (&a.sync, &b.sync) {
638 (Some(_), None) => Ordering::Less,
639 (None, Some(_)) => Ordering::Greater,
640 (Some(a), Some(b)) => a.cmp(b).reverse(),
641 (None, None) => Ordering::Equal,
642 },
643 };
644
645 seeds.sort_by(|a, b| {
647 if a.nid == local {
648 Ordering::Less
649 } else if b.nid == local {
650 Ordering::Greater
651 } else {
652 compare(a, b)
653 }
654 });
655}
656
657struct FetcherSpinner {
658 preferred_seeds: usize,
659 replicas: sync::ReplicationFactor,
660 spinner: term::Spinner,
661}
662
663impl FetcherSpinner {
664 fn new(target: &sync::fetch::Target, progress: &sync::fetch::Progress) -> Self {
665 let preferred_seeds = target.preferred_seeds().len();
666 let replicas = target.replicas();
667 let spinner = term::spinner(format!(
668 "{} of {} preferred seeds, and {} of at least {} total seeds.",
669 term::format::secondary(progress.preferred()),
670 term::format::secondary(preferred_seeds),
671 term::format::secondary(progress.succeeded()),
672 term::format::secondary(replicas.lower_bound())
673 ));
674 Self {
675 preferred_seeds: target.preferred_seeds().len(),
676 replicas: *target.replicas(),
677 spinner,
678 }
679 }
680
681 fn emit_progress(&mut self, progress: &sync::fetch::Progress) {
682 self.spinner.message(format!(
683 "{} of {} preferred seeds, and {} of at least {} total seeds.",
684 term::format::secondary(progress.preferred()),
685 term::format::secondary(self.preferred_seeds),
686 term::format::secondary(progress.succeeded()),
687 term::format::secondary(self.replicas.lower_bound()),
688 ))
689 }
690
691 fn emit_fetching(
692 &mut self,
693 node: &NodeId,
694 addr: &node::Address,
695 progress: &sync::fetch::Progress,
696 ) {
697 self.spinner.message(format!(
698 "{} of {} preferred seeds, and {} of at least {} total seeds… [fetch {}@{}]",
699 term::format::secondary(progress.preferred()),
700 term::format::secondary(self.preferred_seeds),
701 term::format::secondary(progress.succeeded()),
702 term::format::secondary(self.replicas.lower_bound()),
703 term::format::tertiary(term::format::node_id_human_compact(node)),
704 term::format::tertiary(term::format::addr_compact(addr)),
705 ))
706 }
707
708 fn emit_dialing(
709 &mut self,
710 node: &NodeId,
711 addr: &node::Address,
712 progress: &sync::fetch::Progress,
713 ) {
714 self.spinner.message(format!(
715 "{} of {} preferred seeds, and {} of at least {} total seeds… [dial {}@{}]",
716 term::format::secondary(progress.preferred()),
717 term::format::secondary(self.preferred_seeds),
718 term::format::secondary(progress.succeeded()),
719 term::format::secondary(self.replicas.lower_bound()),
720 term::format::tertiary(term::format::node_id_human_compact(node)),
721 term::format::tertiary(term::format::addr_compact(addr)),
722 ))
723 }
724
725 fn finished(mut self, outcome: &SuccessfulOutcome) {
726 match outcome {
727 SuccessfulOutcome::PreferredNodes { preferred } => {
728 self.spinner.message(format!(
729 "Target met: {} preferred seed(s).",
730 term::format::positive(preferred),
731 ));
732 }
733 SuccessfulOutcome::MinReplicas { succeeded, .. } => {
734 self.spinner.message(format!(
735 "Target met: {} seed(s)",
736 term::format::positive(succeeded)
737 ));
738 }
739 SuccessfulOutcome::MaxReplicas {
740 succeeded,
741 min,
742 max,
743 } => {
744 self.spinner.message(format!(
745 "Target met: {} of {} min and {} max seed(s)",
746 succeeded,
747 term::format::secondary(min),
748 term::format::secondary(max)
749 ));
750 }
751 }
752 self.spinner.finish()
753 }
754
755 fn failed(mut self, missed: &sync::fetch::TargetMissed) {
756 let mut message = "Target not met: ".to_string();
757 let missing_preferred_seeds = missed
758 .missed_nodes()
759 .iter()
760 .map(|nid| term::format::node_id_human(nid).to_string())
761 .collect::<Vec<_>>();
762 let required = missed.required_nodes();
763 if !missing_preferred_seeds.is_empty() {
764 message.push_str(&format!(
765 "could not fetch from [{}], and required {} more seed(s)",
766 missing_preferred_seeds.join(", "),
767 required
768 ));
769 } else {
770 message.push_str(&format!("required {required} more seed(s)"));
771 }
772 self.spinner.message(message);
773 self.spinner.failed();
774 }
775}
776
777fn display_fetch_result(result: &sync::FetcherResult, verbose: bool) {
778 match result {
779 sync::FetcherResult::TargetReached(success) => {
780 let progress = success.progress();
781 let results = success.fetch_results();
782 display_success(results.success(), verbose);
783 let failed = progress.failed();
784 if failed > 0 && verbose {
785 term::warning(format!("Failed to fetch from {failed} seed(s)."));
786 for (node, reason) in results.failed() {
787 term::warning(format!(
788 "{}: {}",
789 term::format::node_id_human(node),
790 term::format::yellow(reason),
791 ))
792 }
793 }
794 }
795 sync::FetcherResult::TargetError(failed) => {
796 let results = failed.fetch_results();
797 let progress = failed.progress();
798 let target = failed.target();
799 let succeeded = progress.succeeded();
800 let missed = failed.missed_nodes();
801 term::error(format!(
802 "Fetched from {} preferred seed(s), could not reach {} seed(s)",
803 succeeded,
804 target.replicas().lower_bound(),
805 ));
806 term::error(format!(
807 "Could not replicate from {} preferred seed(s)",
808 missed.len()
809 ));
810 for (node, reason) in results.failed() {
811 term::error(format!(
812 "{}: {}",
813 term::format::node_id_human(node),
814 term::format::negative(reason),
815 ))
816 }
817 if succeeded > 0 {
818 term::info!("Successfully fetched from the following seeds:");
819 display_success(results.success(), verbose)
820 }
821 }
822 }
823}
824
825fn display_success<'a>(
826 results: impl Iterator<Item = (&'a NodeId, &'a [RefUpdate], HashSet<NodeId>)>,
827 verbose: bool,
828) {
829 for (node, updates, _) in results {
830 term::println(
831 "🌱 Fetched from",
832 term::format::secondary(term::format::node_id_human(node)),
833 );
834 if verbose {
835 let mut updates = updates
836 .iter()
837 .filter(|up| !matches!(up, RefUpdate::Skipped { .. }))
838 .peekable();
839 if updates.peek().is_none() {
840 term::indented(term::format::italic("no references were updated"));
841 } else {
842 for update in updates {
843 term::indented(term::format::ref_update_verbose(update))
844 }
845 }
846 }
847 }
848}
849
850fn print_announcer_result(result: &sync::AnnouncerResult, verbose: bool) {
851 use sync::announce::SuccessfulOutcome::*;
852 match result {
853 sync::AnnouncerResult::Success(success) if verbose => {
854 match success.outcome() {
857 MinReplicationFactor { preferred, synced }
858 | MaxReplicationFactor { preferred, synced }
859 | PreferredNodes {
860 preferred,
861 total_nodes_synced: synced,
862 } => {
863 if preferred == 0 {
864 term::success!("Synced {} seed(s)", term::format::positive(synced));
865 } else {
866 term::success!(
867 "Synced {} preferred seed(s) and {} total seed(s)",
868 term::format::positive(preferred),
869 term::format::positive(synced)
870 );
871 }
872 }
873 }
874 print_synced(success.synced());
875 }
876 sync::AnnouncerResult::Success(_) => {
877 }
879 sync::AnnouncerResult::TimedOut(result) => {
880 if result.synced().is_empty() {
881 term::error("All seeds timed out, use `rad sync -v` to see the list of seeds");
882 return;
883 }
884 let timed_out = result.timed_out();
885 term::warning(format!(
886 "{} seed(s) timed out, use `rad sync -v` to see the list of seeds",
887 timed_out.len(),
888 ));
889 if verbose {
890 print_synced(result.synced());
891 for node in timed_out {
892 term::warning(format!("{} timed out", term::format::node_id_human(node)));
893 }
894 }
895 }
896 sync::AnnouncerResult::NoNodes(result) => {
897 term::info!("Announcement could not sync with anymore seeds.");
898 if verbose {
899 print_synced(result.synced())
900 }
901 }
902 }
903}
904
905fn print_synced(synced: &BTreeMap<NodeId, sync::announce::SyncStatus>) {
906 for (node, status) in synced.iter() {
907 let mut message = format!("🌱 Synced with {}", term::format::node_id_human(node));
908
909 match status {
910 sync::announce::SyncStatus::AlreadySynced => {
911 message.push_str(&format!("{}", term::format::dim(" (already in sync)")));
912 }
913 sync::announce::SyncStatus::Synced { duration } => {
914 message.push_str(&format!(
915 "{}",
916 term::format::dim(format!(" in {}s", duration.as_secs()))
917 ));
918 }
919 }
920 term::info!("{}", message);
921 }
922}