1use crate::decisions::Decisions;
6use crate::package::Package;
7use crate::platform::is_platform_package;
8use crate::pool::{Pool, Request};
9use serde_json::Value;
10use std::cmp::Ordering;
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Operation {
15 Install(usize),
16 Update(usize, usize),
18 Uninstall(usize),
19 MarkAliasInstalled(usize),
20 MarkAliasUninstalled(usize),
21}
22
23fn abandoned(p: &Package) -> (bool, Option<String>) {
25 match p.raw.get("abandoned") {
26 None | Some(Value::Null) => (false, None),
27 Some(Value::String(s)) => (!s.is_empty() && s != "0", Some(s.clone())),
29 Some(Value::Bool(b)) => (*b, None),
30 Some(Value::Number(n)) => (n.as_f64() != Some(0.0), None),
31 Some(Value::Array(a)) => (!a.is_empty(), None),
32 Some(Value::Object(o)) => (!o.is_empty(), None),
33 }
34}
35
36fn package_sort(a: &Package, b: &Package) -> Ordering {
38 if a.name == b.name {
39 if a.is_alias() != b.is_alias() {
40 return if a.is_alias() {
41 Ordering::Less
42 } else {
43 Ordering::Greater
44 };
45 }
46 return b.version.as_bytes().cmp(a.version.as_bytes());
47 }
48 b.name.as_bytes().cmp(a.name.as_bytes())
49}
50
51pub struct Transaction {
52 pub operations: Vec<Operation>,
53}
54
55impl Transaction {
56 pub fn new(arena: &[Package], present: &[usize], result: &[usize]) -> Transaction {
58 let mut result_map: Vec<usize> = result.to_vec();
60 result_map.sort_by(|&a, &b| package_sort(&arena[a], &arena[b]));
61 let mut by_name: HashMap<String, Vec<usize>> = HashMap::new();
62 for &idx in result {
63 for name in arena[idx].names(true) {
64 by_name.entry(name).or_default().push(idx);
65 }
66 }
67 for list in by_name.values_mut() {
68 list.sort_by(|&a, &b| package_sort(&arena[a], &arena[b]));
69 }
70 let providers =
71 |target: &str| -> Vec<usize> { by_name.get(target).cloned().unwrap_or_default() };
72
73 let mut operations: Vec<Operation> = Vec::new();
75 let mut present_package_map: HashMap<String, usize> = HashMap::new();
76 let mut remove_map: Vec<(String, usize)> = Vec::new();
77 let mut present_alias_map: HashSet<String> = HashSet::new();
78 let mut remove_alias_map: Vec<(String, usize)> = Vec::new();
79 for &idx in present {
80 let p = &arena[idx];
81 if p.is_alias() {
82 let key = format!("{}::{}", p.name, p.version);
83 present_alias_map.insert(key.clone());
84 if let Some(slot) = remove_alias_map.iter_mut().find(|(k, _)| *k == key) {
85 slot.1 = idx;
86 } else {
87 remove_alias_map.push((key, idx));
88 }
89 } else {
90 present_package_map.insert(p.name.clone(), idx);
91 if let Some(slot) = remove_map.iter_mut().find(|(k, _)| *k == p.name) {
92 slot.1 = idx;
93 } else {
94 remove_map.push((p.name.clone(), idx));
95 }
96 }
97 }
98
99 let mut roots: Vec<usize> = result_map.clone();
101 for &idx in &result_map {
102 if !roots.contains(&idx) {
103 continue;
104 }
105 for link in arena[idx].requires.iter() {
106 for require in providers(&link.target) {
107 if require != idx {
108 roots.retain(|&r| r != require);
109 }
110 }
111 }
112 }
113
114 let mut stack: Vec<usize> = roots;
115 let mut visited: HashSet<usize> = HashSet::new();
116 let mut processed: HashSet<usize> = HashSet::new();
117 while let Some(idx) = stack.pop() {
118 if processed.contains(&idx) {
119 continue;
120 }
121 if !visited.contains(&idx) {
122 visited.insert(idx);
123 stack.push(idx);
124 let p = &arena[idx];
125 if let Some(base) = p.alias_of {
126 stack.push(base);
127 } else {
128 for link in p.requires.iter() {
129 for require in providers(&link.target) {
130 stack.push(require);
131 }
132 }
133 }
134 } else {
135 processed.insert(idx);
136 let p = &arena[idx];
137 if p.is_alias() {
138 let key = format!("{}::{}", p.name, p.version);
139 if present_alias_map.contains(&key) {
140 remove_alias_map.retain(|(k, _)| *k != key);
141 } else {
142 operations.push(Operation::MarkAliasInstalled(idx));
143 }
144 } else if let Some(&source) = present_package_map.get(&p.name) {
145 let s = &arena[source];
146 let (pa, pr) = abandoned(p);
147 let (sa, sr) = abandoned(s);
148 if p.version != s.version
149 || p.dist_reference() != s.dist_reference()
150 || p.source_reference() != s.source_reference()
151 || pa != sa
152 || pr != sr
153 {
154 operations.push(Operation::Update(source, idx));
155 }
156 remove_map.retain(|(k, _)| *k != p.name);
157 } else {
158 operations.push(Operation::Install(idx));
159 remove_map.retain(|(k, _)| *k != p.name);
160 }
161 }
162 }
163 for (_, idx) in &remove_map {
164 operations.insert(0, Operation::Uninstall(*idx));
165 }
166 for (_, idx) in &remove_alias_map {
167 operations.push(Operation::MarkAliasUninstalled(*idx));
168 }
169 let operations = Self::move_plugins_to_front(arena, operations);
170 let operations = Self::move_uninstalls_to_front(operations);
171 Transaction { operations }
172 }
173
174 fn op_package(op: &Operation) -> Option<usize> {
175 match op {
176 Operation::Install(p) => Some(*p),
177 Operation::Update(_, t) => Some(*t),
178 _ => None,
179 }
180 }
181
182 fn move_plugins_to_front(arena: &[Package], mut operations: Vec<Operation>) -> Vec<Operation> {
184 let mut dl_no_deps: Vec<Operation> = Vec::new();
185 let mut dl_with_deps: Vec<Operation> = Vec::new();
186 let mut dl_requires: Vec<String> = Vec::new();
187 let mut plugins_no_deps: Vec<Operation> = Vec::new();
188 let mut plugins_with_deps: Vec<Operation> = Vec::new();
189 let mut plugin_requires: Vec<String> = Vec::new();
190 let mut removed: Vec<usize> = Vec::new();
191 for idx in (0..operations.len()).rev() {
192 let op = &operations[idx];
193 let Some(pkg) = Self::op_package(op) else {
194 continue;
195 };
196 let package = &arena[pkg];
197 let is_dl_plugin = package.package_type == "composer-plugin"
198 && package
199 .raw
200 .get("extra")
201 .and_then(|e| e.get("plugin-modifies-downloads"))
202 == Some(&Value::Bool(true));
203 let names = package.names(true);
204 let non_platform_requires = || -> Vec<String> {
205 package
206 .requires
207 .iter()
208 .filter_map(|l| l.key.clone())
209 .filter(|req| !is_platform_package(req))
210 .collect()
211 };
212 if is_dl_plugin || names.iter().any(|n| dl_requires.contains(n)) {
213 let requires = non_platform_requires();
214 if is_dl_plugin && requires.is_empty() {
215 dl_no_deps.insert(0, op.clone());
216 } else {
217 dl_requires.extend(requires);
218 dl_with_deps.insert(0, op.clone());
219 }
220 removed.push(idx);
221 continue;
222 }
223 let is_plugin = package.package_type == "composer-plugin"
224 || package.package_type == "composer-installer";
225 if is_plugin || names.iter().any(|n| plugin_requires.contains(n)) {
226 let requires = non_platform_requires();
227 if is_plugin && requires.is_empty() {
228 plugins_no_deps.insert(0, op.clone());
229 } else {
230 plugin_requires.extend(requires);
231 plugins_with_deps.insert(0, op.clone());
232 }
233 removed.push(idx);
234 }
235 }
236 removed.sort_unstable_by(|a, b| b.cmp(a));
237 for idx in removed {
238 operations.remove(idx);
239 }
240 let mut out = dl_no_deps;
241 out.extend(dl_with_deps);
242 out.extend(plugins_no_deps);
243 out.extend(plugins_with_deps);
244 out.extend(operations);
245 out
246 }
247
248 fn move_uninstalls_to_front(operations: Vec<Operation>) -> Vec<Operation> {
250 let (uninst, rest): (Vec<Operation>, Vec<Operation>) =
251 operations.into_iter().partition(|op| {
252 matches!(
253 op,
254 Operation::Uninstall(_) | Operation::MarkAliasUninstalled(_)
255 )
256 });
257 let mut out = uninst;
258 out.extend(rest);
259 out
260 }
261}
262
263pub struct LockTransaction {
265 pub transaction: Transaction,
266 pub all: Vec<usize>,
268 pub non_dev: Vec<usize>,
269 pub dev: Vec<usize>,
270 pub present: Vec<usize>,
272}
273
274impl LockTransaction {
275 pub fn empty() -> LockTransaction {
276 LockTransaction {
277 transaction: Transaction {
278 operations: Vec::new(),
279 },
280 all: Vec::new(),
281 non_dev: Vec::new(),
282 dev: Vec::new(),
283 present: Vec::new(),
284 }
285 }
286
287 pub fn new(
288 pool: &Pool,
289 arena: &[Package],
290 request: &Request,
291 decisions: &Decisions,
292 ) -> LockTransaction {
293 let mut present: Vec<usize> = Vec::new();
295 for idx in request
296 .locked_repository
297 .iter()
298 .flatten()
299 .copied()
300 .chain(request.fixed_packages.iter().copied())
301 {
302 if !present.contains(&idx) {
303 present.push(idx);
304 }
305 }
306 let unlockable: HashSet<usize> = request.fixed_packages.iter().copied().collect();
307 let mut all = Vec::new();
309 let mut non_dev = Vec::new();
310 for i in (0..decisions.len()).rev() {
311 let literal = decisions.at_offset(i).literal;
312 if literal > 0 {
313 let idx = pool.literal_to_package(literal);
314 all.push(idx);
315 if !unlockable.contains(&idx) {
316 non_dev.push(idx);
317 }
318 }
319 }
320 let transaction = Transaction::new(arena, &present, &all);
321 LockTransaction {
322 transaction,
323 all,
324 non_dev,
325 dev: Vec::new(),
326 present,
327 }
328 }
329
330 pub fn set_non_dev_packages(&mut self, arena: &[Package], extraction: &LockTransaction) {
332 let packages = extraction.new_lock_packages(arena, false);
333 self.dev = std::mem::take(&mut self.non_dev);
334 self.non_dev = Vec::new();
335 for pkg in packages {
336 let name = &arena[pkg].name;
337 let mut i = 0;
338 while i < self.dev.len() {
339 if arena[self.dev[i]].name == *name {
340 let moved = self.dev.remove(i);
341 self.non_dev.push(moved);
342 } else {
343 i += 1;
344 }
345 }
346 }
347 }
348
349 pub fn new_lock_packages(&self, arena: &[Package], dev_mode: bool) -> Vec<usize> {
351 let source = if dev_mode { &self.dev } else { &self.non_dev };
352 source
353 .iter()
354 .copied()
355 .filter(|&idx| !arena[idx].is_alias())
356 .collect()
357 }
358
359 pub fn aliases(
361 &self,
362 arena: &[Package],
363 aliases: &[crate::root::RootAlias],
364 ) -> Vec<crate::root::RootAlias> {
365 let mut remaining: Vec<Option<&crate::root::RootAlias>> =
366 aliases.iter().map(Some).collect();
367 let mut used = Vec::new();
368 for &idx in &self.all {
369 if !arena[idx].is_alias() {
370 continue;
371 }
372 for slot in remaining.iter_mut() {
373 if let Some(a) = slot {
374 if a.package == arena[idx].name {
375 used.push((*a).clone());
376 *slot = None;
377 }
378 }
379 }
380 }
381 used.sort_by(|a, b| a.package.as_bytes().cmp(b.package.as_bytes()));
382 used
383 }
384}
385
386#[derive(Clone, Copy, PartialEq, Eq)]
388pub enum DisplayRef {
389 SourceRefIfDev,
390 SourceRef,
391 DistRef,
392}
393
394pub fn full_pretty_version(p: &Package, truncate: bool, mode: DisplayRef) -> String {
396 let source_type = p.source.as_ref().map(|s| s.kind.as_str()).unwrap_or("");
397 let dist_ref = p
398 .dist
399 .as_ref()
400 .and_then(|d| d.reference.as_deref())
401 .unwrap_or("");
402 if mode == DisplayRef::SourceRefIfDev
403 && (!p.is_dev()
404 || (!matches!(source_type, "hg" | "git")
405 && (!source_type.is_empty() || dist_ref.is_empty())))
406 {
407 return p.pretty_version.clone();
408 }
409 let source_ref = p.source.as_ref().and_then(|s| s.reference.as_deref());
410 let reference: Option<&str> = match mode {
411 DisplayRef::SourceRefIfDev => match source_ref {
412 Some(r) if !r.is_empty() => Some(r),
413 _ => p.dist.as_ref().and_then(|d| d.reference.as_deref()),
414 },
415 DisplayRef::SourceRef => source_ref,
416 DisplayRef::DistRef => p.dist.as_ref().and_then(|d| d.reference.as_deref()),
417 };
418 let Some(reference) = reference else {
419 return p.pretty_version.clone();
420 };
421 if truncate && reference.len() == 40 && source_type != "svn" {
422 let short = String::from_utf8_lossy(&reference.as_bytes()[..7]);
423 return format!("{} {}", p.pretty_version, short);
424 }
425 format!("{} {}", p.pretty_version, reference)
426}
427
428pub fn is_upgrade(from: &str, to: &str) -> bool {
430 if from == to {
431 return true;
432 }
433 let norm = |v: &str| -> String {
434 if matches!(v, "dev-master" | "dev-trunk" | "dev-default") {
435 "9999999-dev".to_owned()
436 } else {
437 v.to_owned()
438 }
439 };
440 let from = norm(from);
441 let to = norm(to);
442 if from.starts_with("dev-") || to.starts_with("dev-") {
443 return true;
444 }
445 !crate::phpver::version_compare_op(&to, &from, "<")
448}
449
450impl Operation {
451 pub fn show(&self, arena: &[Package], lock: bool) -> Option<String> {
454 let full = |idx: usize| full_pretty_version(&arena[idx], true, DisplayRef::SourceRefIfDev);
455 match *self {
456 Operation::Install(p) => Some(format!(
457 "{} {} ({})",
458 if lock { "Locking" } else { "Installing" },
459 arena[p].pretty_name,
460 full(p)
461 )),
462 Operation::Uninstall(p) => {
463 Some(format!("Removing {} ({})", arena[p].pretty_name, full(p)))
464 }
465 Operation::Update(initial, target) => {
466 let (i, t) = (&arena[initial], &arena[target]);
468 let mut from = full(initial);
469 let mut to = full(target);
470 let source_ref = |p: &Package| p.source.as_ref().and_then(|s| s.reference.clone());
471 let dist_ref = |p: &Package| p.dist.as_ref().and_then(|d| d.reference.clone());
472 if from == to && source_ref(i) != source_ref(t) {
473 from = full_pretty_version(i, true, DisplayRef::SourceRef);
474 to = full_pretty_version(t, true, DisplayRef::SourceRef);
475 } else if from == to && dist_ref(i) != dist_ref(t) {
476 from = full_pretty_version(i, true, DisplayRef::DistRef);
477 to = full_pretty_version(t, true, DisplayRef::DistRef);
478 }
479 let action = if is_upgrade(&i.version, &t.version) {
480 "Upgrading"
481 } else {
482 "Downgrading"
483 };
484 Some(format!("{action} {} ({from} => {to})", i.pretty_name))
485 }
486 Operation::MarkAliasInstalled(_) | Operation::MarkAliasUninstalled(_) => None,
487 }
488 }
489
490 pub fn sort_package(&self) -> usize {
492 match *self {
493 Operation::Install(p)
494 | Operation::Uninstall(p)
495 | Operation::MarkAliasInstalled(p)
496 | Operation::MarkAliasUninstalled(p) => p,
497 Operation::Update(_, t) => t,
498 }
499 }
500}
501
502pub fn lock_operation_lines(arena: &[Package], operations: &[Operation]) -> Vec<String> {
506 let mut uninstalls: Vec<&Operation> = Vec::new();
507 let mut installs_updates: Vec<&Operation> = Vec::new();
508 for op in operations {
509 match op {
510 Operation::Uninstall(_) => uninstalls.push(op),
511 Operation::Install(_) | Operation::Update(..) => installs_updates.push(op),
512 Operation::MarkAliasInstalled(_) | Operation::MarkAliasUninstalled(_) => {}
513 }
514 }
515 let by_name = |a: &&Operation, b: &&Operation| {
516 arena[a.sort_package()]
517 .name
518 .as_bytes()
519 .cmp(arena[b.sort_package()].name.as_bytes())
520 };
521 uninstalls.sort_by(by_name);
522 installs_updates.sort_by(by_name);
523 uninstalls
524 .into_iter()
525 .chain(installs_updates)
526 .filter_map(|op| op.show(arena, true).map(|s| format!(" - {s}")))
527 .collect()
528}