1use cargo_metadata::{
2 Dependency, DependencyKind, Metadata,
3 camino::{Utf8Path, Utf8PathBuf},
4 semver::VersionReq,
5};
6use patcher::{DiffAlgorithm, Differ, MultifilePatch};
7use pathdiff::diff_utf8_paths;
8use std::collections::{BTreeMap, BTreeSet};
9use std::fs::read_to_string;
10use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, table, value};
11
12use crate::MemberDependency;
13
14fn format_req(req: &VersionReq) -> String {
17 let s = req.to_string();
18 if let Some(rest) = s.strip_prefix('^')
19 && !rest.contains([',', ' ', '<', '>', '=', '~', '^', '*'])
20 {
21 rest.to_string()
22 } else {
23 s
24 }
25}
26
27fn req_floor(req: &VersionReq) -> (u64, u64, u64) {
32 req.comparators
33 .first()
34 .map(|c| (c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0)))
35 .unwrap_or((0, 0, 0))
36}
37
38#[derive(Default)]
39struct MemberChanges {
40 to_workspace: Vec<Dependency>,
41 to_inline: Vec<(String, Item, DependencyKind)>,
42}
43
44pub fn generate_diff(
45 add: &BTreeMap<String, Vec<MemberDependency>>,
46 remove: &BTreeSet<String>,
47 inline: &BTreeMap<String, MemberDependency>,
48 metadata: &Metadata,
49 dotted: bool,
50) -> anyhow::Result<()> {
51 let mut changes = Vec::with_capacity(add.len() + inline.len() + 1);
52
53 let workspace_path = metadata.workspace_root.join("Cargo.toml");
54 let workspace_content = read_to_string(&workspace_path)?;
55 let mut workspace_doc: DocumentMut = workspace_content.parse()?;
56
57 let mut inline_items: BTreeMap<String, Item> = BTreeMap::new();
59 if let Some(workspace_dependencies) = workspace_doc
60 .get("workspace")
61 .and_then(|w| w.get("dependencies"))
62 .and_then(|d| d.as_table_like())
63 {
64 for name in inline.keys() {
65 if let Some(item) = workspace_dependencies.get(name) {
66 inline_items.insert(name.clone(), item.clone());
67 }
68 }
69 }
70
71 if let Some(workspace_dependencies) = workspace_doc
72 .get_mut("workspace")
73 .and_then(|w| w.get_mut("dependencies"))
74 .and_then(|d| d.as_table_like_mut())
75 {
76 for name in remove {
77 workspace_dependencies.remove(name);
78 }
79 }
80
81 let mut member_changes: BTreeMap<Utf8PathBuf, MemberChanges> = BTreeMap::new();
82
83 if !add.is_empty() {
84 let Some(workspace_table) = workspace_doc
85 .as_table_mut()
86 .entry("workspace")
87 .or_insert_with(table)
88 .as_table_mut()
89 else {
90 anyhow::bail!("Invalid [workspace] entry");
91 };
92
93 if !workspace_table.contains_key("dependencies") {
97 workspace_table.insert("dependencies", table());
98 }
99
100 let Some(workspace_dependencies) = workspace_table
101 .get_mut("dependencies")
102 .and_then(|d| d.as_table_mut())
103 else {
104 anyhow::bail!("Invalid workspace dependencies entry");
105 };
106
107 for (name, members) in add {
108 let mut dependency: Option<&Dependency> = None;
112 let mut no_default_features = false;
113 let mut features = BTreeSet::new();
114 for member in members {
115 match dependency {
116 None => dependency = Some(&member.dependency),
117 Some(current)
118 if req_floor(&member.dependency.req) > req_floor(¤t.req) =>
119 {
120 dependency = Some(&member.dependency);
121 }
122 _ => {}
123 }
124
125 features.extend(member.dependency.features.iter().cloned());
126 no_default_features |= !member.dependency.uses_default_features;
127
128 member_changes
129 .entry(member.manifest_path.clone())
130 .or_default()
131 .to_workspace
132 .push(member.dependency.clone());
133 }
134
135 if let Some(dependency) = dependency {
136 let req_str = format_req(&dependency.req);
137 let value = if no_default_features || !features.is_empty() {
138 let mut entry = InlineTable::new();
139 entry.insert("version", req_str.into());
140
141 if no_default_features {
142 entry.insert("default-features", false.into());
143 }
144
145 if !features.is_empty() {
146 entry.insert("features", Array::from_iter(features).into());
147 }
148
149 entry.into()
150 } else {
151 value(req_str)
152 };
153
154 if !workspace_dependencies.contains_key(name) {
159 workspace_dependencies.insert(name, value);
160 }
161 }
162 }
163
164 workspace_dependencies.sort_values();
167 }
168
169 for (name, member) in inline {
170 if let Some(item) = inline_items.remove(name) {
171 member_changes
172 .entry(member.manifest_path.clone())
173 .or_default()
174 .to_inline
175 .push((name.clone(), item, member.dependency.kind));
176 }
177 }
178
179 for (path, mc) in member_changes {
180 update_member(&path, &mc, dotted, &metadata.workspace_root, &mut changes)?;
181 }
182
183 changes.push((workspace_path, workspace_content, workspace_doc.to_string()));
184
185 let mut patches = Vec::new();
186
187 for (path, original, modified) in changes {
188 if original == modified {
189 continue;
190 }
191 let differ = Differ::new(&original, &modified);
192 let mut patch = differ.generate();
193
194 let relative_path = diff_utf8_paths(&path, &metadata.workspace_root).unwrap_or(path);
195 patch.old_file = relative_path.to_string();
196 patch.new_file = relative_path.to_string();
197
198 patches.push(patch);
199 }
200
201 let multi_patch = MultifilePatch::new(patches);
202
203 println!("{multi_patch}");
204 Ok(())
205}
206
207fn update_member(
208 path: &Utf8Path,
209 mc: &MemberChanges,
210 dotted: bool,
211 workspace_root: &Utf8Path,
212 changes: &mut Vec<(Utf8PathBuf, String, String)>,
213) -> anyhow::Result<()> {
214 let member_content = read_to_string(path)?;
215 let mut member_doc: DocumentMut = member_content.parse()?;
216 let member_dir = path.parent().unwrap_or(path);
217
218 for dep in &mc.to_workspace {
219 let memmber_dependencies = match dep.kind {
220 DependencyKind::Normal => member_doc["dependencies"].as_table_mut(),
221 DependencyKind::Development => member_doc["dev-dependencies"].as_table_mut(),
222 DependencyKind::Build => member_doc["build-dependencies"].as_table_mut(),
223 _ => None,
224 };
225
226 if let Some(member_dependencies) = memmber_dependencies {
227 update_dependency(member_dependencies, dep, dotted);
228 }
229 }
230
231 for (name, item, kind) in &mc.to_inline {
232 let memmber_dependencies = match kind {
233 DependencyKind::Normal => member_doc["dependencies"].as_table_mut(),
234 DependencyKind::Development => member_doc["dev-dependencies"].as_table_mut(),
235 DependencyKind::Build => member_doc["build-dependencies"].as_table_mut(),
236 _ => None,
237 };
238
239 if let Some(member_dependencies) = memmber_dependencies {
240 inline_dependency(member_dependencies, name, item, workspace_root, member_dir);
241 }
242 }
243
244 changes.push((path.to_path_buf(), member_content, member_doc.to_string()));
245
246 Ok(())
247}
248
249fn update_dependency(member_dependencies: &mut Table, dep: &Dependency, dotted: bool) {
250 if let Some(entry) = member_dependencies[&dep.name].as_table_like_mut() {
251 entry.remove("version");
252 entry.remove("default-features");
253 let rest: Vec<(String, toml_edit::Value)> = entry
255 .iter()
256 .filter(|(k, _)| *k != "workspace")
257 .filter_map(|(k, v)| v.as_value().map(|val| (k.to_string(), val.clone())))
258 .collect();
259
260 entry.clear();
261 entry.insert("workspace", value(true));
262
263 for (k, v) in rest {
264 entry.insert(&k, Item::Value(v));
265 }
266
267 entry.fmt();
268 } else {
269 let mut entry = InlineTable::new();
270 entry.set_dotted(dotted);
271 entry.insert("workspace", true.into());
272 member_dependencies[&dep.name] = entry.into();
273 }
274}
275
276fn rebase_path(ws_path: &str, workspace_root: &Utf8Path, member_dir: &Utf8Path) -> String {
279 let abs = workspace_root.join(ws_path);
280 diff_utf8_paths(&abs, member_dir)
281 .map(|p| p.to_string())
282 .unwrap_or_else(|| ws_path.to_string())
283}
284
285fn inline_dependency(
286 member_dependencies: &mut Table,
287 name: &str,
288 ws_item: &Item,
289 workspace_root: &Utf8Path,
290 member_dir: &Utf8Path,
291) {
292 let mut extras: Vec<(String, Item)> = Vec::new();
294 if let Some(entry) = member_dependencies
295 .get(name)
296 .and_then(|e| e.as_table_like())
297 {
298 for (k, v) in entry.iter() {
299 if k != "workspace" {
300 extras.push((k.to_string(), v.clone()));
301 }
302 }
303 }
304
305 if extras.is_empty() {
306 let rebased = rebase_ws_item(ws_item, workspace_root, member_dir);
308 member_dependencies.insert(name, rebased);
309 } else {
310 let mut merged = InlineTable::new();
312 match ws_item {
313 Item::Value(toml_edit::Value::String(s)) => {
314 merged.insert("version", s.value().clone().into());
315 }
316
317 Item::Value(toml_edit::Value::InlineTable(t)) => {
318 for (k, v) in t.iter() {
319 if k == "path"
320 && let Some(p) = v.as_str()
321 {
322 merged.insert(k, rebase_path(p, workspace_root, member_dir).into());
323 continue;
324 }
325
326 merged.insert(k, v.clone());
327 }
328 }
329
330 Item::Table(t) => {
331 for (k, v) in t.iter() {
332 if k == "path"
333 && let Some(p) = v.as_str()
334 {
335 merged.insert(k, rebase_path(p, workspace_root, member_dir).into());
336 continue;
337 }
338
339 if let Some(val) = v.as_value() {
340 merged.insert(k, val.clone());
341 }
342 }
343 }
344 _ => {}
345 }
346
347 for (k, v) in extras {
348 if let Some(val) = v.as_value() {
349 merged.insert(&k, val.clone());
350 }
351 }
352
353 merged.fmt();
354 member_dependencies[name] = value(merged);
355 }
356}
357
358fn rebase_ws_item(item: &Item, workspace_root: &Utf8Path, member_dir: &Utf8Path) -> Item {
360 match item {
361 Item::Value(toml_edit::Value::InlineTable(t)) => {
362 let mut new_t = t.clone();
363 if let Some(p) = t.get("path").and_then(|v| v.as_str()) {
364 new_t.insert("path", rebase_path(p, workspace_root, member_dir).into());
365 }
366
367 Item::Value(toml_edit::Value::InlineTable(new_t))
368 }
369
370 Item::Table(t) => {
371 let mut new_t = t.clone();
372 if let Some(p) = t.get("path").and_then(|v| v.as_str()) {
373 new_t.insert("path", value(rebase_path(p, workspace_root, member_dir)));
374 }
375
376 Item::Table(new_t)
377 }
378
379 other => other.clone(),
381 }
382}