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