1use anyhow::Result;
7use regex::Regex;
8use std::collections::{HashMap, HashSet};
9use std::fs;
10use std::path::PathBuf;
11
12use crate::get_cargo_metadata;
13use crate::get_rust_paths;
14
15pub fn bump_versions_command(packages: Vec<String>, all: bool) -> Result<()> {
16 let target_crates = if all {
18 get_all_workspace_crates()?
19 } else {
20 packages
21 };
22
23 if target_crates.is_empty() {
24 log::info!("No crates to bump.");
25 return Ok(());
26 }
27
28 log::info!("Analyzing workspace dependencies...");
29
30 let metadata = get_cargo_metadata()?;
32
33 let workspace_member_ids: HashSet<String> = metadata
35 .workspace_members
36 .iter()
37 .map(|id| id.to_string())
38 .collect();
39
40 let mut workspace_members = HashSet::new();
41 let mut crate_paths = HashMap::new();
42
43 for pkg in &metadata.packages {
44 if workspace_member_ids.contains(&pkg.id.to_string()) {
45 workspace_members.insert(pkg.name.to_string());
46 crate_paths.insert(
47 pkg.name.to_string(),
48 pkg.manifest_path.as_std_path().to_path_buf(),
49 );
50 }
51 }
52
53 for crate_name in &target_crates {
55 if !workspace_members.contains(crate_name) {
56 return Err(anyhow::anyhow!(
57 "Crate '{}' not found in workspace",
58 crate_name
59 ));
60 }
61 }
62
63 let mut crates_to_bump = HashSet::new();
65 let mut version_updates = HashMap::new();
66
67 for target in &target_crates {
69 crates_to_bump.insert(target.clone());
70 }
71
72 for target_crate in &target_crates {
74 for pkg in &metadata.packages {
76 let pkg_name = pkg.name.as_str();
77
78 if pkg_name == target_crate && workspace_members.contains(pkg_name) {
79 for dep in &pkg.dependencies {
81 let dep_name = dep.name.as_str();
82 let is_workspace_dep = dep.source.is_none(); if is_workspace_dep
86 && workspace_members.contains(dep_name)
87 && !matches!(dep.kind, cargo_metadata::DependencyKind::Development)
88 {
89 crates_to_bump.insert(dep_name.to_string());
90 }
91 }
92 break;
93 }
94 }
95 }
96
97 let sorted_crates: Vec<String> = crates_to_bump.iter().cloned().collect();
98 log::info!("Bumping versions for: {}", sorted_crates.join(", "));
99
100 let target_set: HashSet<String> = target_crates.iter().cloned().collect();
102 let deps: Vec<String> = crates_to_bump.difference(&target_set).cloned().collect();
103 if !deps.is_empty() {
104 log::info!("Found dependencies: {}", deps.join(", "));
105 }
106
107 for crate_name in &crates_to_bump {
109 if let Some(crate_path) = crate_paths.get(crate_name) {
110 let (old_version, new_version) = bump_crate_version(crate_path)?;
111 version_updates.insert(crate_name.clone(), new_version.clone());
112 log::info!("Bumping {crate_name}: {old_version} → {new_version}");
113 }
114 }
115
116 update_dependent_versions(&version_updates)?;
118
119 log::info!("\nUpdated {} crates successfully.", crates_to_bump.len());
120 Ok(())
121}
122
123pub fn get_all_workspace_crates() -> Result<Vec<String>> {
124 let metadata = get_cargo_metadata()?;
125 let mut crates = Vec::new();
126
127 let workspace_member_ids: HashSet<String> = metadata
128 .workspace_members
129 .iter()
130 .map(|id| id.to_string())
131 .collect();
132
133 for pkg in &metadata.packages {
134 if workspace_member_ids.contains(&pkg.id.to_string()) {
135 crates.push(pkg.name.to_string());
136 }
137 }
138
139 Ok(crates)
140}
141
142pub fn bump_crate_version(crate_path: &PathBuf) -> Result<(String, String)> {
143 let content = fs::read_to_string(crate_path)?;
144 let lines: Vec<&str> = content.lines().collect();
145
146 let version_re = Regex::new(r#"(^\s*version\s*=\s*")([^"]*)(".*$)"#)?;
147
148 for (line_no, line) in lines.iter().enumerate() {
149 if let Some(captures) = version_re.captures(line) {
150 let current_version = captures.get(2).unwrap().as_str();
151 let new_version = increment_patch_version(current_version)?;
152
153 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
155 new_lines[line_no] = format!(
156 "{}{}{}",
157 captures.get(1).unwrap().as_str(),
158 new_version,
159 captures.get(3).unwrap().as_str()
160 );
161
162 let new_content = new_lines.join("\n") + "\n";
163 fs::write(crate_path, new_content)?;
164
165 return Ok((current_version.to_string(), new_version));
166 }
167 }
168
169 Err(anyhow::anyhow!(
170 "Could not find version in {:?}",
171 crate_path
172 ))
173}
174
175fn increment_patch_version(version: &str) -> Result<String> {
176 let parts: Vec<&str> = version.split('.').collect();
177 if parts.len() >= 3 {
178 let major = parts[0];
179 let minor = parts[1];
180 let patch: u32 = parts[2]
181 .parse()
182 .map_err(|_| anyhow::anyhow!("Invalid patch version: {}", parts[2]))?;
183 let new_patch = patch + 1;
184
185 if parts.len() > 3 {
187 let extra: Vec<&str> = parts[3..].to_vec();
188 Ok(format!(
189 "{}.{}.{}.{}",
190 major,
191 minor,
192 new_patch,
193 extra.join(".")
194 ))
195 } else {
196 Ok(format!("{major}.{minor}.{new_patch}"))
197 }
198 } else {
199 Err(anyhow::anyhow!("Invalid version format: {}", version))
200 }
201}
202
203pub fn update_dependent_versions(updates: &HashMap<String, String>) -> Result<()> {
204 let rust_paths = get_rust_paths()?;
205 let section_re = Regex::new(r"^\s*\[([^\[\]]*)\]\s*$")?;
206
207 for path in rust_paths {
208 let content = fs::read_to_string(&path)?;
209 let lines: Vec<&str> = content.lines().collect();
210 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
211 let mut modified = false;
212
213 let mut in_dep_section = false;
214 let mut block_depth = 0;
215
216 for (line_no, line) in lines.iter().enumerate() {
217 if let Some(captures) = section_re.captures(line) {
219 if block_depth != 0 {
220 continue;
221 }
222 let section = captures.get(1).unwrap().as_str().trim();
223 in_dep_section = section == "dependencies"
225 || section == "build-dependencies"
226 || section == "dev-dependencies";
227 continue;
228 }
229
230 if !in_dep_section {
231 continue;
232 }
233
234 block_depth += line.matches('{').count() as i32 - line.matches('}').count() as i32;
236 block_depth += line.matches('[').count() as i32 - line.matches(']').count() as i32;
237
238 if block_depth == 0 {
239 for (crate_name, new_version) in updates {
241 let pattern = format!(
242 r#"(^\s*{}\s*=.*version\s*=\s*")([^"]*)(".*$)"#,
243 regex::escape(crate_name)
244 );
245 if let Some(captures) = Regex::new(&pattern)?.captures(line) {
246 new_lines[line_no] = format!(
247 "{}{}{}",
248 captures.get(1).unwrap().as_str(),
249 new_version,
250 captures.get(3).unwrap().as_str()
251 );
252 modified = true;
253 break;
254 }
255 }
256 }
257 }
258
259 if modified {
260 let new_content = new_lines.join("\n") + "\n";
261 fs::write(&path, new_content)?;
262 }
263 }
264
265 Ok(())
266}