use std::path::Path;
use standard_version::{CargoVersionFile, UpdateResult, VersionFile};
use super::{Ecosystem, SyncOutcome, WriteOutcome, native_write, try_sync};
use crate::ui;
pub struct Rust;
fn workspace_native_write(root: &Path, new_version: &str) -> WriteOutcome {
let root_cargo = root.join("Cargo.toml");
let root_content = match std::fs::read_to_string(&root_cargo) {
Ok(c) => c,
Err(_) => return WriteOutcome::NotDetected,
};
let parsed: toml::Value = match toml::from_str(&root_content) {
Ok(v) => v,
Err(_) => return native_write(root, &CargoVersionFile, new_version),
};
let members: Vec<String> = parsed
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.map(str::to_string)
.collect()
})
.unwrap_or_default();
if members.is_empty() {
return native_write(root, &CargoVersionFile, new_version);
}
let mut results: Vec<UpdateResult> = Vec::new();
if let WriteOutcome::Fallback { results: r } =
native_write(root, &CargoVersionFile, new_version)
{
results.extend(r);
}
for pattern in &members {
let glob_pat = root.join(pattern).join("Cargo.toml");
let entries = match glob::glob(&glob_pat.to_string_lossy()) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
update_member_crate(&entry, new_version, &mut results);
}
}
let root_content_after = std::fs::read_to_string(&root_cargo).unwrap_or(root_content);
if let Some(updated) = update_workspace_deps(&root_content_after, &parsed, new_version) {
if std::fs::write(&root_cargo, &updated).is_err() {
ui::warning(&format!("{}: failed to write", root_cargo.display()));
} else if results.is_empty() {
results.push(UpdateResult {
path: root_cargo,
name: CargoVersionFile.name().to_string(),
old_version: String::new(),
new_version: new_version.to_string(),
extra: None,
});
}
}
if results.is_empty() {
WriteOutcome::NotDetected
} else {
WriteOutcome::Fallback { results }
}
}
fn update_workspace_deps(content: &str, parsed: &toml::Value, new_version: &str) -> Option<String> {
let local_deps: std::collections::HashSet<String> = parsed
.get("workspace")
.and_then(|w| w.get("dependencies"))
.and_then(|d| d.as_table())
.map(|t| {
t.iter()
.filter(|(_, v)| v.get("path").is_some())
.map(|(k, _)| k.clone())
.collect()
})
.unwrap_or_default();
if local_deps.is_empty() {
return None;
}
let mut result = String::with_capacity(content.len());
let mut changed = false;
let mut in_section = false;
let mut rewritten: std::collections::HashSet<String> = std::collections::HashSet::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[workspace.dependencies]" {
in_section = true;
result.push_str(line);
result.push('\n');
continue;
} else if trimmed.starts_with('[') {
in_section = false;
}
if in_section {
if let Some((new_line, dep_name)) = try_rewrite_dep_line(line, &local_deps, new_version)
{
result.push_str(&new_line);
result.push('\n');
rewritten.insert(dep_name);
changed = true;
continue;
}
}
result.push_str(line);
result.push('\n');
}
for dep in &local_deps {
if !rewritten.contains(dep) {
ui::warning(&format!(
"[workspace.dependencies] {dep}: version not updated \
— unsupported format (inline table required). \
Update manually: {dep} = {{ version = \"{new_version}\", path = \"...\" }}"
));
}
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
changed.then_some(result)
}
fn try_rewrite_dep_line(
line: &str,
local_deps: &std::collections::HashSet<String>,
new_version: &str,
) -> Option<(String, String)> {
let trimmed = line.trim();
let eq_pos = trimmed.find('=')?;
let dep_name = trimmed[..eq_pos].trim().trim_matches('"');
if !local_deps.contains(dep_name) {
return None;
}
let value_part = trimmed[eq_pos + 1..].trim();
if !value_part.starts_with('{') || !value_part.contains('}') {
return None;
}
let new_value = replace_inline_version(value_part, new_version)?;
let leading = &line[..line.len() - line.trim_start().len()];
let key_part = &trimmed[..eq_pos + 1]; Some((
format!("{leading}{key_part} {new_value}"),
dep_name.to_string(),
))
}
fn replace_inline_version(inline: &str, new_version: &str) -> Option<String> {
let marker = "version = \"";
let start = inline.find(marker)?;
let after_open = start + marker.len();
let end_quote = inline[after_open..].find('"')?;
let after_close = after_open + end_quote + 1;
Some(format!(
"{}version = \"{new_version}\"{}",
&inline[..start],
&inline[after_close..]
))
}
fn is_publish_false(content: &str) -> bool {
let mut in_package = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
} else if trimmed.starts_with('[') {
in_package = false;
}
if in_package && trimmed.starts_with("publish") && trimmed.contains("false") {
return true;
}
}
false
}
fn update_member_crate(path: &Path, new_version: &str, results: &mut Vec<UpdateResult>) {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
ui::warning(&format!("{}: {e}", path.display()));
return;
}
};
if is_publish_false(&content) {
return;
}
let old_version = match CargoVersionFile.read_version(&content) {
Some(v) => v,
None => return,
};
let updated = match CargoVersionFile.write_version(&content, new_version) {
Ok(u) => u,
Err(_) => return,
};
let new_ver = CargoVersionFile
.read_version(&updated)
.unwrap_or_else(|| new_version.to_string());
if std::fs::write(path, &updated).is_err() {
ui::warning(&format!("{}: failed to write", path.display()));
return;
}
results.push(UpdateResult {
path: path.to_path_buf(),
name: CargoVersionFile.name().to_string(),
old_version,
new_version: new_ver,
extra: None,
});
}
impl Ecosystem for Rust {
fn name(&self) -> &'static str {
"rust"
}
fn detect(&self, root: &Path) -> bool {
root.join("Cargo.toml").exists()
}
fn version_files(&self) -> &[&str] {
&["Cargo.toml"]
}
fn write_version(&self, root: &Path, new_version: &str) -> WriteOutcome {
workspace_native_write(root, new_version)
}
fn sync_lock(&self, root: &Path) -> Vec<SyncOutcome> {
vec![try_sync(
root,
"Cargo.lock",
"cargo",
&["update", "--workspace"],
)]
}
fn lock_files(&self) -> &[&str] {
&["Cargo.lock"]
}
fn version_file_engine(&self) -> Option<Box<dyn VersionFile>> {
Some(Box::new(CargoVersionFile))
}
}