cargo_scaffold/
git.rs

1use anyhow::{anyhow, Result};
2use console::{Emoji, Style};
3use std::path::Path;
4
5pub(crate) fn clone(
6    repository: &str,
7    reference_opt: Option<&str>,
8    target_dir: &Path,
9    private_key_path: Option<&Path>,
10) -> Result<()> {
11    let cyan = Style::new().cyan();
12    println!(
13        "{} {}",
14        Emoji("🔄", ""),
15        cyan.apply_to("Cloning repository…"),
16    );
17
18    let mut auth = auth_git2::GitAuthenticator::default();
19    if let Some(private_key_path) = private_key_path {
20        auth = auth.add_ssh_key_from_file(private_key_path, None)
21    }
22
23    let git_config = git2::Config::open_default()
24        .map_err(|e| anyhow!(e).context("Opening git configuration"))?;
25
26    let mut fetch_options = git2::FetchOptions::new();
27
28    // Add credentials callback.
29    let mut callbacks = git2::RemoteCallbacks::new();
30    callbacks.credentials(auth.credentials(&git_config));
31    fetch_options.remote_callbacks(callbacks);
32
33    if reference_opt.is_some() {
34        fetch_options.download_tags(git2::AutotagOption::All);
35    }
36
37    // we don't need to download the entire history
38    fetch_options.depth(1);
39
40    // Prepare builder.
41    let mut builder = git2::build::RepoBuilder::new();
42    builder.fetch_options(fetch_options);
43
44    // Clone the project.
45    let repo = builder.clone(repository, target_dir)?;
46
47    // Either a git tag, commit
48    if let Some(git_reference) = reference_opt {
49        match repo.revparse_ext(git_reference) {
50            Ok((obj, reference)) => {
51                repo.checkout_tree(&obj, None)?;
52                match reference {
53                    // tagref is an actual reference like branches or tags
54                    Some(reporef) => repo.set_head(reporef.name().expect("tag has a name; qed")),
55                    // this is a commit, not a reference
56                    None => repo.set_head_detached(obj.id()),
57                }?;
58            }
59            Err(_) => {
60                // It might be a branch
61                std::fs::remove_dir_all(target_dir)?;
62                builder.branch(git_reference);
63                let _repo = builder.clone(repository, target_dir)?;
64            }
65        }
66    }
67
68    Ok(())
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use tempfile::tempdir;
75
76    #[test]
77    fn clone_http() {
78        let template_path = "https://github.com/http-rs/surf.git";
79        let tmp_dir = tempdir().unwrap();
80        clone(template_path, None, tmp_dir.path(), None).unwrap();
81    }
82
83    #[test]
84    fn clone_http_commit() {
85        let commit = Some("8f0039488b3877ca59592900bc7ad645a83e2886");
86        let template_path = "https://github.com/http-rs/surf.git";
87        let tmp_dir = tempdir().unwrap();
88        clone(template_path, commit, tmp_dir.path(), None).unwrap();
89    }
90
91    #[test]
92    fn clone_http_branch() {
93        let branch = Some("main");
94        let template_path = "https://github.com/apollographql/router.git";
95        let tmp_dir = tempdir().unwrap();
96        clone(template_path, branch, tmp_dir.path(), None).unwrap();
97    }
98
99    #[test]
100    // warn: your ssh key must be in pem format
101    fn clone_ssh() {
102        let template_path = "git@github.com:http-rs/surf.git";
103        let tmp_dir = tempdir().unwrap();
104        clone(template_path, None, tmp_dir.path(), None).unwrap();
105    }
106
107    #[test]
108    // warn: your ssh key must be in pem format
109    fn clone_ssh_commit() {
110        let commit = Some("8f0039488b3877ca59592900bc7ad645a83e2886");
111        let template_path = "git@github.com:http-rs/surf.git";
112        let tmp_dir = tempdir().unwrap();
113        clone(template_path, commit, tmp_dir.path(), None).unwrap();
114    }
115}