agpm_cli/resolver/
conflict_service.rs1use anyhow::Result;
7use std::collections::HashMap;
8
9use crate::core::ResourceType;
10use crate::lockfile::ResourceId;
11use crate::manifest::{DetailedDependency, ResourceDependency};
12use crate::utils::EMPTY_VARIANT_INPUTS_HASH;
13use crate::version::conflict::VersionConflict;
14
15use super::types::DependencyKey;
16
17pub struct ConflictService;
22
23impl ConflictService {
24 pub fn new() -> Self {
26 Self
27 }
28
29 pub fn detect_version_conflicts(
39 &mut self,
40 dependencies: &HashMap<DependencyKey, DetailedDependency>,
41 ) -> Result<Vec<VersionConflict>> {
42 let mut conflicts = Vec::new();
43
44 let mut grouped: HashMap<(ResourceType, String, String, String), Vec<_>> = HashMap::new();
46
47 for (key, dep) in dependencies {
48 let source = dep.source.clone().unwrap_or_default();
49 let tool = dep.tool.clone().unwrap_or_default();
50
51 let group_key = (
52 key.0, dep.path.clone(),
54 source,
55 tool,
56 );
57 grouped.entry(group_key).or_default().push((key, dep));
58 }
59
60 for ((resource_type, path, source, tool), deps) in grouped {
62 if deps.len() > 1 {
63 let mut conflicting_requirements = Vec::new();
65
66 for (key, dep) in &deps {
67 let requirement = dep.version.clone().unwrap_or_else(|| "latest".to_string());
68
69 conflicting_requirements.push(
70 crate::version::conflict::ConflictingRequirement {
71 required_by: format!("{}/{}", key.0, key.1), requirement,
73 resolved_sha: String::new(), resolved_version: None,
75 parent_version_constraint: None,
76 parent_resolved_sha: None,
77 },
78 );
79 }
80
81 let resource_id = ResourceId::new(
83 path.clone(),
84 if source.is_empty() {
85 None
86 } else {
87 Some(source.clone())
88 },
89 if tool.is_empty() {
90 None
91 } else {
92 Some(tool.clone())
93 },
94 resource_type,
95 EMPTY_VARIANT_INPUTS_HASH.clone(),
96 );
97
98 conflicts.push(VersionConflict {
99 resource: resource_id,
100 conflicting_requirements,
101 });
102 }
103 }
104
105 Ok(conflicts)
106 }
107
108 pub fn detect_path_conflicts(
118 dependencies: &HashMap<DependencyKey, DetailedDependency>,
119 ) -> Vec<(String, Vec<String>)> {
120 let mut conflicts = Vec::new();
121 let mut install_paths: HashMap<String, Vec<String>> = HashMap::new();
122
123 for (key, dep) in dependencies {
125 let install_path = format!("{}/{}", key.0, key.1); install_paths.entry(install_path.clone()).or_default().push(format!(
127 "{}/{} (from {})",
128 key.0,
129 key.1,
130 dep.source.as_deref().unwrap_or("local")
131 ));
132 }
133
134 for (path, deps) in install_paths {
136 if deps.len() > 1 {
137 conflicts.push((path, deps));
138 }
139 }
140
141 conflicts
142 }
143
144 pub fn has_conflict(
156 &mut self,
157 dependencies: &HashMap<DependencyKey, DetailedDependency>,
158 new_dep: &ResourceDependency,
159 new_key: &DependencyKey,
160 ) -> bool {
161 let (new_path, new_source, new_tool) = match new_dep {
163 ResourceDependency::Simple(path) => (path, None, None),
164 ResourceDependency::Detailed(details) => {
165 (&details.path, details.source.as_deref(), details.tool.as_deref())
166 }
167 };
168
169 for (key, dep) in dependencies {
171 if key.0 == new_key.0 && key.1 != new_key.1 && dep.path == *new_path
174 && dep.source == new_source.map(String::from)
175 && dep.tool == new_tool.map(String::from)
176 {
177 return true;
178 }
179 }
180
181 false
182 }
183}
184
185impl Default for ConflictService {
186 fn default() -> Self {
187 Self::new()
188 }
189}