git_meta/
clone.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4
5use crate::{GitCredentials, GitRepo, GitRepoCloneRequest, GitRepoInfo};
6use git_url_parse::GitUrl;
7
8use color_eyre::eyre::{eyre, Result};
9use tracing::{debug, info};
10
11impl GitRepoCloneRequest {
12    /// Create a new `GitRepo` with `url`.
13    /// Use along with `with_*` methods to set other fields of `GitRepo`.
14    /// Use `GitRepoCloner` if you need to clone the repo, and convert back with `GitRepo.into()`
15    pub fn new<S: AsRef<str>>(url: S) -> Result<Self> {
16        let url = if let Ok(u) = GitUrl::parse(url.as_ref()) {
17            u
18        } else {
19            return Err(eyre!("url failed to parse as GitUrl"));
20        };
21
22        Ok(Self {
23            url,
24            credentials: None,
25            head: None,
26            branch: None,
27            path: None,
28        })
29    }
30
31    /// Set the location of `GitRepo` on the filesystem
32    pub fn with_path(mut self, path: PathBuf) -> Result<Self> {
33        // We want to get the absolute path of the directory of the repo
34        self.path = if let Ok(p) = fs::canonicalize(path) {
35            Some(p)
36        } else {
37            return Err(eyre!("Directory was not found"));
38        };
39        Ok(self)
40    }
41
42    /// Intended to be set with the remote name branch of GitRepo
43    pub fn with_branch(mut self, branch: Option<String>) -> Self {
44        if let Some(b) = branch {
45            self.branch = Some(b);
46        }
47        self
48    }
49
50    // TODO: Fix this for clone
51    ///// Reinit `GitRepo` with commit id
52    //pub fn with_commit(mut self, commit_id: Option<String>) -> Self {
53    //    self = GitRepo::open(self.path.expect("No path set"), self.branch, commit_id)
54    //        .expect("Unable to open GitRepo with commit id");
55    //    self
56    //}
57
58    /// Set `GitCredentials` for private repos.
59    /// `None` indicates public repo
60    pub fn with_credentials(mut self, creds: Option<GitCredentials>) -> Self {
61        self.credentials = creds;
62        self
63    }
64
65    pub fn to_repo(&self) -> GitRepo {
66        self.into()
67    }
68
69    pub fn to_info(&self) -> GitRepoInfo {
70        self.into()
71    }
72
73    // TODO: Can we make this mut self?
74    pub fn git_clone<P: AsRef<Path>>(&self, target: P) -> Result<GitRepo> {
75        let git_info: GitRepoInfo = self.into();
76        let cb = git_info.build_git2_remotecallback()?;
77
78        let mut builder = git2::build::RepoBuilder::new();
79        let mut fetch_options = git2::FetchOptions::new();
80
81        fetch_options.remote_callbacks(cb);
82        builder.fetch_options(fetch_options);
83
84        if let Some(b) = &self.branch {
85            builder.branch(b);
86        }
87
88        let repo = match builder.clone(&self.url.to_string(), target.as_ref()) {
89            Ok(repo) => repo,
90            Err(e) => return Err(eyre!("failed to clone: {}", e)),
91        };
92
93        // Ensure we don't lose the credentials while updating
94        let mut git_repo: GitRepo = repo.try_into()?;
95        git_repo = git_repo.with_credentials(self.credentials.clone());
96
97        Ok(git_repo)
98    }
99
100    // TODO: Can we make this mut self?
101    pub fn git_clone_shallow<P: AsRef<Path>>(&self, target: P) -> Result<GitRepo> {
102        let repo = if let Some(cred) = self.credentials.clone() {
103            match cred {
104                crate::GitCredentials::SshKey {
105                    username,
106                    public_key,
107                    private_key,
108                    passphrase,
109                } => {
110                    let mut parsed_uri = self.url.trim_auth();
111                    parsed_uri.user = Some(username.to_string());
112
113                    let privkey_path =
114                        if let Ok(path) = private_key.clone().into_os_string().into_string() {
115                            path
116                        } else {
117                            return Err(eyre!("Couldn't convert path to string"));
118                        };
119
120                    let shell_clone_command = if let Ok(spawn) = Command::new("git")
121                        .arg("clone")
122                        .arg(format!("{}", parsed_uri))
123                        .arg(format!("{}", target.as_ref().display()))
124                        .arg("--no-single-branch")
125                        .arg("--depth=1")
126                        .arg("--config")
127                        .arg(format!("core.sshcommand=ssh -i {privkey_path}"))
128                        .stdout(Stdio::piped())
129                        .stderr(Stdio::null())
130                        .spawn()
131                    {
132                        spawn
133                    } else {
134                        return Err(eyre!("failed to run git clone"));
135                    };
136
137                    let clone_out = if let Ok(wait) = shell_clone_command.wait_with_output() {
138                        wait
139                    } else {
140                        return Err(eyre!("failed to open stdout"));
141                    };
142
143                    debug!("Clone output: {:?}", clone_out);
144
145                    // Re-create the GitCredentials
146                    let creds = GitCredentials::SshKey {
147                        username,
148                        public_key,
149                        private_key,
150                        passphrase,
151                    };
152
153                    if let Ok(repo) = GitRepo::open(target.as_ref().to_path_buf(), None, None) {
154                        repo
155                    } else {
156                        return Err(eyre!("Failed to open shallow clone dir: {:?}", clone_out));
157                    }
158                    .with_credentials(Some(creds))
159                }
160                crate::GitCredentials::UserPassPlaintext { username, password } => {
161                    let mut cli_remote_url = self.url.clone();
162                    cli_remote_url.user = Some(username.to_string());
163                    cli_remote_url.token = Some(password.to_string());
164
165                    let shell_clone_command = if let Ok(spawn) = Command::new("git")
166                        .arg("clone")
167                        .arg(format!("{}", cli_remote_url))
168                        .arg(format!("{}", target.as_ref().display()))
169                        .arg("--no-single-branch")
170                        .arg("--depth=1")
171                        .stdout(Stdio::piped())
172                        .stderr(Stdio::null())
173                        .spawn()
174                    {
175                        spawn
176                    } else {
177                        return Err(eyre!("Failed to run git clone"));
178                    };
179
180                    let clone_out = if let Some(stdout) = shell_clone_command.stdout {
181                        stdout
182                    } else {
183                        return Err(eyre!("Failed to open stdout"));
184                    };
185
186                    // Re-create the GitCredentials
187                    let creds = GitCredentials::UserPassPlaintext { username, password };
188
189                    if let Ok(repo) = GitRepo::open(target.as_ref().to_path_buf(), None, None) {
190                        repo
191                    } else {
192                        return Err(eyre!("Failed to open shallow clone dir: {:?}", clone_out));
193                    }
194                    .with_credentials(Some(creds))
195                }
196            }
197        } else {
198            let parsed_uri = self.url.trim_auth();
199
200            info!("Url: {}", format!("{}", parsed_uri));
201            info!("Directory: {}", format!("{}", target.as_ref().display()));
202
203            let shell_clone_command = if let Ok(spawn) = Command::new("git")
204                .arg("clone")
205                .arg(format!("{}", parsed_uri))
206                .arg(format!("{}", target.as_ref().display()))
207                .arg("--no-single-branch")
208                .arg("--depth=1")
209                .stdout(Stdio::piped())
210                .stderr(Stdio::null())
211                .spawn()
212            {
213                spawn
214            } else {
215                return Err(eyre!("Failed to run git clone"));
216            };
217
218            let clone_out = if let Ok(stdout) = shell_clone_command.wait_with_output() {
219                stdout
220            } else {
221                return Err(eyre!("Failed to wait for output"));
222            }
223            .stdout;
224
225            if let Ok(repo) = GitRepo::open(target.as_ref().to_path_buf(), None, None) {
226                repo
227            } else {
228                return Err(eyre!("Failed to open shallow clone dir: {:?}", clone_out));
229            }
230        };
231
232        Ok(repo)
233    }
234}