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}