Skip to main content

cargo_mate/tools/
workspace_sync.rs

1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::path::Path;
5use std::fs;
6use std::collections::HashMap;
7use serde::{Deserialize, Serialize};
8use toml;
9#[derive(Debug, Clone)]
10pub struct WorkspaceSyncTool;
11#[derive(Debug, Deserialize, Serialize)]
12struct WorkspaceConfig {
13    workspace: Workspace,
14}
15#[derive(Debug, Deserialize, Serialize)]
16struct Workspace {
17    members: Vec<String>,
18}
19#[derive(Debug, Deserialize, Serialize)]
20struct CargoToml {
21    package: Option<Package>,
22    dependencies: HashMap<String, Dependency>,
23    #[serde(rename = "dev-dependencies")]
24    dev_dependencies: Option<HashMap<String, Dependency>>,
25}
26#[derive(Debug, Deserialize, Serialize)]
27struct Package {
28    name: String,
29    version: String,
30}
31#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(untagged)]
33enum Dependency {
34    Simple(String),
35    Detailed(DependencyDetail),
36}
37#[derive(Debug, Clone, Deserialize, Serialize)]
38struct DependencyDetail {
39    version: Option<String>,
40    path: Option<String>,
41    git: Option<String>,
42    branch: Option<String>,
43    features: Option<Vec<String>>,
44}
45#[derive(Debug, Clone, serde::Serialize)]
46struct DependencyAnalysis {
47    name: String,
48    versions: HashMap<String, String>,
49    conflicts: Vec<String>,
50}
51impl WorkspaceSyncTool {
52    pub fn new() -> Self {
53        Self
54    }
55    fn find_workspace_root(&self) -> Result<String> {
56        let mut current = std::env::current_dir()
57            .map_err(|e| ToolError::ExecutionFailed(
58                format!("Cannot get current directory: {}", e),
59            ))?;
60        loop {
61            let cargo_toml = current.join("Cargo.toml");
62            if cargo_toml.exists() {
63                let content = fs::read_to_string(&cargo_toml)
64                    .map_err(|e| ToolError::ExecutionFailed(
65                        format!("Cannot read Cargo.toml: {}", e),
66                    ))?;
67                if content.contains("[workspace]") {
68                    return Ok(current.to_string_lossy().to_string());
69                }
70            }
71            if let Some(parent) = current.parent() {
72                current = parent.to_path_buf();
73            } else {
74                break;
75            }
76        }
77        Err(ToolError::ExecutionFailed("Not in a workspace".to_string()))
78    }
79    fn parse_cargo_toml(&self, path: &Path) -> Result<CargoToml> {
80        let content = fs::read_to_string(path)
81            .map_err(|e| ToolError::ExecutionFailed(
82                format!("Cannot read {}: {}", path.display(), e),
83            ))?;
84        toml::from_str(&content)
85            .map_err(|e| ToolError::ExecutionFailed(
86                format!("Cannot parse {}: {}", path.display(), e),
87            ))
88    }
89    fn analyze_workspace_dependencies(
90        &self,
91        workspace_root: &str,
92    ) -> Result<HashMap<String, DependencyAnalysis>> {
93        let workspace_config_path = Path::new(workspace_root).join("Cargo.toml");
94        let workspace_config: WorkspaceConfig = {
95            let content = fs::read_to_string(&workspace_config_path)?;
96            toml::from_str(&content)?
97        };
98        let mut analyses: HashMap<String, DependencyAnalysis> = HashMap::new();
99        if let Ok(root_cargo) = self.parse_cargo_toml(&workspace_config_path) {
100            self.analyze_dependencies(
101                &mut analyses,
102                &root_cargo.dependencies,
103                "workspace-root",
104            );
105            if let Some(dev_deps) = &root_cargo.dev_dependencies {
106                self.analyze_dependencies(&mut analyses, dev_deps, "workspace-root-dev");
107            }
108        }
109        for member in &workspace_config.workspace.members {
110            let member_path = Path::new(workspace_root).join(member).join("Cargo.toml");
111            if let Ok(member_cargo) = self.parse_cargo_toml(&member_path) {
112                self.analyze_dependencies(
113                    &mut analyses,
114                    &member_cargo.dependencies,
115                    &member,
116                );
117                if let Some(dev_deps) = &member_cargo.dev_dependencies {
118                    self.analyze_dependencies(
119                        &mut analyses,
120                        dev_deps,
121                        &format!("{}-dev", member),
122                    );
123                }
124            }
125        }
126        Ok(analyses)
127    }
128    fn analyze_dependencies(
129        &self,
130        analyses: &mut HashMap<String, DependencyAnalysis>,
131        deps: &HashMap<String, Dependency>,
132        source: &str,
133    ) {
134        for (name, dep) in deps {
135            if let Some(analysis) = analyses.get_mut(name) {
136                match dep {
137                    Dependency::Simple(version) => {
138                        analysis.versions.insert(source.to_string(), version.clone());
139                    }
140                    Dependency::Detailed(detail) => {
141                        if let Some(version) = &detail.version {
142                            analysis
143                                .versions
144                                .insert(source.to_string(), version.clone());
145                        }
146                    }
147                }
148            } else {
149                let mut versions = HashMap::new();
150                match dep {
151                    Dependency::Simple(version) => {
152                        versions.insert(source.to_string(), version.clone());
153                    }
154                    Dependency::Detailed(detail) => {
155                        if let Some(version) = &detail.version {
156                            versions.insert(source.to_string(), version.clone());
157                        }
158                    }
159                }
160                analyses
161                    .insert(
162                        name.clone(),
163                        DependencyAnalysis {
164                            name: name.clone(),
165                            versions,
166                            conflicts: Vec::new(),
167                        },
168                    );
169            }
170        }
171    }
172    fn detect_conflicts(&self, analyses: &mut HashMap<String, DependencyAnalysis>) {
173        for analysis in analyses.values_mut() {
174            if analysis.versions.len() > 1 {
175                let versions: Vec<&String> = analysis.versions.values().collect();
176                let first_version = versions[0];
177                for version in &versions[1..] {
178                    if *version != first_version {
179                        analysis
180                            .conflicts
181                            .push(
182                                format!(
183                                    "Version mismatch: {} vs {}", first_version, version
184                                ),
185                            );
186                    }
187                }
188            }
189        }
190    }
191    fn sync_dependencies(&self, workspace_root: &str, dry_run: bool) -> Result<()> {
192        let analyses = self.analyze_workspace_dependencies(workspace_root)?;
193        let mut analyses = analyses;
194        self.detect_conflicts(&mut analyses);
195        if dry_run {
196            self.display_sync_plan(&analyses);
197        } else {
198            self.apply_sync_plan(&analyses, workspace_root)?;
199        }
200        Ok(())
201    }
202    fn display_sync_plan(&self, analyses: &HashMap<String, DependencyAnalysis>) {
203        println!("{}", "📋 Workspace Dependency Sync Plan".bold().blue());
204        println!("{}", "═".repeat(50).blue());
205        let conflicts: Vec<_> = analyses
206            .values()
207            .filter(|a| !a.conflicts.is_empty())
208            .collect();
209        let synced: Vec<_> = analyses
210            .values()
211            .filter(|a| a.conflicts.is_empty() && a.versions.len() > 1)
212            .collect();
213        let unique: Vec<_> = analyses
214            .values()
215            .filter(|a| a.versions.len() == 1)
216            .collect();
217        if !conflicts.is_empty() {
218            println!("\n{}", "🔴 Conflicts Found:".red().bold());
219            for analysis in &conflicts {
220                println!(
221                    "  {} - {} versions found", analysis.name.red(), analysis.versions
222                    .len()
223                );
224                for conflict in &analysis.conflicts {
225                    println!("    {}", conflict.yellow());
226                }
227            }
228        }
229        if !synced.is_empty() {
230            println!("\n{}", "✅ Already Synced:".green().bold());
231            for analysis in &synced {
232                if let Some(version) = analysis.versions.values().next() {
233                    println!(
234                        "  {} - {} crates", analysis.name.green(), analysis.versions
235                        .len()
236                    );
237                }
238            }
239        }
240        if !unique.is_empty() {
241            println!("\n{}", "📦 Unique Dependencies:".cyan().bold());
242            for analysis in &unique {
243                if let Some(version) = analysis.versions.values().next() {
244                    println!("  {} - {}", analysis.name.cyan(), version);
245                }
246            }
247        }
248    }
249    fn apply_sync_plan(
250        &self,
251        analyses: &HashMap<String, DependencyAnalysis>,
252        workspace_root: &str,
253    ) -> Result<()> {
254        println!("{}", "🔄 Applying Dependency Synchronization".bold().yellow());
255        self.display_sync_plan(analyses);
256        println!("\n{}", "⚠️  Actual synchronization not yet implemented".yellow());
257        println!("   This would modify Cargo.toml files to ensure version consistency");
258        Ok(())
259    }
260    fn generate_report(
261        &self,
262        analyses: &HashMap<String, DependencyAnalysis>,
263        format: OutputFormat,
264    ) -> Result<()> {
265        match format {
266            OutputFormat::Json => {
267                let report = serde_json::json!(
268                    { "workspace_analysis" : analyses, "summary" : { "total_dependencies"
269                    : analyses.len(), "conflicts" : analyses.values().filter(| a | ! a
270                    .conflicts.is_empty()).count(), "synced" : analyses.values().filter(|
271                    a | a.conflicts.is_empty() && a.versions.len() > 1).count(), "unique"
272                    : analyses.values().filter(| a | a.versions.len() == 1).count(), } }
273                );
274                println!("{}", serde_json::to_string_pretty(& report).unwrap());
275            }
276            OutputFormat::Table => {
277                println!(
278                    "{:<30} {:<15} {:<10} {:<50}", "Dependency", "Versions", "Status",
279                    "Details"
280                );
281                println!("{}", "─".repeat(105));
282                for analysis in analyses.values() {
283                    let status = if !analysis.conflicts.is_empty() {
284                        "CONFLICT".red().to_string()
285                    } else if analysis.versions.len() > 1 {
286                        "SYNCED".green().to_string()
287                    } else {
288                        "UNIQUE".cyan().to_string()
289                    };
290                    let details = if !analysis.conflicts.is_empty() {
291                        analysis.conflicts.join(", ")
292                    } else {
293                        format!("Used in {} crates", analysis.versions.len())
294                    };
295                    println!(
296                        "{:<30} {:<15} {:<10} {:<50}", analysis.name, analysis.versions
297                        .len().to_string(), status, details.chars().take(47).collect::<
298                        String > ()
299                    );
300                }
301            }
302            OutputFormat::Human => {
303                self.display_sync_plan(analyses);
304            }
305        }
306        Ok(())
307    }
308}
309impl Tool for WorkspaceSyncTool {
310    fn name(&self) -> &'static str {
311        "workspace-sync"
312    }
313    fn description(&self) -> &'static str {
314        "Keep workspace dependencies in sync and manage version bumps"
315    }
316    fn command(&self) -> Command {
317        Command::new(self.name())
318            .about(self.description())
319            .long_about(
320                "Analyze and synchronize dependencies across workspace members to ensure version consistency and detect conflicts",
321            )
322            .args(
323                &[
324                    Arg::new("sync-versions")
325                        .long("sync-versions")
326                        .help("Synchronize dependency versions across workspace")
327                        .action(clap::ArgAction::SetTrue),
328                    Arg::new("check-conflicts")
329                        .long("check-conflicts")
330                        .help("Check for version conflicts between workspace members")
331                        .action(clap::ArgAction::SetTrue),
332                    Arg::new("bump-minor")
333                        .long("bump-minor")
334                        .help("Bump minor versions for all workspace dependencies")
335                        .action(clap::ArgAction::SetTrue),
336                    Arg::new("bump-major")
337                        .long("bump-major")
338                        .help("Bump major versions for all workspace dependencies")
339                        .action(clap::ArgAction::SetTrue),
340                    Arg::new("bump-patch")
341                        .long("bump-patch")
342                        .help("Bump patch versions for all workspace dependencies")
343                        .action(clap::ArgAction::SetTrue),
344                    Arg::new("report")
345                        .long("report")
346                        .help("Generate workspace dependency report")
347                        .action(clap::ArgAction::SetTrue),
348                ],
349            )
350            .args(&common_options())
351    }
352    fn execute(&self, matches: &ArgMatches) -> Result<()> {
353        let sync_versions = matches.get_flag("sync-versions");
354        let check_conflicts = matches.get_flag("check-conflicts");
355        let bump_minor = matches.get_flag("bump-minor");
356        let bump_major = matches.get_flag("bump-major");
357        let bump_patch = matches.get_flag("bump-patch");
358        let report = matches.get_flag("report");
359        let dry_run = matches.get_flag("dry-run");
360        let output_format = parse_output_format(matches);
361        let verbose = matches.get_flag("verbose");
362        if verbose {
363            println!(
364                "🔍 {} - Analyzing workspace dependencies", "CargoMate WorkspaceSync"
365                .bold().blue()
366            );
367        }
368        let workspace_root = self.find_workspace_root()?;
369        if verbose {
370            println!("📁 Workspace root: {}", workspace_root.cyan());
371        }
372        let mut analyses = self.analyze_workspace_dependencies(&workspace_root)?;
373        self.detect_conflicts(&mut analyses);
374        if report {
375            self.generate_report(&analyses, output_format)?;
376        } else if check_conflicts {
377            let conflicts: Vec<_> = analyses
378                .values()
379                .filter(|a| !a.conflicts.is_empty())
380                .collect();
381            if conflicts.is_empty() {
382                println!("✅ No dependency conflicts found!");
383            } else {
384                println!("🔴 Found {} dependency conflicts:", conflicts.len());
385                for analysis in &conflicts {
386                    println!(
387                        "  {}: {}", analysis.name.red(), analysis.conflicts.join(", ")
388                    );
389                }
390            }
391        } else if sync_versions {
392            self.sync_dependencies(&workspace_root, dry_run)?;
393        } else if bump_minor || bump_major || bump_patch {
394            println!("🔄 Version bumping not yet implemented");
395            println!(
396                "   This would bump versions across all workspace Cargo.toml files"
397            );
398        } else {
399            self.generate_report(&analyses, output_format)?;
400        }
401        Ok(())
402    }
403}
404impl Default for WorkspaceSyncTool {
405    fn default() -> Self {
406        Self::new()
407    }
408}