1use super::*;
2
3pub(crate) fn set_item_string_preserving_decor(slot: &mut Item, new_string: &str) {
12 let mut replacement: Item = value(new_string);
13 if let Some(old_value) = slot.as_value()
14 && let Some(new_value) = replacement.as_value_mut()
15 {
16 *new_value.decor_mut() = old_value.decor().clone();
17 }
18 *slot = replacement;
19}
20
21fn read_root_version(doc: &DocumentMut) -> Option<String> {
34 let workspace_version: Option<&str> = doc
35 .get("workspace")
36 .and_then(|workspace: &Item| workspace.get("package"))
37 .and_then(|package: &Item| package.get("version"))
38 .and_then(|version_item: &Item| version_item.as_str());
39 let package_version: Option<&str> = doc
40 .get("package")
41 .and_then(|package: &Item| package.get("version"))
42 .and_then(|version_item: &Item| version_item.as_str());
43 workspace_version
44 .or(package_version)
45 .map(|version: &str| version.to_string())
46}
47
48fn read_member_version(
60 member_doc: &DocumentMut,
61 workspace_version: Option<&str>,
62) -> Result<String, SyncError> {
63 let version_item: &Item = member_doc
64 .get("package")
65 .and_then(|package: &Item| package.get("version"))
66 .ok_or_else(|| SyncError::MemberNameMissing("Cargo.toml".to_string()))?;
67 if let Some(version) = version_item.as_str() {
68 return Ok(version.to_string());
69 }
70 let inherits: bool = version_item
71 .get("workspace")
72 .and_then(|workspace_item: &Item| workspace_item.as_bool())
73 .unwrap_or(false);
74 if inherits && let Some(version) = workspace_version {
75 return Ok(version.to_string());
76 }
77 Err(SyncError::WorkspaceVersionMissing("Cargo.toml".to_string()))
78}
79
80fn read_workspace_members(doc: &DocumentMut) -> Result<Vec<String>, SyncError> {
90 let members: Vec<String> = doc
91 .get("workspace")
92 .and_then(|workspace: &Item| workspace.get("members"))
93 .and_then(|members_item: &Item| members_item.as_array())
94 .ok_or_else(|| SyncError::WorkspaceMembersMissing("Cargo.toml".to_string()))?
95 .iter()
96 .filter_map(|member: &TomlEditValue| member.as_str().map(|s: &str| s.to_string()))
97 .collect();
98 Ok(members)
99}
100
101fn read_member_crate_name(doc: &DocumentMut) -> Result<String, SyncError> {
111 let name: String = doc
112 .get("package")
113 .and_then(|package: &Item| package.get("name"))
114 .and_then(|name_item: &Item| name_item.as_str())
115 .ok_or_else(|| SyncError::MemberNameMissing("Cargo.toml".to_string()))?
116 .to_string();
117 Ok(name)
118}
119
120fn find_dep_alias_for_member_path(deps: &dyn TableLike, member_path: &str) -> Option<String> {
132 for (alias, entry) in deps.iter() {
133 if let Some(path) = entry
134 .get("path")
135 .and_then(|path_item: &Item| path_item.as_str())
136 && path == member_path
137 {
138 return Some(alias.to_string());
139 }
140 }
141 None
142}
143
144fn scan_dep_entry(doc: &DocumentMut, member_path: &str) -> Option<(String, Option<String>)> {
157 let deps: &dyn TableLike = doc
158 .get("workspace")
159 .and_then(|workspace: &Item| workspace.get("dependencies"))
160 .and_then(|deps_item: &Item| deps_item.as_table_like())?;
161 let current_alias: String = find_dep_alias_for_member_path(deps, member_path)?;
162 let existing_version: Option<String> = deps
163 .get(¤t_alias)
164 .and_then(|entry: &Item| entry.get("version"))
165 .and_then(|version_item: &Item| version_item.as_str())
166 .map(|version: &str| version.to_string());
167 Some((current_alias, existing_version))
168}
169
170fn rewrite_entry_version(deps: &mut dyn TableLike, current_alias: &str, workspace_version: &str) {
179 let Some(entry) = deps.get_mut(current_alias) else {
180 return;
181 };
182 match entry.get_mut("version") {
183 Some(version_slot) => set_item_string_preserving_decor(version_slot, workspace_version),
184 None => {
185 if let Some(entry_table) = entry.as_table_like_mut() {
186 entry_table.insert("version", value(workspace_version));
187 }
188 }
189 }
190}
191
192pub async fn execute_sync(manifest_path: &str) -> Result<SyncReport, SyncError> {
229 let path: &Path = Path::new(manifest_path);
230 let content: String = read_to_string(path).await?;
231 let mut doc: DocumentMut = content.parse().map_err(|_| SyncError::ManifestParseError)?;
232 let workspace_version: Option<String> = read_root_version(&doc);
233 let members: Vec<String> = read_workspace_members(&doc)?;
234 if members.is_empty() {
235 log::info!("sync: no workspace members, nothing to do");
236 return Ok(SyncReport {
237 workspace_version: workspace_version.unwrap_or_default(),
238 renamed_entries: Vec::new(),
239 versioned_entries: Vec::new(),
240 file_changed: false,
241 });
242 }
243 let mut renamed_entries: Vec<(String, String)> = Vec::new();
244 let mut versioned_entries: Vec<(String, String)> = Vec::new();
245 let mut needs_rewrite: bool = false;
246 let mut member_paths: Vec<String> = Vec::new();
250 if doc.get("package").is_some() {
251 member_paths.push(".".to_string());
252 }
253 member_paths.extend(members.iter().cloned());
254 for member_path in &member_paths {
255 let member_manifest_path: PathBuf = path
256 .parent()
257 .unwrap_or_else(|| Path::new("."))
258 .join(member_path)
259 .join("Cargo.toml");
260 if !member_manifest_path.exists() {
261 return Err(SyncError::MemberManifestMissing(
262 member_manifest_path.display().to_string(),
263 ));
264 }
265 let member_content: String = read_to_string(&member_manifest_path).await?;
266 let member_doc: DocumentMut = member_content
267 .parse()
268 .map_err(|_| SyncError::ManifestParseError)?;
269 let canonical_alias: String = read_member_crate_name(&member_doc)?;
270 let target_version: String = match &workspace_version {
274 Some(version) => version.clone(),
275 None => read_member_version(&member_doc, None)?,
276 };
277 let (current_alias, existing_version): (String, Option<String>) =
278 match scan_dep_entry(&doc, member_path) {
279 Some(scanned) => scanned,
280 None => {
281 log::info!(
282 "sync: {} -> no [workspace.dependencies] entry, skipping",
283 member_path
284 );
285 continue;
286 }
287 };
288 let alias_needs_rename: bool = current_alias != canonical_alias;
289 let version_needs_rewrite: bool =
290 existing_version.as_deref() != Some(target_version.as_str());
291 if !alias_needs_rename && !version_needs_rewrite {
292 continue;
293 }
294 needs_rewrite = true;
295 let deps: &mut dyn TableLike = doc
296 .get_mut("workspace")
297 .and_then(|workspace: &mut Item| workspace.get_mut("dependencies"))
298 .and_then(|deps_item: &mut Item| deps_item.as_table_like_mut())
299 .ok_or(SyncError::ManifestParseError)?;
300 rewrite_entry_version(deps, ¤t_alias, &target_version);
301 if alias_needs_rename {
302 if let Some(entry) = deps.remove(¤t_alias) {
303 deps.insert(&canonical_alias, entry);
304 }
305 log::info!(
306 "sync: {} renamed {} -> {}",
307 member_path,
308 current_alias,
309 canonical_alias
310 );
311 renamed_entries.push((current_alias, canonical_alias.clone()));
312 } else {
313 log::info!(
314 "sync: {} -> {} v{}",
315 member_path,
316 canonical_alias,
317 target_version
318 );
319 }
320 versioned_entries.push((member_path.clone(), canonical_alias));
321 }
322 let file_changed: bool = needs_rewrite;
323 let report_version: String = workspace_version.unwrap_or_else(|| "per-member".to_string());
324 if file_changed {
325 write(path, doc.to_string()).await?;
326 log::info!(
327 "sync: wrote {} entries to v{}",
328 versioned_entries.len(),
329 report_version
330 );
331 } else {
332 log::info!("sync: already in sync: v{}", report_version);
333 }
334 Ok(SyncReport {
335 workspace_version: report_version,
336 renamed_entries,
337 versioned_entries,
338 file_changed,
339 })
340}