vivacity_resolver/
path_repo.rs1use crate::loader;
14use crate::package::{Origin, Package};
15use crate::repository::RepoError;
16use serde_json::{Map, Value};
17use sha1::{Digest, Sha1};
18use std::path::Path;
19use vivacity_core::glob;
20use vivacity_core::root_version;
21
22pub struct PathRepository {
24 pub members: Vec<usize>,
26 pub url_as_written: String,
28}
29
30impl PathRepository {
31 pub fn repo_name(&self) -> String {
33 format!("path repo ({})", self.url_as_written)
34 }
35}
36
37fn php_is_absolute_path(path: &str) -> bool {
40 path.starts_with('/') || path.as_bytes().get(1) == Some(&b':') || path.starts_with("\\\\")
41}
42
43pub fn open(
46 def: &Value,
47 project_dir: &Path,
48 origin: Origin,
49 arena: &mut Vec<Package>,
50) -> Result<PathRepository, RepoError> {
51 let url_as_written = match def.get("url") {
52 Some(Value::String(s)) => s.clone(),
53 _ => {
54 return Err(RepoError::data(
55 "You must specify the `url` configuration for the path repository",
56 ))
57 }
58 };
59 let url = glob::expand_path(&url_as_written);
60 let mut options: Map<String, Value> = match def.get("options") {
61 Some(Value::Object(m)) => m.clone(),
62 _ => Map::new(),
63 };
64 if options.get("relative").is_none_or(Value::is_null) {
66 options.insert("relative".into(), Value::Bool(!php_is_absolute_path(&url)));
67 }
68 let options = Value::Object(options);
69 let serialized_options = vivacity_core::phpserialize::serialize(&options)
70 .map_err(|e| RepoError::data(format!("path repository options: {e}")))?;
71
72 let url_matches: Vec<String> = glob::glob_dirs(&url, project_dir)
73 .into_iter()
74 .map(|m| {
75 let m = if cfg!(windows) {
77 m.replace('\\', "/")
78 } else {
79 m
80 };
81 m.trim_end_matches('/').to_owned()
82 })
83 .collect();
84 if url_matches.is_empty() {
85 let has_magic = |s: &str| s.contains(['*', '{', '}']);
86 if has_magic(&url) {
87 let mut walk = url.clone();
88 while has_magic(&walk) {
89 walk = glob::php_dirname(&walk);
90 }
91 if project_dir.join(&walk).is_dir() {
92 return Ok(PathRepository {
93 members: Vec::new(),
94 url_as_written,
95 });
96 }
97 }
98 return Err(RepoError::data(format!(
99 "The `url` supplied for the path ({url}) repository does not exist"
100 )));
101 }
102
103 let reference_mode = match options.get("reference") {
107 None | Some(Value::Null) => "auto".to_owned(),
108 Some(Value::String(s)) => s.clone(),
109 Some(_) => String::new(),
110 };
111 let mut members: Vec<usize> = Vec::new();
112 for matched in url_matches {
113 let dir = project_dir.join(&matched);
114 let dir = std::fs::canonicalize(&dir).unwrap_or(dir);
116 let composer_file = dir.join("composer.json");
117 if !composer_file.exists() {
118 continue;
119 }
120 let json = std::fs::read(&composer_file)
121 .map_err(|e| RepoError::data(format!("{}: {e}", composer_file.display())))?;
122 let mut package: Map<String, Value> = match serde_json::from_slice::<Value>(&json) {
123 Ok(Value::Object(m)) => m,
124 Ok(_) => {
125 return Err(RepoError::data(format!(
126 "\"{}\" does not contain valid JSON",
127 composer_file.display()
128 )))
129 }
130 Err(e) => {
131 return Err(RepoError::data(format!(
132 "\"{}\" does not contain valid JSON\n{e}",
133 composer_file.display()
134 )))
135 }
136 };
137 let mut dist = Map::new();
138 dist.insert("type".into(), Value::String("path".into()));
139 dist.insert("url".into(), Value::String(matched.clone()));
140 match reference_mode.as_str() {
141 "none" => {
142 dist.insert("reference".into(), Value::Null);
143 }
144 "config" | "auto" => {
145 let mut h = Sha1::new();
146 h.update(&json);
147 h.update(serialized_options.as_bytes());
148 dist.insert(
149 "reference".into(),
150 Value::String(format!("{:x}", h.finalize())),
151 );
152 }
153 _ => {}
154 }
155 package.insert("dist".into(), Value::Object(dist));
156
157 let mut transport = Map::new();
159 for (k, v) in options.as_object().into_iter().flatten() {
160 if k == "symlink" || k == "relative" {
161 transport.insert(k.clone(), v.clone());
162 }
163 }
164 package.insert("transport-options".into(), Value::Object(transport));
165
166 let name = package
167 .get("name")
168 .and_then(Value::as_str)
169 .map(str::to_owned);
170 if let Some(name) = &name {
171 if let Some(v) = options
172 .get("versions")
173 .and_then(|v| v.get(name))
174 .filter(|v| !v.is_null())
175 {
176 package.insert("version".into(), v.clone());
177 }
178 }
179 let has_version = |p: &Map<String, Value>| p.get("version").is_some_and(|v| !v.is_null());
180
181 if !has_version(&package) {
182 if let Some(env_version) = root_version::root_version_from_env() {
185 let head = |d: &Path| root_version::git(d, &["rev-parse", "HEAD"]);
186 if let (Some(a), Some(b)) = (head(&dir), head(project_dir)) {
187 if a == b {
188 package.insert("version".into(), Value::String(env_version));
189 }
190 }
191 }
192 }
193
194 if reference_mode == "auto" && dir.join(".git").is_dir() {
195 if let Some(out) = root_version::git(
196 &dir,
197 &[
198 "rev-list",
199 "-n1",
200 "--format=%H",
201 "HEAD",
202 "--no-show-signature",
203 ],
204 ) {
205 let sha = out
207 .lines()
208 .filter(|l| !l.starts_with("commit "))
209 .collect::<Vec<_>>()
210 .join("\n");
211 if let Some(d) = package.get_mut("dist").and_then(Value::as_object_mut) {
212 d.insert("reference".into(), Value::String(sha.trim().to_owned()));
213 }
214 }
215 }
216
217 let mut feature: Option<String> = None;
218 if !has_version(&package) {
219 match root_version::guess_version(&Value::Object(package.clone()), &dir) {
220 Some(g) if !g.pretty_version.is_empty() => {
221 feature = g.feature_pretty_version.filter(|f| !f.is_empty());
222 package.insert("version".into(), Value::String(g.pretty_version));
223 }
224 _ => {
225 package.insert("version".into(), Value::String("dev-main".into()));
226 }
227 }
228 }
229 let add = |config: Value, arena: &mut Vec<Package>, members: &mut Vec<usize>| {
230 let ids = loader::load_packages(&[config], origin, arena, false).map_err(|e| {
231 RepoError::data(format!(
232 "Failed loading the package in {}: {}",
233 composer_file.display(),
234 e.0
235 ))
236 })?;
237 members.extend(ids);
238 Ok::<(), RepoError>(())
239 };
240 if let Some(feature_version) = feature {
241 let mut feature_package = package.clone();
242 feature_package.insert("version".into(), Value::String(feature_version));
243 add(Value::Object(feature_package), arena, &mut members)?;
244 }
245 add(Value::Object(package), arena, &mut members)?;
246 }
247 Ok(PathRepository {
248 members,
249 url_as_written,
250 })
251}