1#![warn(missing_docs)]
12
13mod cloner_builder;
14mod source;
15
16pub use cloner_builder::*;
17pub use source::*;
18
19use std::fs;
20use std::path::Path;
21use std::path::PathBuf;
22use std::process::Command;
23
24use anyhow::{Context, bail};
25
26use cargo::core::Package;
27use cargo::core::dependency::Dependency;
28use cargo::sources::registry::IndexSummary;
29use cargo::sources::source::QueryKind;
30use cargo::sources::source::Source;
31use cargo::sources::{PathSource, SourceConfigMap};
32use cargo::util::cache_lock::CacheLockMode;
33use cargo::util::context::GlobalContext;
34use semver::VersionReq;
35
36use walkdir::WalkDir;
37
38pub use cargo::{core::SourceId, util::CargoResult};
40
41#[derive(PartialEq, Eq, Debug)]
43pub struct Crate {
44 name: String,
45 version: Option<String>,
46}
47
48impl Crate {
49 pub fn new(name: String, version: Option<String>) -> Crate {
52 Crate { name, version }
53 }
54}
55
56pub struct Cloner {
58 pub(crate) context: GlobalContext,
60 pub(crate) directory: PathBuf,
63 pub(crate) srcid: SourceId,
65 pub(crate) use_git: bool,
67}
68
69impl Cloner {
70 pub fn builder() -> ClonerBuilder {
74 ClonerBuilder::new()
75 }
76
77 pub fn clone_in_dir(&self, crate_: &Crate) -> CargoResult<()> {
80 let _lock = self
81 .context
82 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
83
84 let mut src = get_source(&self.srcid, &self.context)?;
85
86 self.clone_in(crate_, &self.directory, &mut src)
87 }
88
89 pub fn clone(&self, crates: &[Crate]) -> CargoResult<()> {
92 let _lock = self
93 .context
94 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
95
96 let mut src = get_source(&self.srcid, &self.context)?;
97
98 for crate_ in crates {
99 let mut dest_path = self.directory.clone();
100
101 dest_path.push(&crate_.name);
102
103 self.clone_in(crate_, &dest_path, &mut src)?;
104 }
105
106 Ok(())
107 }
108
109 fn clone_in<'a, T>(&self, crate_: &Crate, dest_path: &Path, src: &mut T) -> CargoResult<()>
110 where
111 T: Source + 'a,
112 {
113 if !dest_path.exists() {
114 fs::create_dir_all(dest_path)?;
115 }
116
117 self.context
118 .shell()
119 .verbose(|s| s.note(format!("Cloning into {:?}", &self.directory)))?;
120
121 let is_empty = dest_path.read_dir()?.next().is_none();
123 if !is_empty {
124 bail!(
125 "destination path '{}' already exists and is not an empty directory.",
126 dest_path.display()
127 );
128 }
129
130 self.clone_single(crate_, dest_path, src)
131 }
132
133 fn clone_single<'a, T>(&self, crate_: &Crate, dest_path: &Path, src: &mut T) -> CargoResult<()>
134 where
135 T: Source + 'a,
136 {
137 let pkg = select_pkg(&self.context, src, &crate_.name, crate_.version.as_deref())?;
138
139 if self.use_git {
140 let repo = &pkg.manifest().metadata().repository;
141
142 if repo.is_none() {
143 bail!(
144 "Cannot clone {} from git repo because it is not specified in package's manifest.",
145 &crate_.name
146 )
147 }
148
149 clone_git_repo(repo.as_ref().unwrap(), dest_path)?;
150 } else {
151 clone_directory(pkg.root(), dest_path)?;
152 }
153
154 Ok(())
155 }
156}
157
158fn get_source<'a>(
159 srcid: &SourceId,
160 context: &'a GlobalContext,
161) -> CargoResult<Box<dyn Source + 'a>> {
162 let mut source = if srcid.is_path() {
163 let path = srcid.url().to_file_path().expect("path must be valid");
164 Box::new(PathSource::new(&path, *srcid, context))
165 } else {
166 let map = SourceConfigMap::new(context)?;
167 map.load(*srcid, &Default::default())?
168 };
169
170 source.invalidate_cache();
171 Ok(source)
172}
173
174fn select_pkg<'a, T>(
175 context: &GlobalContext,
176 src: &mut T,
177 name: &str,
178 vers: Option<&str>,
179) -> CargoResult<Package>
180where
181 T: Source + 'a,
182{
183 let dep = Dependency::parse(name, vers, src.source_id())?;
184 let mut summaries = vec![];
185
186 loop {
187 match src.query(&dep, QueryKind::Exact, &mut |summary| {
188 summaries.push(summary)
189 })? {
190 std::task::Poll::Ready(()) => break,
191 std::task::Poll::Pending => src.block_until_ready()?,
192 }
193 }
194
195 let latest = summaries
196 .iter()
197 .filter_map(|idxs| match idxs {
198 IndexSummary::Candidate(s) => Some(s),
199 _ => None,
200 })
201 .max_by_key(|s| s.version());
202
203 match latest {
204 Some(l) => {
205 context
206 .shell()
207 .note(format!("Downloading {} {}", name, l.version()))?;
208 let pkg = Box::new(src).download_now(l.package_id(), context)?;
209 Ok(pkg)
210 }
211 None => bail!("Package `{}@{}` not found", name, vers.unwrap_or("*.*.*")),
212 }
213}
214
215fn parse_version_req(version: &str) -> CargoResult<String> {
216 let first = version.chars().next();
219
220 if first.is_none() {
221 bail!("Version cannot be empty.")
222 };
223
224 let is_req = "<>=^~".contains(first.unwrap()) || version.contains('*');
225
226 if is_req {
227 let vers = VersionReq::parse(version)
228 .with_context(|| format!("Invalid version requirement: `{version}`."))?;
229 Ok(vers.to_string())
230 } else {
231 Ok(format!("={version}"))
232 }
233}
234
235fn clone_directory(from: &Path, to: &Path) -> CargoResult<()> {
238 if !to.is_dir() {
239 bail!("Not a directory: {}", to.to_string_lossy());
240 }
241 for entry in WalkDir::new(from) {
242 let entry = entry.unwrap();
243 let file_type = entry.file_type();
244 let mut dest_path = to.to_owned();
245 dest_path.push(entry.path().strip_prefix(from).unwrap());
246
247 if entry.file_name() == ".cargo-ok" {
248 continue;
249 }
250
251 if file_type.is_file() {
252 fs::copy(entry.path(), &dest_path)?;
254 } else if file_type.is_dir() {
255 if dest_path == to {
256 continue;
257 }
258 fs::create_dir(&dest_path)?;
259 }
260 }
261
262 Ok(())
263}
264
265fn clone_git_repo(repo: &str, to: &Path) -> CargoResult<()> {
266 let status = Command::new("git")
267 .arg("clone")
268 .arg(repo)
269 .arg(to.to_str().unwrap())
270 .status()
271 .context("Failed to clone from git repo.")?;
272
273 if !status.success() {
274 bail!("Failed to clone from git repo.")
275 }
276
277 Ok(())
278}
279
280pub fn parse_name_and_version(spec: &str) -> CargoResult<Crate> {
282 if !spec.contains('@') {
283 return Ok(Crate::new(spec.to_owned(), None));
284 }
285
286 let mut parts = spec.split('@');
287 let crate_ = parts
288 .next()
289 .context(format!("Crate name missing in `{spec}`."))?;
290 let version = parts
291 .next()
292 .context(format!("Crate version missing in `{spec}`."))?;
293
294 Ok(Crate::new(
295 crate_.to_owned(),
296 Some(parse_version_req(version)?),
297 ))
298}
299
300#[cfg(test)]
301mod tests {
302 use std::{env, path::PathBuf};
303
304 use super::*;
305 use tempfile::tempdir;
306
307 #[test]
308 fn test_parse_version_req() {
309 assert_eq!("=12.4.5", parse_version_req("12.4.5").unwrap());
310 assert_eq!("=12.4.5", parse_version_req("=12.4.5").unwrap());
311 assert_eq!("12.2.*", parse_version_req("12.2.*").unwrap());
312 }
313
314 #[test]
315 fn test_parse_version_req_invalid_req() {
316 assert_eq!(
317 "Invalid version requirement: `=foo`.",
318 parse_version_req("=foo").unwrap_err().to_string()
319 );
320 }
321
322 #[test]
323 fn test_clone_directory() {
324 let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
325 let from = PathBuf::from(manifest_dir).join("tests/data");
326 let to = tempdir().unwrap();
327 let to_path = to.path();
328
329 clone_directory(&from, to_path).unwrap();
330
331 assert!(to_path.join("Cargo.toml").exists());
332 assert!(!to_path.join("cargo-ok").exists());
333 }
334
335 #[test]
336 fn test_clone_repo() {
337 let to = tempdir().unwrap();
338 let to_path = to.path();
339
340 clone_git_repo("https://github.com/janlikar/cargo-clone", to_path).unwrap();
341
342 assert!(to_path.exists());
343 assert!(to_path.join(".git").exists());
344 }
345
346 #[test]
347 fn test_parse_name_and_version() {
348 assert_eq!(
349 parse_name_and_version("foo").unwrap(),
350 Crate::new(String::from("foo"), None)
351 );
352 assert_eq!(
353 parse_name_and_version("foo@1.1.3").unwrap(),
354 Crate::new(String::from("foo"), Some(String::from("=1.1.3")))
355 );
356 assert_eq!(
357 parse_name_and_version("foo@~1.1.3").unwrap(),
358 Crate::new(String::from("foo"), Some(String::from("~1.1.3")))
359 );
360 assert_eq!(
361 parse_name_and_version("foo@1.1.*").unwrap(),
362 Crate::new(String::from("foo"), Some(String::from("1.1.*")))
363 );
364 }
365}