Skip to main content

git_same/commands/
sync_cmd.rs

1//! Sync command handler.
2//!
3//! Combined operation: discover repos -> clone new ones -> fetch/pull existing ones.
4
5use 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
19/// Sync repositories for a workspace.
20pub 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    // Resolve workspace and ensure base path exists (offer to fix if user moved it)
28    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    // Dry-run output
77    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    // Execute shared workflow
111    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    // Update last_synced
167    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;