1use std::collections::HashMap;
2use std::ffi::OsStr;
3use std::io::BufRead;
4use std::sync::atomic::AtomicBool;
5use std::thread;
6use std::{fs::File, process::Command};
7
8use anyhow::{anyhow, bail, Context as _, Error, Result};
9use camino::{Utf8Path, Utf8PathBuf};
10use clap::{Arg, ArgAction, ArgMatches, ValueHint};
11use crossbeam::channel::Sender;
12use gix_config::file::init::Options;
13use gix_config::file::Metadata;
14use rayon::{prelude::*, ThreadPoolBuilder};
15
16pub struct GitCache {
17 cache_base_dir: Utf8PathBuf,
18}
19
20pub struct ScpScheme<'a> {
21 _user: &'a str,
22 host: &'a str,
23 path: &'a str,
24}
25
26impl<'a> TryFrom<&'a str> for ScpScheme<'a> {
27 type Error = anyhow::Error;
28
29 fn try_from(value: &'a str) -> std::result::Result<Self, Self::Error> {
30 if let Some((at_pos, colon_pos)) = url_split_scp_scheme(value) {
31 let (_user, rest) = value.split_at(at_pos);
32 let (host, path) = rest.split_at(colon_pos - at_pos);
33
34 let (_, host) = host.split_at(1);
36 let (_, path) = path.split_at(1);
37
38 Ok(ScpScheme { _user, host, path })
39 } else {
40 Err(anyhow!("url does not parse as git scp scheme"))
41 }
42 }
43}
44
45impl GitCache {
46 pub fn new(cache_base_dir: Utf8PathBuf) -> Result<Self, Error> {
47 std::fs::create_dir_all(&cache_base_dir)
48 .with_context(|| format!("creating git cache base directory {cache_base_dir}"))?;
49
50 Ok(Self { cache_base_dir })
51 }
52
53 pub fn cloner(&self) -> GitCacheClonerBuilder {
54 let mut cloner = GitCacheClonerBuilder::default();
55 cloner.cache_base_dir(self.cache_base_dir.clone());
56 cloner
57 }
58
59 pub fn prefetcher(&self) -> GitCachePrefetcherBuilder {
60 let mut prefetcher = GitCachePrefetcherBuilder::default();
61 prefetcher.cache_base_dir(self.cache_base_dir.clone());
62 prefetcher
63 }
64}
65
66#[macro_use]
67extern crate derive_builder;
68
69#[derive(Builder)]
70pub struct GitCacheCloner {
71 cache_base_dir: Utf8PathBuf,
72 #[builder(setter(custom))]
73 repository_url: String,
74 #[builder(default = "true")]
75 cached: bool,
76 #[builder(default)]
77 update: bool,
78 #[builder(default)]
79 target_path: Option<Utf8PathBuf>,
80 #[builder(default)]
81 sparse_paths: Option<Vec<String>>,
82 #[builder(default)]
83 recurse_submodules: Option<Vec<String>>,
84 #[builder(default)]
85 recurse_all_submodules: bool,
86 #[builder(default)]
87 shallow_submodules: bool,
88 #[builder(default)]
89 commit: Option<String>,
90 #[builder(default)]
91 extra_clone_args: Option<Vec<String>>,
92 #[builder(default)]
93 jobs: Option<usize>,
94}
95
96impl GitCacheClonerBuilder {
97 pub fn repository_url(&mut self, url: String) -> &mut Self {
98 if self.cached.is_none() {
99 self.cached = Some(!repo_is_local(&url));
100 }
101 self.repository_url = Some(url);
102 self
103 }
104
105 pub fn do_clone(&mut self) -> Result<(), Error> {
106 self.build()
107 .expect("GitCacheCloner builder correctly set up")
108 .do_clone()
109 }
110 pub fn extra_clone_args_from_matches(&mut self, matches: &ArgMatches) -> &mut Self {
111 self.extra_clone_args(Some(get_pass_through_args(matches)))
112 }
113}
114
115fn repo_is_local(url: &str) -> bool {
122 if let Ok(url) = url::Url::parse(url) {
123 url.scheme() == "file"
124 } else {
125 (url.starts_with("./") || url.starts_with('/'))
126 || (!url_is_scp_scheme(url))
127 || std::path::Path::new(url).exists()
128 }
129}
130
131fn url_split_scp_scheme(url: &str) -> Option<(usize, usize)> {
132 let at = url.find('@');
133 let colon = url.find(':');
134
135 if let Some(colon_pos) = colon {
136 if let Some(at_pos) = at {
137 if at_pos < colon_pos {
138 return Some((at_pos, colon_pos));
139 }
140 }
141 }
142 None
143}
144
145fn url_is_scp_scheme(url: &str) -> bool {
146 url_split_scp_scheme(url).is_some()
147}
148
149impl GitCacheCloner {
150 fn do_clone(&self) -> Result<(), Error> {
151 let repository = &self.repository_url;
152 let wanted_commit = self.commit.as_ref();
153 let target_path;
154
155 if self.cached {
156 let cache_repo = GitCacheRepo::new(&self.cache_base_dir, &self.repository_url);
157 target_path = cache_repo.target_path(self.target_path.as_ref())?;
158
159 let mut lock = cache_repo.lockfile()?;
160 {
161 let _lock = lock.write()?;
162 if !cache_repo.mirror()? {
163 let try_update =
164 wanted_commit.is_some_and(|commit| !cache_repo.has_commit(commit).unwrap());
165
166 if self.update || try_update {
167 println!("git-cache: updating cache for {repository}...");
168 cache_repo.update()?;
169 }
170
171 if let Some(commit) = wanted_commit {
172 if try_update && !cache_repo.has_commit(commit)? {
173 bail!("git-cache: {repository} does not contain commit {commit}");
174 }
175 }
176 }
177 }
178 {
179 let _lock = lock.read()?;
180 cache_repo.clone(target_path.as_str(), self.extra_clone_args.as_ref())?;
181 }
182 } else {
183 target_path =
184 target_path_from_url_maybe(&self.repository_url, self.target_path.as_ref())?;
185
186 direct_clone(
187 &self.repository_url,
188 target_path.as_str(),
189 self.extra_clone_args.as_ref(),
190 )?;
191 }
192
193 let target_repo = GitRepo {
194 path: target_path.clone(),
195 };
196
197 if let Some(commit) = wanted_commit {
198 target_repo.set_config("advice.detachedHead", "false")?;
199 target_repo.checkout(commit)?;
200 }
201 if let Some(sparse_paths) = self.sparse_paths.as_ref() {
202 target_repo.sparse_checkout(sparse_paths)?;
203 }
204
205 if self.recurse_all_submodules || self.recurse_submodules.is_some() {
206 let filter = if !self.recurse_all_submodules {
207 self.recurse_submodules.clone()
208 } else {
209 None
210 };
211
212 let cache = self.cache()?;
213
214 let jobs = self.jobs.unwrap_or(1);
215
216 static RAYON_CONFIGURED: AtomicBool = AtomicBool::new(false);
217
218 if !RAYON_CONFIGURED.swap(true, std::sync::atomic::Ordering::AcqRel) {
219 let _ = ThreadPoolBuilder::new().num_threads(jobs).build_global();
220 }
221
222 target_repo
223 .get_submodules(filter)?
224 .par_iter()
225 .map(|submodule| {
226 println!(
227 "git-cache: cloning {} into {}...",
228 submodule.url, submodule.path
229 );
230 target_repo.clone_submodule(
231 submodule,
232 &cache,
233 self.shallow_submodules,
234 self.update,
235 )
236 })
237 .collect::<Result<Vec<_>, _>>()?;
238 };
239
240 Ok(())
241 }
242
243 pub fn cache(&self) -> Result<GitCache, anyhow::Error> {
244 GitCache::new(self.cache_base_dir.clone())
245 }
246}
247
248#[derive(Builder)]
249#[builder(build_fn(validate = "Self::validate"))]
250pub struct GitCachePrefetcher {
251 cache_base_dir: Utf8PathBuf,
252 repository_urls: Vec<String>,
253 #[builder(default)]
254 update: bool,
255 #[builder(default)]
256 recurse_all_submodules: bool,
257 #[builder(default)]
258 jobs: Option<usize>,
259}
260
261impl GitCachePrefetcherBuilder {
262 pub fn validate(&self) -> Result<(), String> {
263 if let Some(urls) = &self.repository_urls {
264 for url in urls {
265 if repo_is_local(&url) {
266 return Err(format!(
267 "can only cache remote repositories, '{url}' is local"
268 ));
269 }
270 }
271 }
272 Ok(())
273 }
274
275 pub fn do_prefetch(&mut self) -> Result<(), Error> {
276 self.build()
277 .expect("GitCachePrefetcher builder correctly set up")
278 .do_prefetch()
279 }
280}
281
282enum Prefetch {
283 Done,
284 Url(String),
285}
286
287impl GitCachePrefetcher {
288 fn do_prefetch(&self) -> Result<(), Error> {
289 let (sender, receiver) = crossbeam::channel::unbounded::<String>();
290 let (sender2, receiver2) = crossbeam::channel::unbounded::<Prefetch>();
291
292 let mut handles = Vec::new();
293
294 let n_workers = self.jobs.unwrap_or(1);
295
296 for _ in 0..n_workers {
297 let r = receiver.clone();
298 let cache_base_dir = self.cache_base_dir.clone();
299 let recurse = self.recurse_all_submodules;
300 let update = self.update;
301 let sender2 = sender2.clone();
302
303 let handle = thread::spawn(move || {
304 for repository_url in r.iter() {
305 if let Err(e) =
306 prefetch_url(&repository_url, &cache_base_dir, update, recurse, &sender2)
307 {
308 println!("git-cache: error prefetching {repository_url}: {e}");
309 }
310 }
311 });
312 handles.push(handle);
313 }
314
315 for repository_url in &self.repository_urls {
316 let _ = sender2.send(Prefetch::Url(repository_url.clone()));
317 }
318
319 let mut left = 0usize;
320 let mut total = 0;
321 for prefetch in receiver2 {
322 match prefetch {
323 Prefetch::Done => left -= 1,
324 Prefetch::Url(url) => {
325 left += 1;
326 total += 1;
327 let _ = sender.send(url);
328 }
329 }
330 if left == 0 {
331 break;
332 }
333 }
334
335 drop(sender);
337
338 for handle in handles {
340 handle.join().unwrap();
341 }
342
343 println!("git-cache: finished pre-fetching {total} repositories.");
344
345 Ok(())
346 }
347
348 pub fn cache(&self) -> Result<GitCache, anyhow::Error> {
349 GitCache::new(self.cache_base_dir.clone())
350 }
351}
352
353pub struct GitRepo {
354 path: Utf8PathBuf,
355}
356
357pub struct GitCacheRepo {
358 url: String,
359 repo: GitRepo,
360}
361
362impl GitRepo {
363 fn git(&self) -> std::process::Command {
364 let mut command = Command::new("git");
365 command.arg("-C").arg(&self.path);
366
367 command
368 }
369
370 fn is_initialized(&self) -> Result<bool> {
371 Ok(self.path.is_dir()
372 && matches!(
373 self.git()
374 .arg("rev-parse")
375 .arg("--git-dir")
376 .output()?
377 .stdout
378 .as_slice(),
379 b".\n" | b".git\n"
380 ))
381 }
382
383 fn has_commit(&self, commit: &str) -> Result<bool> {
384 Ok(self
385 .git()
386 .arg("cat-file")
387 .arg("-e")
388 .arg(format!("{}^{{commit}}", commit))
389 .status()?
390 .success())
391 }
392
393 fn set_config(&self, key: &str, value: &str) -> Result<()> {
394 self.git()
395 .arg("config")
396 .arg(key)
397 .arg(value)
398 .status()?
399 .success()
400 .true_or(anyhow!("cannot set configuration value"))
401 }
402
403 fn checkout(&self, commit: &str) -> Result<()> {
404 self.git()
405 .arg("checkout")
406 .arg(commit)
407 .status()?
408 .success()
409 .true_or(anyhow!("error checking out commit"))
410 }
411
412 fn submodule_commits(&self) -> Result<HashMap<String, String>> {
413 let output = self.git().arg("submodule").arg("status").output()?;
414
415 let res = output
416 .stdout
417 .lines()
418 .map(|line| line.unwrap())
419 .map(|line| {
420 let commit = line[1..41].to_string();
422 let path = line[42..].to_string();
423 (path, commit)
424 })
425 .collect::<HashMap<String, String>>();
426 Ok(res)
427 }
428
429 fn sparse_checkout<I, S>(&self, sparse_paths: I) -> std::result::Result<(), anyhow::Error>
430 where
431 I: IntoIterator<Item = S>,
432 S: AsRef<OsStr>,
433 {
434 self.git()
435 .arg("sparse-checkout")
436 .arg("set")
437 .arg("--no-cone")
438 .arg("--skip-checks")
439 .args(sparse_paths)
440 .status()?
441 .success()
442 .true_or(anyhow!("error setting up sparse checkout"))
443 }
444
445 fn get_submodules(
446 &self,
447 filter: Option<Vec<String>>,
448 ) -> std::result::Result<Vec<SubmoduleSpec>, anyhow::Error> {
449 use gix_config::File;
450 let mut path = self.path.clone();
451 path.push(".gitmodules");
452
453 if !path.exists() {
454 return Ok(Vec::new());
455 }
456
457 let gitconfig = File::from_path_no_includes(path.into(), gix_config::Source::Api)?;
458 let gitmodules = gitconfig.sections_by_name("submodule");
459
460 if gitmodules.is_none() {
461 return Ok(Vec::new());
462 }
463
464 let submodule_commits = self.submodule_commits()?;
465
466 let mut submodules = Vec::new();
467 for module in gitmodules.unwrap() {
468 let path = module.body().value("path");
469 let url = module.body().value("url");
470 let branch = module.body().value("branch").map(|b| b.to_string());
471
472 if path.is_none() || url.is_none() {
473 eprintln!("git-cache: submodule missing path or url");
474 continue;
475 }
476 let path = path.unwrap().into_owned().to_string();
477 let url = url.unwrap().into_owned().to_string();
478
479 let commit = submodule_commits.get(&path);
480
481 if commit.is_none() {
482 eprintln!("git-cache: could not find submodule commit for path `{path}`");
483 }
484
485 if let Some(filter) = filter.as_ref() {
486 if !filter.contains(&path) {
487 continue;
488 }
489 }
490
491 submodules.push(SubmoduleSpec::new(
492 path,
493 url,
494 commit.unwrap().clone(),
495 branch,
496 ));
497 }
498
499 Ok(submodules)
500 }
501
502 fn clone_submodule(
503 &self,
504 submodule: &SubmoduleSpec,
505 cache: &GitCache,
506 shallow_submodules: bool,
507 update: bool,
508 ) -> std::result::Result<(), anyhow::Error> {
509 let submodule_path = self.path.join(&submodule.path);
510
511 let mut cloner = cache.cloner();
512
513 cloner
514 .repository_url(submodule.url.clone())
515 .target_path(Some(submodule_path))
516 .recurse_all_submodules(true)
517 .shallow_submodules(shallow_submodules)
518 .commit(Some(submodule.commit.clone()))
519 .update(update);
520
521 cloner.do_clone()?;
526
527 self.init_submodule(&submodule.path)?;
528
529 Ok(())
530 }
531
532 fn init_submodule(&self, path: &str) -> std::result::Result<(), anyhow::Error> {
533 self.git()
534 .arg("submodule")
535 .arg("init")
536 .arg("--")
537 .arg(path)
538 .status()?
539 .success()
540 .true_or(anyhow!("error initializing submodule"))
541 }
542}
543
544impl GitCacheRepo {
545 pub fn new(base_path: &Utf8Path, url: &str) -> Self {
546 let mut path = base_path.to_path_buf();
547 path.push(Self::repo_path_from_url(url));
548 Self {
549 repo: GitRepo { path },
550 url: url.to_string(),
551 }
552 }
553
554 fn mirror(&self) -> Result<bool> {
555 if !self.repo.is_initialized()? {
556 println!("git-cache: cloning {} into cache...", self.url);
557 std::fs::create_dir_all(&self.repo.path)?;
558 Command::new("git")
559 .arg("clone")
560 .arg("--mirror")
561 .arg("--")
562 .arg(&self.url)
563 .arg(&self.repo.path)
564 .status()?
565 .success()
566 .true_or(anyhow!("error mirroring repository"))?;
567
568 Ok(true)
569 } else {
570 Ok(false)
571 }
572 }
573
574 fn update(&self) -> Result<()> {
575 self.repo
576 .git()
577 .arg("remote")
578 .arg("update")
579 .status()?
580 .success()
581 .true_or(anyhow!("error updating repository"))
582 }
583
584 fn repo_path_from_url(url: &str) -> Utf8PathBuf {
587 let mut path = if let Ok(url) = url::Url::parse(url) {
588 assert!(url.scheme() != "file");
589 let (_, path) = url.path().split_at(1);
590 Utf8PathBuf::from(url.host_str().unwrap()).join(path)
591 } else if let Ok(scp_scheme) = ScpScheme::try_from(url) {
592 Utf8PathBuf::from(scp_scheme.host).join(scp_scheme.path)
593 } else {
594 unreachable!("shouldn't be here");
595 };
596 path.set_extension("git");
597
598 path
599 }
600
601 fn clone(&self, target_path: &str, pass_through_args: Option<&Vec<String>>) -> Result<()> {
602 direct_clone(self.repo.path.as_str(), target_path, pass_through_args)?;
603
604 Command::new("git")
605 .arg("-C")
606 .arg(target_path)
607 .arg("remote")
608 .arg("set-url")
609 .arg("origin")
610 .arg(&self.url)
611 .status()?
612 .success()
613 .true_or(anyhow!("error updating remote url"))?;
614 Ok(())
615 }
616
617 pub fn target_path(&self, target_path: Option<&Utf8PathBuf>) -> Result<Utf8PathBuf> {
618 target_path_from_url_maybe(&self.url, target_path)
619 }
620
621 fn has_commit(&self, commit: &str) -> std::result::Result<bool, anyhow::Error> {
626 self.repo.has_commit(commit)
627 }
628
629 fn lockfile(&self) -> Result<fd_lock::RwLock<File>> {
630 let base_path = self.repo.path.parent().unwrap();
631 std::fs::create_dir_all(&base_path)
632 .with_context(|| format!("creating repo base path '{base_path}'"))?;
633
634 let lock_path = self.repo.path.with_extension("git.lock");
635 Ok(fd_lock::RwLock::new(
636 std::fs::File::create(&lock_path)
637 .with_context(|| format!("creating lock file \"{lock_path}\""))?,
638 ))
639 }
640
641 fn get_submodules(&self) -> std::result::Result<Vec<String>, anyhow::Error> {
642 let output = self
643 .repo
644 .git()
645 .arg("show")
646 .arg("HEAD:.gitmodules")
647 .output()?;
648
649 let data = output.stdout;
650 let gitconfig =
651 gix_config::File::from_bytes_no_includes(&data, Metadata::api(), Options::default())?;
652 let gitmodules = gitconfig.sections_by_name("submodule");
653
654 if let Some(gitmodules) = gitmodules {
655 Ok(gitmodules
656 .filter_map(|submodule| submodule.body().value("url").map(|cow| cow.to_string()))
657 .collect())
658 } else {
659 return Ok(vec![]);
660 }
661 }
662}
663
664fn direct_clone(
665 repo: &str,
666 target_path: &str,
667 pass_through_args: Option<&Vec<String>>,
668) -> Result<(), Error> {
669 let mut clone_cmd = Command::new("git");
670 clone_cmd.arg("clone").arg("--shared");
671 if let Some(args) = pass_through_args {
672 clone_cmd.args(args);
673 }
674 clone_cmd
675 .arg("--")
676 .arg(repo)
677 .arg(target_path)
678 .status()?
679 .success()
680 .true_or(anyhow!("cloning failed"))?;
681 Ok(())
682}
683
684fn prefetch_url(
685 repository_url: &str,
686 cache_base_dir: &Utf8Path,
687 update: bool,
688 recurse: bool,
689 sender: &Sender<Prefetch>,
690) -> Result<(), Error> {
691 scopeguard::defer! {
692 let _ = sender.send(Prefetch::Done);
693 }
694
695 let cache_repo = GitCacheRepo::new(cache_base_dir, repository_url);
696
697 let mut lock = cache_repo.lockfile()?;
698 {
699 let _lock = lock.write()?;
700 if !cache_repo.mirror()? {
701 if update {
702 println!("git-cache: updating cache for {repository_url}...");
703 cache_repo.update()?;
704 }
705 }
706 }
707
708 if recurse {
709 let _lock = lock.read()?;
710 for url in cache_repo.get_submodules()? {
711 println!("git-cache: {repository_url} getting submodule: {url}");
712 let _ = sender.send(Prefetch::Url(url));
713 }
714 }
715
716 Ok(())
717}
718
719fn target_path_from_url_maybe(
720 url: &str,
721 target_path: Option<&Utf8PathBuf>,
722) -> Result<Utf8PathBuf, Error> {
723 target_path.map(shellexpand::tilde);
724
725 let url_path = Utf8PathBuf::from(url);
726 let url_path_filename = Utf8PathBuf::from(url_path.file_name().unwrap());
727 let target_path = target_path.unwrap_or(&url_path_filename);
728
729 if !target_path.is_clone_target()? {
730 return Err(anyhow!(
731 "fatal: destination path '{target_path}' already exists and is not an empty directory."
732 ));
733 }
734
735 Ok(target_path.clone())
736}
737
738pub fn clap_git_cache_dir_arg() -> Arg {
739 Arg::new("git_cache_dir")
740 .short('c')
741 .long("cache-dir")
742 .help("git cache base directory")
743 .required(false)
744 .default_value("~/.gitcache")
745 .value_parser(clap::value_parser!(Utf8PathBuf))
746 .value_hint(ValueHint::DirPath)
747 .env("GIT_CACHE_DIR")
748 .num_args(1)
749}
750
751pub fn clap_clone_command(name: &'static str) -> clap::Command {
752 use clap::Command;
753 Command::new(name)
754 .about("clone repository")
755 .arg(
756 Arg::new("repository")
757 .help("repository to clone")
758 .required(true),
759 )
760 .arg(
761 Arg::new("target_path")
762 .help("target path")
763 .required(false)
764 .value_parser(clap::value_parser!(Utf8PathBuf))
765 .value_hint(ValueHint::DirPath),
766 )
767 .arg(
768 Arg::new("update")
769 .short('U')
770 .long("update")
771 .action(ArgAction::SetTrue)
772 .help("force update of cached repo"),
773 )
774 .arg(
775 Arg::new("commit")
776 .long("commit")
777 .value_name("HASH")
778 .conflicts_with("branch")
779 .help("check out specific commit"),
780 )
781 .arg(
782 Arg::new("sparse-add")
783 .long("sparse-add")
784 .value_name("PATH")
785 .conflicts_with("branch")
786 .action(ArgAction::Append)
787 .help("do a sparse checkout, keep PATH"),
788 )
789 .arg(
790 Arg::new("recurse-submodules")
791 .long("recurse-submodules")
792 .value_name("pathspec")
793 .action(ArgAction::Append)
794 .num_args(0..=1)
795 .require_equals(true)
796 .help("recursively clone submodules"),
797 )
798 .arg(
799 Arg::new("shallow-submodules")
800 .long("shallow-submodules")
801 .action(ArgAction::SetTrue)
802 .overrides_with("no-shallow-submodules")
803 .help("shallow-clone submodules"),
804 )
805 .arg(
806 Arg::new("no-shallow-submodules")
807 .long("no-shallow-submodules")
808 .action(ArgAction::SetTrue)
809 .overrides_with("shallow-submodules")
810 .help("don't shallow-clone submodules"),
811 )
812 .arg(
813 Arg::new("jobs")
814 .long("jobs")
815 .short('j')
816 .help("The number of submodules fetched at the same time.")
817 .num_args(1)
818 .value_parser(clap::value_parser!(usize)),
819 )
820 .args(pass_through_args())
821 .after_help(
822 "These regular \"git clone\" options are passed through:\n
823 [--template=<template-directory>]
824 [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
825 [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]
826 [--dissociate] [--separate-git-dir <git-dir>]
827 [--depth <depth>] [--[no-]single-branch] [--no-tags]
828 [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]
829 [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]
830 [--filter=<filter> [--also-filter-submodules]]",
831 )
832}
833
834pub fn clap_prefetch_command(name: &'static str) -> clap::Command {
835 use clap::Command;
836 Command::new(name)
837 .about("pre-fetch repositories into the cache")
838 .arg(
839 Arg::new("repositories")
840 .help("repositories to prefetch")
841 .required(true)
842 .num_args(1..),
843 )
844 .arg(
845 Arg::new("update")
846 .short('U')
847 .long("update")
848 .action(ArgAction::SetTrue)
849 .help("force update of already cached repo(s)"),
850 )
851 .arg(
852 Arg::new("recurse-submodules")
853 .long("recurse-submodules")
854 .short('r')
855 .action(ArgAction::SetTrue)
856 .help("recursively prefetch submodules"),
857 )
858 .arg(
859 Arg::new("jobs")
860 .long("jobs")
861 .short('j')
862 .help("The number of reposititories fetched at the same time.")
863 .num_args(1)
864 .value_parser(clap::value_parser!(usize)),
865 )
866}
867
868fn pass_through_args() -> Vec<Arg> {
869 let mut args = Vec::new();
870
871 for (short, long) in [
873 ('l', "local"),
874 ('q', "quiet"),
876 ('s', "shared"),
877 ('v', "verbose"),
878 ]
879 .into_iter()
880 {
881 args.push(
882 Arg::new(long)
883 .short(short)
884 .long(long)
885 .hide(true)
886 .action(ArgAction::SetTrue),
887 );
888 }
889
890 args.push(
892 Arg::new("no-checkout")
893 .short('n')
894 .long("no-checkout")
895 .hide(true)
896 .num_args(0)
897 .default_value_if("commit", clap::builder::ArgPredicate::IsPresent, "true"),
898 );
899
900 args.push(
901 Arg::new("sparse")
902 .long("sparse")
903 .hide(true)
904 .num_args(0)
905 .default_value_if("sparse-add", clap::builder::ArgPredicate::IsPresent, "true"),
906 );
907
908 for (short, long) in [
910 ('b', "branch"),
911 ('c', "config"),
912 ('o', "origin"),
913 ('u', "upload-pack"),
914 ]
915 .into_iter()
916 {
917 args.push(
918 Arg::new(long)
919 .short(short)
920 .long(long)
921 .num_args(1)
922 .hide(true),
923 );
924 }
925
926 for id in [
928 "also-filter-submodules",
929 "bare",
930 "dissociate",
931 "mirror",
932 "no-hardlinks",
933 "no-reject-shallow",
934 "no-remote-submodules",
935 "no-single-branch",
936 "no-tags",
937 "reject-shallow",
938 "remote-submodules",
939 "single-branch",
940 ]
941 .into_iter()
942 {
943 args.push(Arg::new(id).long(id).action(ArgAction::SetTrue).hide(true));
944 }
945
946 for id in [
948 "bundle-uri",
949 "depth",
950 "filter",
951 "reference",
952 "reference-if-able",
953 "separate-git-dir",
954 "shallow-exclude",
955 "shallow-since",
956 "template",
957 ]
958 .into_iter()
959 {
960 args.push(Arg::new(id).long(id).num_args(1).hide(true));
961 }
962
963 args
964}
965
966fn get_pass_through_args(matches: &ArgMatches) -> Vec<String> {
967 let mut args = Vec::new();
968 for id in [
970 "local",
971 "no-checkout",
972 "quiet",
973 "shared",
974 "verbose",
975 "also-filter-submodules",
976 "bare",
977 "dissociate",
978 "mirror",
979 "no-hardlinks",
980 "no-reject-shallow",
981 "no-remote-submodules",
982 "no-single-branch",
983 "no-tags",
984 "reject-shallow",
985 "remote-submodules",
986 "single-branch",
987 "sparse",
988 ]
989 .into_iter()
990 {
991 if matches.get_flag(id) {
992 args.push(format!("--{id}"));
993 }
994 }
995
996 for id in [
998 "branch",
999 "bundle-uri",
1000 "config",
1001 "depth",
1002 "filter",
1003 "origin",
1004 "reference",
1005 "reference-if-able",
1006 "separate-git-dir",
1007 "shallow-exclude",
1008 "shallow-since",
1009 "template",
1010 "upload-pack",
1011 ]
1012 .into_iter()
1013 {
1014 if let Some(occurrences) = matches.get_occurrences::<String>(id) {
1015 for occurrence in occurrences.flatten() {
1016 args.push(format!("--{id}"));
1017 args.push(occurrence.clone());
1018 }
1019 }
1020 }
1021
1022 args
1023}
1024
1025trait CanCloneInto {
1026 fn is_clone_target(&self) -> Result<bool, Error>;
1027}
1028
1029impl CanCloneInto for camino::Utf8Path {
1030 fn is_clone_target(&self) -> Result<bool, Error> {
1031 Ok((!self.exists()) || (self.is_dir() && { self.read_dir()?.next().is_none() }))
1032 }
1033}
1034
1035trait TrueOr {
1036 fn true_or(self, error: Error) -> Result<()>;
1037}
1038
1039impl TrueOr for bool {
1040 fn true_or(self, error: Error) -> Result<()> {
1041 if self {
1042 Ok(())
1043 } else {
1044 Err(error)
1045 }
1046 }
1047}
1048
1049#[derive(Debug, Clone)]
1050struct SubmoduleSpec {
1051 path: String,
1052 url: String,
1053 #[allow(dead_code)]
1054 branch: Option<String>,
1055 commit: String,
1056}
1057
1058impl SubmoduleSpec {
1059 pub fn new(path: String, url: String, commit: String, branch: Option<String>) -> Self {
1060 Self {
1061 path,
1062 url,
1063 commit,
1064 branch,
1065 }
1066 }
1067}