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 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 pub fn with_path(mut self, path: PathBuf) -> Result<Self> {
33 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 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 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 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 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 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 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 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}