use cargo_metadata::{
Dependency, DependencyKind, Metadata,
camino::{Utf8Path, Utf8PathBuf},
semver::VersionReq,
};
use patcher::{DiffAlgorithm, Differ, MultifilePatch};
use pathdiff::diff_utf8_paths;
use std::collections::{BTreeMap, BTreeSet};
use std::fs::read_to_string;
use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, table, value};
use crate::MemberDependency;
fn format_req(req: &VersionReq) -> String {
let s = req.to_string();
if let Some(rest) = s.strip_prefix('^')
&& !rest.contains([',', ' ', '<', '>', '=', '~', '^', '*'])
{
rest.to_string()
} else {
s
}
}
fn req_floor(req: &VersionReq) -> (u64, u64, u64) {
req.comparators
.first()
.map(|c| (c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0)))
.unwrap_or((0, 0, 0))
}
#[derive(Default)]
struct MemberChanges {
to_workspace: Vec<Dependency>,
to_inline: Vec<(String, Item, DependencyKind)>,
}
pub fn generate_diff(
add: &BTreeMap<String, Vec<MemberDependency>>,
remove: &BTreeSet<String>,
inline: &BTreeMap<String, MemberDependency>,
metadata: &Metadata,
dotted: bool,
) -> anyhow::Result<()> {
let mut changes = Vec::with_capacity(add.len() + inline.len() + 1);
let workspace_path = metadata.workspace_root.join("Cargo.toml");
let workspace_content = read_to_string(&workspace_path)?;
let mut workspace_doc: DocumentMut = workspace_content.parse()?;
let mut inline_items: BTreeMap<String, Item> = BTreeMap::new();
if let Some(workspace_dependencies) = workspace_doc
.get("workspace")
.and_then(|w| w.get("dependencies"))
.and_then(|d| d.as_table_like())
{
for name in inline.keys() {
if let Some(item) = workspace_dependencies.get(name) {
inline_items.insert(name.clone(), item.clone());
}
}
}
if let Some(workspace_dependencies) = workspace_doc
.get_mut("workspace")
.and_then(|w| w.get_mut("dependencies"))
.and_then(|d| d.as_table_like_mut())
{
for name in remove {
workspace_dependencies.remove(name);
}
}
let mut member_changes: BTreeMap<Utf8PathBuf, MemberChanges> = BTreeMap::new();
if !add.is_empty() {
let Some(workspace_table) = workspace_doc
.as_table_mut()
.entry("workspace")
.or_insert_with(table)
.as_table_mut()
else {
anyhow::bail!("Invalid [workspace] entry");
};
if !workspace_table.contains_key("dependencies") {
workspace_table.insert("dependencies", table());
}
let Some(workspace_dependencies) = workspace_table
.get_mut("dependencies")
.and_then(|d| d.as_table_mut())
else {
anyhow::bail!("Invalid workspace dependencies entry");
};
for (name, members) in add {
let mut dependency: Option<&Dependency> = None;
let mut no_default_features = false;
let mut features = BTreeSet::new();
for member in members {
match dependency {
None => dependency = Some(&member.dependency),
Some(current)
if req_floor(&member.dependency.req) > req_floor(¤t.req) =>
{
dependency = Some(&member.dependency);
}
_ => {}
}
features.extend(member.dependency.features.iter().cloned());
no_default_features |= !member.dependency.uses_default_features;
member_changes
.entry(member.manifest_path.clone())
.or_default()
.to_workspace
.push(member.dependency.clone());
}
if let Some(dependency) = dependency {
let req_str = format_req(&dependency.req);
let value = if no_default_features || !features.is_empty() {
let mut entry = InlineTable::new();
entry.insert("version", req_str.into());
if no_default_features {
entry.insert("default-features", false.into());
}
if !features.is_empty() {
entry.insert("features", Array::from_iter(features).into());
}
entry.into()
} else {
value(req_str)
};
if !workspace_dependencies.contains_key(name) {
workspace_dependencies.insert(name, value);
}
}
}
workspace_dependencies.sort_values();
}
for (name, member) in inline {
if let Some(item) = inline_items.remove(name) {
member_changes
.entry(member.manifest_path.clone())
.or_default()
.to_inline
.push((name.clone(), item, member.dependency.kind));
}
}
for (path, mc) in member_changes {
update_member(&path, &mc, dotted, &metadata.workspace_root, &mut changes)?;
}
changes.push((workspace_path, workspace_content, workspace_doc.to_string()));
let mut patches = Vec::new();
for (path, original, modified) in changes {
if original == modified {
continue;
}
let differ = Differ::new(&original, &modified);
let mut patch = differ.generate();
let relative_path = diff_utf8_paths(&path, &metadata.workspace_root).unwrap_or(path);
patch.old_file = relative_path.to_string();
patch.new_file = relative_path.to_string();
patches.push(patch);
}
let multi_patch = MultifilePatch::new(patches);
println!("{multi_patch}");
Ok(())
}
fn update_member(
path: &Utf8Path,
mc: &MemberChanges,
dotted: bool,
workspace_root: &Utf8Path,
changes: &mut Vec<(Utf8PathBuf, String, String)>,
) -> anyhow::Result<()> {
let member_content = read_to_string(path)?;
let mut member_doc: DocumentMut = member_content.parse()?;
let member_dir = path.parent().unwrap_or(path);
for dep in &mc.to_workspace {
let memmber_dependencies = match dep.kind {
DependencyKind::Normal => member_doc["dependencies"].as_table_mut(),
DependencyKind::Development => member_doc["dev-dependencies"].as_table_mut(),
DependencyKind::Build => member_doc["build-dependencies"].as_table_mut(),
_ => None,
};
if let Some(member_dependencies) = memmber_dependencies {
update_dependency(member_dependencies, dep, dotted);
}
}
for (name, item, kind) in &mc.to_inline {
let memmber_dependencies = match kind {
DependencyKind::Normal => member_doc["dependencies"].as_table_mut(),
DependencyKind::Development => member_doc["dev-dependencies"].as_table_mut(),
DependencyKind::Build => member_doc["build-dependencies"].as_table_mut(),
_ => None,
};
if let Some(member_dependencies) = memmber_dependencies {
inline_dependency(member_dependencies, name, item, workspace_root, member_dir);
}
}
changes.push((path.to_path_buf(), member_content, member_doc.to_string()));
Ok(())
}
fn update_dependency(member_dependencies: &mut Table, dep: &Dependency, dotted: bool) {
if let Some(entry) = member_dependencies[&dep.name].as_table_like_mut() {
entry.remove("version");
entry.remove("default-features");
let rest: Vec<(String, toml_edit::Value)> = entry
.iter()
.filter(|(k, _)| *k != "workspace")
.filter_map(|(k, v)| v.as_value().map(|val| (k.to_string(), val.clone())))
.collect();
entry.clear();
entry.insert("workspace", value(true));
for (k, v) in rest {
entry.insert(&k, Item::Value(v));
}
entry.fmt();
} else {
let mut entry = InlineTable::new();
entry.set_dotted(dotted);
entry.insert("workspace", true.into());
member_dependencies[&dep.name] = entry.into();
}
}
fn rebase_path(ws_path: &str, workspace_root: &Utf8Path, member_dir: &Utf8Path) -> String {
let abs = workspace_root.join(ws_path);
diff_utf8_paths(&abs, member_dir)
.map(|p| p.to_string())
.unwrap_or_else(|| ws_path.to_string())
}
fn inline_dependency(
member_dependencies: &mut Table,
name: &str,
ws_item: &Item,
workspace_root: &Utf8Path,
member_dir: &Utf8Path,
) {
let mut extras: Vec<(String, Item)> = Vec::new();
if let Some(entry) = member_dependencies
.get(name)
.and_then(|e| e.as_table_like())
{
for (k, v) in entry.iter() {
if k != "workspace" {
extras.push((k.to_string(), v.clone()));
}
}
}
if extras.is_empty() {
let rebased = rebase_ws_item(ws_item, workspace_root, member_dir);
member_dependencies.insert(name, rebased);
} else {
let mut merged = InlineTable::new();
match ws_item {
Item::Value(toml_edit::Value::String(s)) => {
merged.insert("version", s.value().clone().into());
}
Item::Value(toml_edit::Value::InlineTable(t)) => {
for (k, v) in t.iter() {
if k == "path"
&& let Some(p) = v.as_str()
{
merged.insert(k, rebase_path(p, workspace_root, member_dir).into());
continue;
}
merged.insert(k, v.clone());
}
}
Item::Table(t) => {
for (k, v) in t.iter() {
if k == "path"
&& let Some(p) = v.as_str()
{
merged.insert(k, rebase_path(p, workspace_root, member_dir).into());
continue;
}
if let Some(val) = v.as_value() {
merged.insert(k, val.clone());
}
}
}
_ => {}
}
for (k, v) in extras {
if let Some(val) = v.as_value() {
merged.insert(&k, val.clone());
}
}
merged.fmt();
member_dependencies[name] = value(merged);
}
}
fn rebase_ws_item(item: &Item, workspace_root: &Utf8Path, member_dir: &Utf8Path) -> Item {
match item {
Item::Value(toml_edit::Value::InlineTable(t)) => {
let mut new_t = t.clone();
if let Some(p) = t.get("path").and_then(|v| v.as_str()) {
new_t.insert("path", rebase_path(p, workspace_root, member_dir).into());
}
Item::Value(toml_edit::Value::InlineTable(new_t))
}
Item::Table(t) => {
let mut new_t = t.clone();
if let Some(p) = t.get("path").and_then(|v| v.as_str()) {
new_t.insert("path", value(rebase_path(p, workspace_root, member_dir)));
}
Item::Table(new_t)
}
other => other.clone(),
}
}