git_same/commands/
sync_cmd.rs1use super::warn_if_concurrency_capped;
6use crate::cli::SyncCmdArgs;
7use crate::config::{Config, WorkspaceManager};
8use crate::errors::Result;
9use crate::operations::clone::CloneProgress;
10use crate::operations::sync::{SyncMode, SyncProgress};
11use crate::output::{
12 format_count, CloneProgressBar, DiscoveryProgressBar, Output, SyncProgressBar, Verbosity,
13};
14use crate::workflows::sync_workspace::{
15 execute_prepared_sync, prepare_sync_workspace, SyncWorkspaceRequest,
16};
17use std::sync::Arc;
18
19pub async fn run(args: &SyncCmdArgs, config: &Config, output: &Output) -> Result<()> {
21 let verbosity = if output.is_json() {
22 Verbosity::Quiet
23 } else {
24 output.verbosity()
25 };
26
27 let mut workspace = WorkspaceManager::resolve(args.workspace.as_deref(), config)?;
29 super::ensure_base_path(&workspace, output)?;
30
31 output.info("Discovering repositories...");
32 let discovery_progress = DiscoveryProgressBar::new(verbosity);
33 let prepared = prepare_sync_workspace(
34 SyncWorkspaceRequest {
35 config,
36 workspace: &workspace,
37 refresh: args.refresh,
38 skip_uncommitted: !args.no_skip_uncommitted,
39 pull: args.pull,
40 concurrency_override: args.concurrency,
41 create_base_path: false,
42 },
43 &discovery_progress,
44 )
45 .await?;
46 discovery_progress.finish();
47
48 output.verbose(&format!(
49 "Authenticated as {:?} via {}",
50 prepared.auth.username, prepared.auth.method
51 ));
52
53 if prepared.used_cache {
54 if let Some(age_secs) = prepared.cache_age_secs {
55 output.verbose(&format!(
56 "Using cached discovery ({} repos, {} seconds old)",
57 prepared.repos.len(),
58 age_secs
59 ));
60 }
61 }
62
63 if prepared.repos.is_empty() {
64 output.warn("No repositories found matching filters");
65 return Ok(());
66 }
67
68 output.info(&format_count(
69 prepared.repos.len(),
70 "repositories discovered",
71 ));
72
73 let effective_concurrency = warn_if_concurrency_capped(prepared.requested_concurrency, output);
74 debug_assert_eq!(effective_concurrency, prepared.effective_concurrency);
75
76 let had_clones = !prepared.plan.to_clone.is_empty();
78 if args.dry_run {
79 if had_clones {
80 output.info(&format!(
81 "Would clone {} new repositories:",
82 prepared.plan.to_clone.len()
83 ));
84 for repo in &prepared.plan.to_clone {
85 output.info(&format!(" + {}", repo.full_name()));
86 }
87 }
88
89 if !prepared.to_sync.is_empty() {
90 let op = if prepared.sync_mode == SyncMode::Pull {
91 "pull"
92 } else {
93 "fetch"
94 };
95 output.info(&format!(
96 "Would {} {} existing repositories:",
97 op,
98 prepared.to_sync.len()
99 ));
100 for repo in &prepared.to_sync {
101 output.info(&format!(" ~ {}", repo.repo.full_name()));
102 }
103 } else if !had_clones {
104 output.success("All repositories are up to date");
105 }
106
107 return Ok(());
108 }
109
110 let clone_progress = Arc::new(CloneProgressBar::new(
112 prepared.plan.to_clone.len(),
113 verbosity,
114 ));
115 let clone_progress_dyn: Arc<dyn CloneProgress> = clone_progress.clone();
116
117 let operation = if prepared.sync_mode == SyncMode::Pull {
118 "Pull"
119 } else {
120 "Fetch"
121 };
122 let sync_progress = Arc::new(SyncProgressBar::new(
123 prepared.to_sync.len(),
124 verbosity,
125 operation,
126 ));
127 let sync_progress_dyn: Arc<dyn SyncProgress> = sync_progress.clone();
128
129 let outcome =
130 execute_prepared_sync(&prepared, false, clone_progress_dyn, sync_progress_dyn).await;
131
132 if let Some(summary) = &outcome.clone_summary {
133 clone_progress.finish(summary.success, summary.failed, summary.skipped);
134 if summary.has_failures() {
135 output.warn(&format!("{} repositories failed to clone", summary.failed));
136 } else if summary.success > 0 {
137 output.success(&format!("Cloned {} new repositories", summary.success));
138 }
139 }
140
141 if let Some(summary) = &outcome.sync_summary {
142 sync_progress.finish(summary.success, summary.failed, summary.skipped);
143
144 let with_updates = outcome
145 .sync_results
146 .iter()
147 .filter(|r| r.had_updates)
148 .count();
149 if summary.has_failures() {
150 output.warn(&format!(
151 "{} of {} repositories failed to {}",
152 summary.failed,
153 summary.total(),
154 operation.to_lowercase()
155 ));
156 } else {
157 output.success(&format!(
158 "{}ed {} repositories ({} with updates)",
159 operation, summary.success, with_updates
160 ));
161 }
162 } else if !had_clones {
163 output.success("All repositories are up to date");
164 }
165
166 workspace.last_synced = Some(chrono::Utc::now().to_rfc3339());
168 if let Err(e) = WorkspaceManager::save(&workspace) {
169 output.verbose(&format!("Warning: Failed to update last_synced: {}", e));
170 }
171
172 Ok(())
173}
174
175#[cfg(test)]
176#[path = "sync_cmd_tests.rs"]
177mod tests;