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 .args(sparse_paths)
438 .status()?
439 .success()
440 .true_or(anyhow!("error setting up sparse checkout"))
441 }
442
443 fn get_submodules(
444 &self,
445 filter: Option<Vec<String>>,
446 ) -> std::result::Result<Vec<SubmoduleSpec>, anyhow::Error> {
447 use gix_config::File;
448 let mut path = self.path.clone();
449 path.push(".gitmodules");
450
451 if !path.exists() {
452 return Ok(Vec::new());
453 }
454
455 let gitconfig = File::from_path_no_includes(path.into(), gix_config::Source::Api)?;
456 let gitmodules = gitconfig.sections_by_name("submodule");
457
458 if gitmodules.is_none() {
459 return Ok(Vec::new());
460 }
461
462 let submodule_commits = self.submodule_commits()?;
463
464 let mut submodules = Vec::new();
465 for module in gitmodules.unwrap() {
466 let path = module.body().value("path");
467 let url = module.body().value("url");
468 let branch = module.body().value("branch").map(|b| b.to_string());
469
470 if path.is_none() || url.is_none() {
471 eprintln!("git-cache: submodule missing path or url");
472 continue;
473 }
474 let path = path.unwrap().into_owned().to_string();
475 let url = url.unwrap().into_owned().to_string();
476
477 let commit = submodule_commits.get(&path);
478
479 if commit.is_none() {
480 eprintln!("git-cache: could not find submodule commit for path `{path}`");
481 }
482
483 if let Some(filter) = filter.as_ref() {
484 if !filter.contains(&path) {
485 continue;
486 }
487 }
488
489 submodules.push(SubmoduleSpec::new(
490 path,
491 url,
492 commit.unwrap().clone(),
493 branch,
494 ));
495 }
496
497 Ok(submodules)
498 }
499
500 fn clone_submodule(
501 &self,
502 submodule: &SubmoduleSpec,
503 cache: &GitCache,
504 shallow_submodules: bool,
505 update: bool,
506 ) -> std::result::Result<(), anyhow::Error> {
507 let submodule_path = self.path.join(&submodule.path);
508
509 let mut cloner = cache.cloner();
510
511 cloner
512 .repository_url(submodule.url.clone())
513 .target_path(Some(submodule_path))
514 .recurse_all_submodules(true)
515 .shallow_submodules(shallow_submodules)
516 .commit(Some(submodule.commit.clone()))
517 .update(update);
518
519 cloner.do_clone()?;
524
525 self.init_submodule(&submodule.path)?;
526
527 Ok(())
528 }
529
530 fn init_submodule(&self, path: &str) -> std::result::Result<(), anyhow::Error> {
531 self.git()
532 .arg("submodule")
533 .arg("init")
534 .arg("--")
535 .arg(path)
536 .status()?
537 .success()
538 .true_or(anyhow!("error initializing submodule"))
539 }
540}
541
542impl GitCacheRepo {
543 pub fn new(base_path: &Utf8Path, url: &str) -> Self {
544 let mut path = base_path.to_path_buf();
545 path.push(Self::repo_path_from_url(url));
546 Self {
547 repo: GitRepo { path },
548 url: url.to_string(),
549 }
550 }
551
552 fn mirror(&self) -> Result<bool> {
553 if !self.repo.is_initialized()? {
554 println!("git-cache: cloning {} into cache...", self.url);
555 std::fs::create_dir_all(&self.repo.path)?;
556 Command::new("git")
557 .arg("clone")
558 .arg("--mirror")
559 .arg("--")
560 .arg(&self.url)
561 .arg(&self.repo.path)
562 .status()?
563 .success()
564 .true_or(anyhow!("error mirroring repository"))?;
565
566 Ok(true)
567 } else {
568 Ok(false)
569 }
570 }
571
572 fn update(&self) -> Result<()> {
573 self.repo
574 .git()
575 .arg("remote")
576 .arg("update")
577 .status()?
578 .success()
579 .true_or(anyhow!("error updating repository"))
580 }
581
582 fn repo_path_from_url(url: &str) -> Utf8PathBuf {
585 let mut path = if let Ok(url) = url::Url::parse(url) {
586 assert!(url.scheme() != "file");
587 let (_, path) = url.path().split_at(1);
588 Utf8PathBuf::from(url.host_str().unwrap()).join(path)
589 } else if let Ok(scp_scheme) = ScpScheme::try_from(url) {
590 Utf8PathBuf::from(scp_scheme.host).join(scp_scheme.path)
591 } else {
592 unreachable!("shouldn't be here");
593 };
594 path.set_extension("git");
595
596 path
597 }
598
599 fn clone(&self, target_path: &str, pass_through_args: Option<&Vec<String>>) -> Result<()> {
600 direct_clone(self.repo.path.as_str(), target_path, pass_through_args)?;
601
602 Command::new("git")
603 .arg("-C")
604 .arg(target_path)
605 .arg("remote")
606 .arg("set-url")
607 .arg("origin")
608 .arg(&self.url)
609 .status()?
610 .success()
611 .true_or(anyhow!("error updating remote url"))?;
612 Ok(())
613 }
614
615 pub fn target_path(&self, target_path: Option<&Utf8PathBuf>) -> Result<Utf8PathBuf> {
616 target_path_from_url_maybe(&self.url, target_path)
617 }
618
619 fn has_commit(&self, commit: &str) -> std::result::Result<bool, anyhow::Error> {
624 self.repo.has_commit(commit)
625 }
626
627 fn lockfile(&self) -> Result<fd_lock::RwLock<File>> {
628 let base_path = self.repo.path.parent().unwrap();
629 std::fs::create_dir_all(&base_path)
630 .with_context(|| format!("creating repo base path '{base_path}'"))?;
631
632 let lock_path = self.repo.path.with_extension("git.lock");
633 Ok(fd_lock::RwLock::new(
634 std::fs::File::create(&lock_path)
635 .with_context(|| format!("creating lock file \"{lock_path}\""))?,
636 ))
637 }
638
639 fn get_submodules(&self) -> std::result::Result<Vec<String>, anyhow::Error> {
640 let output = self
641 .repo
642 .git()
643 .arg("show")
644 .arg("HEAD:.gitmodules")
645 .output()?;
646
647 let data = output.stdout;
648 let gitconfig =
649 gix_config::File::from_bytes_no_includes(&data, Metadata::api(), Options::default())?;
650 let gitmodules = gitconfig.sections_by_name("submodule");
651
652 if let Some(gitmodules) = gitmodules {
653 Ok(gitmodules
654 .filter_map(|submodule| submodule.body().value("url").map(|cow| cow.to_string()))
655 .collect())
656 } else {
657 return Ok(vec![]);
658 }
659 }
660}
661
662fn direct_clone(
663 repo: &str,
664 target_path: &str,
665 pass_through_args: Option<&Vec<String>>,
666) -> Result<(), Error> {
667 let mut clone_cmd = Command::new("git");
668 clone_cmd.arg("clone").arg("--shared");
669 if let Some(args) = pass_through_args {
670 clone_cmd.args(args);
671 }
672 clone_cmd
673 .arg("--")
674 .arg(repo)
675 .arg(target_path)
676 .status()?
677 .success()
678 .true_or(anyhow!("cloning failed"))?;
679 Ok(())
680}
681
682fn prefetch_url(
683 repository_url: &str,
684 cache_base_dir: &Utf8Path,
685 update: bool,
686 recurse: bool,
687 sender: &Sender<Prefetch>,
688) -> Result<(), Error> {
689 scopeguard::defer! {
690 let _ = sender.send(Prefetch::Done);
691 }
692
693 let cache_repo = GitCacheRepo::new(cache_base_dir, repository_url);
694
695 let mut lock = cache_repo.lockfile()?;
696 {
697 let _lock = lock.write()?;
698 if !cache_repo.mirror()? {
699 if update {
700 println!("git-cache: updating cache for {repository_url}...");
701 cache_repo.update()?;
702 }
703 }
704 }
705
706 if recurse {
707 let _lock = lock.read()?;
708 for url in cache_repo.get_submodules()? {
709 println!("git-cache: {repository_url} getting submodule: {url}");
710 let _ = sender.send(Prefetch::Url(url));
711 }
712 }
713
714 Ok(())
715}
716
717fn target_path_from_url_maybe(
718 url: &str,
719 target_path: Option<&Utf8PathBuf>,
720) -> Result<Utf8PathBuf, Error> {
721 target_path.map(shellexpand::tilde);
722
723 let url_path = Utf8PathBuf::from(url);
724 let url_path_filename = Utf8PathBuf::from(url_path.file_name().unwrap());
725 let target_path = target_path.unwrap_or(&url_path_filename);
726
727 if !target_path.is_clone_target()? {
728 return Err(anyhow!(
729 "fatal: destination path '{target_path}' already exists and is not an empty directory."
730 ));
731 }
732
733 Ok(target_path.clone())
734}
735
736pub fn clap_git_cache_dir_arg() -> Arg {
737 Arg::new("git_cache_dir")
738 .short('c')
739 .long("cache-dir")
740 .help("git cache base directory")
741 .required(false)
742 .default_value("~/.gitcache")
743 .value_parser(clap::value_parser!(Utf8PathBuf))
744 .value_hint(ValueHint::DirPath)
745 .env("GIT_CACHE_DIR")
746 .num_args(1)
747}
748
749pub fn clap_clone_command(name: &'static str) -> clap::Command {
750 use clap::Command;
751 Command::new(name)
752 .about("clone repository")
753 .arg(
754 Arg::new("repository")
755 .help("repository to clone")
756 .required(true),
757 )
758 .arg(
759 Arg::new("target_path")
760 .help("target path")
761 .required(false)
762 .value_parser(clap::value_parser!(Utf8PathBuf))
763 .value_hint(ValueHint::DirPath),
764 )
765 .arg(
766 Arg::new("update")
767 .short('U')
768 .long("update")
769 .action(ArgAction::SetTrue)
770 .help("force update of cached repo"),
771 )
772 .arg(
773 Arg::new("commit")
774 .long("commit")
775 .value_name("HASH")
776 .conflicts_with("branch")
777 .help("check out specific commit"),
778 )
779 .arg(
780 Arg::new("sparse-add")
781 .long("sparse-add")
782 .value_name("PATH")
783 .conflicts_with("branch")
784 .action(ArgAction::Append)
785 .help("do a sparse checkout, keep PATH"),
786 )
787 .arg(
788 Arg::new("recurse-submodules")
789 .long("recurse-submodules")
790 .value_name("pathspec")
791 .action(ArgAction::Append)
792 .num_args(0..=1)
793 .require_equals(true)
794 .help("recursively clone submodules"),
795 )
796 .arg(
797 Arg::new("shallow-submodules")
798 .long("shallow-submodules")
799 .action(ArgAction::SetTrue)
800 .overrides_with("no-shallow-submodules")
801 .help("shallow-clone submodules"),
802 )
803 .arg(
804 Arg::new("no-shallow-submodules")
805 .long("no-shallow-submodules")
806 .action(ArgAction::SetTrue)
807 .overrides_with("shallow-submodules")
808 .help("don't shallow-clone submodules"),
809 )
810 .arg(
811 Arg::new("jobs")
812 .long("jobs")
813 .short('j')
814 .help("The number of submodules fetched at the same time.")
815 .num_args(1)
816 .value_parser(clap::value_parser!(usize)),
817 )
818 .args(pass_through_args())
819 .after_help(
820 "These regular \"git clone\" options are passed through:\n
821 [--template=<template-directory>]
822 [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
823 [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]
824 [--dissociate] [--separate-git-dir <git-dir>]
825 [--depth <depth>] [--[no-]single-branch] [--no-tags]
826 [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]
827 [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]
828 [--filter=<filter> [--also-filter-submodules]]",
829 )
830}
831
832pub fn clap_prefetch_command(name: &'static str) -> clap::Command {
833 use clap::Command;
834 Command::new(name)
835 .about("pre-fetch repositories into the cache")
836 .arg(
837 Arg::new("repositories")
838 .help("repositories to prefetch")
839 .required(true)
840 .num_args(1..),
841 )
842 .arg(
843 Arg::new("update")
844 .short('U')
845 .long("update")
846 .action(ArgAction::SetTrue)
847 .help("force update of already cached repo(s)"),
848 )
849 .arg(
850 Arg::new("recurse-submodules")
851 .long("recurse-submodules")
852 .short('r')
853 .action(ArgAction::SetTrue)
854 .help("recursively prefetch submodules"),
855 )
856 .arg(
857 Arg::new("jobs")
858 .long("jobs")
859 .short('j')
860 .help("The number of reposititories fetched at the same time.")
861 .num_args(1)
862 .value_parser(clap::value_parser!(usize)),
863 )
864}
865
866fn pass_through_args() -> Vec<Arg> {
867 let mut args = Vec::new();
868
869 for (short, long) in [
871 ('l', "local"),
872 ('q', "quiet"),
874 ('s', "shared"),
875 ('v', "verbose"),
876 ]
877 .into_iter()
878 {
879 args.push(
880 Arg::new(long)
881 .short(short)
882 .long(long)
883 .hide(true)
884 .action(ArgAction::SetTrue),
885 );
886 }
887
888 args.push(
890 Arg::new("no-checkout")
891 .short('n')
892 .long("no-checkout")
893 .hide(true)
894 .num_args(0)
895 .default_value_if("commit", clap::builder::ArgPredicate::IsPresent, "true"),
896 );
897
898 args.push(
899 Arg::new("sparse")
900 .long("sparse")
901 .hide(true)
902 .num_args(0)
903 .default_value_if("sparse-add", clap::builder::ArgPredicate::IsPresent, "true"),
904 );
905
906 for (short, long) in [
908 ('b', "branch"),
909 ('c', "config"),
910 ('o', "origin"),
911 ('u', "upload-pack"),
912 ]
913 .into_iter()
914 {
915 args.push(
916 Arg::new(long)
917 .short(short)
918 .long(long)
919 .num_args(1)
920 .hide(true),
921 );
922 }
923
924 for id in [
926 "also-filter-submodules",
927 "bare",
928 "dissociate",
929 "mirror",
930 "no-hardlinks",
931 "no-reject-shallow",
932 "no-remote-submodules",
933 "no-single-branch",
934 "no-tags",
935 "reject-shallow",
936 "remote-submodules",
937 "single-branch",
938 ]
939 .into_iter()
940 {
941 args.push(Arg::new(id).long(id).action(ArgAction::SetTrue).hide(true));
942 }
943
944 for id in [
946 "bundle-uri",
947 "depth",
948 "filter",
949 "reference",
950 "reference-if-able",
951 "separate-git-dir",
952 "shallow-exclude",
953 "shallow-since",
954 "template",
955 ]
956 .into_iter()
957 {
958 args.push(Arg::new(id).long(id).num_args(1).hide(true));
959 }
960
961 args
962}
963
964fn get_pass_through_args(matches: &ArgMatches) -> Vec<String> {
965 let mut args = Vec::new();
966 for id in [
968 "local",
969 "no-checkout",
970 "quiet",
971 "shared",
972 "verbose",
973 "also-filter-submodules",
974 "bare",
975 "dissociate",
976 "mirror",
977 "no-hardlinks",
978 "no-reject-shallow",
979 "no-remote-submodules",
980 "no-single-branch",
981 "no-tags",
982 "reject-shallow",
983 "remote-submodules",
984 "single-branch",
985 "sparse",
986 ]
987 .into_iter()
988 {
989 if matches.get_flag(id) {
990 args.push(format!("--{id}"));
991 }
992 }
993
994 for id in [
996 "branch",
997 "bundle-uri",
998 "config",
999 "depth",
1000 "filter",
1001 "origin",
1002 "reference",
1003 "reference-if-able",
1004 "separate-git-dir",
1005 "shallow-exclude",
1006 "shallow-since",
1007 "template",
1008 "upload-pack",
1009 ]
1010 .into_iter()
1011 {
1012 if let Some(occurrences) = matches.get_occurrences::<String>(id) {
1013 for occurrence in occurrences.flatten() {
1014 args.push(format!("--{id}"));
1015 args.push(occurrence.clone());
1016 }
1017 }
1018 }
1019
1020 args
1021}
1022
1023trait CanCloneInto {
1024 fn is_clone_target(&self) -> Result<bool, Error>;
1025}
1026
1027impl CanCloneInto for camino::Utf8Path {
1028 fn is_clone_target(&self) -> Result<bool, Error> {
1029 Ok((!self.exists()) || (self.is_dir() && { self.read_dir()?.next().is_none() }))
1030 }
1031}
1032
1033trait TrueOr {
1034 fn true_or(self, error: Error) -> Result<()>;
1035}
1036
1037impl TrueOr for bool {
1038 fn true_or(self, error: Error) -> Result<()> {
1039 if self {
1040 Ok(())
1041 } else {
1042 Err(error)
1043 }
1044 }
1045}
1046
1047#[derive(Debug, Clone)]
1048struct SubmoduleSpec {
1049 path: String,
1050 url: String,
1051 #[allow(dead_code)]
1052 branch: Option<String>,
1053 commit: String,
1054}
1055
1056impl SubmoduleSpec {
1057 pub fn new(path: String, url: String, commit: String, branch: Option<String>) -> Self {
1058 Self {
1059 path,
1060 url,
1061 commit,
1062 branch,
1063 }
1064 }
1065}