1use crate::prelude::*;
2use getset::{Getters, Setters, WithSetters};
3use home::home_dir;
4
5#[derive(Clone, Debug, Serialize, Deserialize, Default, Getters, Setters, WithSetters)]
12#[getset(get = "pub", set = "pub")]
13#[serde(rename = "git")]
14pub struct GitRepository {
15 repo: String,
16 #[serde(rename = "res", skip_serializing_if = "Option::is_none")]
18 resource: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 tag: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 branch: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 rev: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 path: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 ssh_key: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 ssh_passphrase: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 token: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 username: Option<String>,
39}
40
41impl PartialEq for GitRepository {
42 fn eq(&self, other: &Self) -> bool {
43 self.repo == other.repo
44 }
45}
46impl EnvEvalable<GitRepository> for GitRepository {
47 fn env_eval(self, dict: &EnvDict) -> GitRepository {
48 Self {
49 repo: self.repo.env_eval(dict),
50 resource: self.resource.env_eval(dict),
51 tag: self.tag.env_eval(dict),
52 branch: self.branch.env_eval(dict),
53 rev: self.rev.env_eval(dict),
54 path: self.path.env_eval(dict),
55 ssh_key: self.ssh_key.env_eval(dict),
56 ssh_passphrase: self.ssh_passphrase.env_eval(dict),
57 token: self.token.env_eval(dict),
58 username: self.username.env_eval(dict),
59 }
60 }
61}
62
63impl GitRepository {
64 fn set_option_field(
65 mut self,
66 slot: impl FnOnce(&mut Self) -> &mut Option<String>,
67 value: Option<String>,
68 ) -> Self {
69 *slot(&mut self) = value;
70 self
71 }
72
73 pub fn from<S: Into<String>>(repo: S) -> Self {
74 Self {
75 repo: repo.into(),
76 ..Default::default()
77 }
78 }
79 pub fn with_tag<S: Into<String>>(self, tag: S) -> Self {
80 self.with_opt_tag(Some(tag.into()))
81 }
82 pub fn with_opt_tag(self, tag: Option<String>) -> Self {
83 self.set_option_field(|repo| &mut repo.tag, tag)
84 }
85 pub fn with_branch<S: Into<String>>(self, branch: S) -> Self {
86 self.with_opt_branch(Some(branch.into()))
87 }
88 pub fn with_opt_branch(self, branch: Option<String>) -> Self {
89 self.set_option_field(|repo| &mut repo.branch, branch)
90 }
91 pub fn with_rev<S: Into<String>>(mut self, rev: S) -> Self {
92 self.rev = Some(rev.into());
93 self
94 }
95 pub fn with_path<S: Into<String>>(mut self, path: S) -> Self {
96 self.path = Some(path.into());
97 self
98 }
99 pub fn with_ssh_key<S: Into<String>>(mut self, ssh_key: S) -> Self {
101 self.ssh_key = Some(ssh_key.into());
102 self
103 }
104 pub fn with_ssh_passphrase<S: Into<String>>(mut self, ssh_passphrase: S) -> Self {
106 self.ssh_passphrase = Some(ssh_passphrase.into());
107 self
108 }
109 pub fn with_token<S: Into<String>>(self, token: S) -> Self {
111 self.with_opt_token(Some(token.into()))
112 }
113 pub fn with_username<S: Into<String>>(self, username: S) -> Self {
115 self.with_opt_username(Some(username.into()))
116 }
117 pub fn with_opt_token(self, token: Option<String>) -> Self {
119 self.set_option_field(|repo| &mut repo.token, token)
120 }
121 pub fn with_opt_username(self, username: Option<String>) -> Self {
123 self.set_option_field(|repo| &mut repo.username, username)
124 }
125
126 pub fn with_github_token<S: Into<String>>(self, token: S) -> Self {
129 let token = token.into();
130 self.with_opt_username(Some("git".to_string()))
131 .with_opt_token(Some(token))
132 }
133
134 pub fn with_gitlab_token<S: Into<String>>(self, token: S) -> Self {
137 let token = token.into();
138 self.with_opt_username(Some("oauth2".to_string()))
139 .with_opt_token(Some(token))
140 }
141
142 pub fn with_gitea_token<S: Into<String>>(self, token: S) -> Self {
145 let token = token.into();
146 self.with_opt_username(Some("git".to_string()))
147 .with_opt_token(Some(token))
148 }
149
150 pub fn with_env_token(self, env_var: &str) -> Self {
155 match std::env::var(env_var) {
156 Ok(token) => self.with_opt_token(Some(token)),
157 Err(_) => self,
158 }
159 }
160
161 pub fn with_github_env_token(self) -> Self {
163 self.with_env_token("GITHUB_TOKEN")
164 }
165
166 pub fn with_gitlab_env_token(self) -> Self {
168 match std::env::var("GITLAB_TOKEN") {
169 Ok(token) => self
170 .with_opt_username(Some("oauth2".to_string()))
171 .with_opt_token(Some(token)),
172 Err(_) => self,
173 }
174 }
175
176 pub fn with_gitea_env_token(self) -> Self {
178 self.with_env_token("GITEA_TOKEN")
179 }
180
181 pub fn with_git_credentials(mut self) -> Self {
183 if let Some(credentials) = Self::read_git_credentials() {
184 for (url, username, token) in credentials {
185 if self.repo.starts_with(&url) {
186 self = self
187 .with_opt_username(Some(username))
188 .with_opt_token(Some(token));
189 break;
190 }
191 }
192 }
193 self
194 }
195 pub fn read_git_credentials() -> Option<Vec<(String, String, String)>> {
197 let home = home_dir()?;
198 let credentials_path = home.join(".git-credentials");
199 Self::read_git_credentials_at(&credentials_path)
200 }
201
202 fn read_git_credentials_at(path: &Path) -> Option<Vec<(String, String, String)>> {
203 use std::fs;
204 use std::io::{BufRead, BufReader};
205
206 if !path.exists() {
207 return None;
208 }
209
210 let file = fs::File::open(path).ok()?;
211 let reader = BufReader::new(file);
212 let mut credentials = Vec::new();
213
214 for line in reader.lines().map_while(Result::ok) {
215 let line = line.trim();
216 if line.is_empty() || line.starts_with('#') {
217 continue;
218 }
219
220 if let Ok(url) = url::Url::parse(line) {
221 let host = url.host_str()?;
222 let scheme = url.scheme();
223 let path = url.path();
224
225 let base_url = format!("{scheme}://{host}{path}");
226
227 let username = url.username();
228 if !username.is_empty()
229 && let Some(password) = url.password()
230 {
231 credentials.push((base_url, username.to_string(), password.to_string()));
232 }
233 }
234 }
235
236 if credentials.is_empty() {
237 None
238 } else {
239 Some(credentials)
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use std::io::Write;
248 use tempfile::NamedTempFile;
249
250 #[test]
251 fn test_git_repository_from() {
252 let repo = GitRepository::from("https://github.com/user/repo.git");
253 assert_eq!(repo.repo(), "https://github.com/user/repo.git");
254 assert!(repo.tag().is_none());
255 assert!(repo.branch().is_none());
256 assert!(repo.token().is_none());
257 }
258
259 #[test]
260 fn test_git_repository_with_tag() {
261 let repo = GitRepository::from("https://github.com/user/repo.git").with_tag("v1.0.0");
262 assert_eq!(repo.tag().as_ref(), Some(&"v1.0.0".to_string()));
263 }
264
265 #[test]
266 fn test_git_repository_with_opt_tag() {
267 let repo1 = GitRepository::from("https://github.com/user/repo.git")
268 .with_opt_tag(Some("v1.0.0".to_string()));
269 assert_eq!(repo1.tag().as_ref(), Some(&"v1.0.0".to_string()));
270
271 let repo2 = GitRepository::from("https://github.com/user/repo.git").with_opt_tag(None);
272 assert!(repo2.tag().is_none());
273 }
274
275 #[test]
276 fn test_git_repository_with_branch() {
277 let repo = GitRepository::from("https://github.com/user/repo.git").with_branch("main");
278 assert_eq!(repo.branch().as_ref(), Some(&"main".to_string()));
279 }
280
281 #[test]
282 fn test_git_repository_with_opt_branch() {
283 let repo1 = GitRepository::from("https://github.com/user/repo.git")
284 .with_opt_branch(Some("main".to_string()));
285 assert_eq!(repo1.branch().as_ref(), Some(&"main".to_string()));
286
287 let repo2 = GitRepository::from("https://github.com/user/repo.git").with_opt_branch(None);
288 assert!(repo2.branch().is_none());
289 }
290
291 #[test]
292 fn test_git_repository_with_rev() {
293 let repo = GitRepository::from("https://github.com/user/repo.git").with_rev("abc123");
294 assert_eq!(repo.rev().as_ref(), Some(&"abc123".to_string()));
295 }
296
297 #[test]
298 fn test_git_repository_with_path() {
299 let repo = GitRepository::from("https://github.com/user/repo.git").with_path("subdir");
300 assert_eq!(repo.path().as_ref(), Some(&"subdir".to_string()));
301 }
302
303 #[test]
304 fn test_git_repository_with_ssh_key() {
305 let repo = GitRepository::from("git@github.com:user/repo.git").with_ssh_key("/path/to/key");
306 assert_eq!(repo.ssh_key().as_ref(), Some(&"/path/to/key".to_string()));
307 }
308
309 #[test]
310 fn test_git_repository_with_ssh_passphrase() {
311 let repo =
312 GitRepository::from("git@github.com:user/repo.git").with_ssh_passphrase("secret");
313 assert_eq!(repo.ssh_passphrase().as_ref(), Some(&"secret".to_string()));
314 }
315
316 #[test]
317 fn test_git_repository_with_token() {
318 let repo = GitRepository::from("https://github.com/user/repo.git").with_token("token123");
319 assert_eq!(repo.token().as_ref(), Some(&"token123".to_string()));
320 }
321
322 #[test]
323 fn test_git_repository_with_username() {
324 let repo = GitRepository::from("https://github.com/user/repo.git").with_username("user");
325 assert_eq!(repo.username().as_ref(), Some(&"user".to_string()));
326 }
327
328 #[test]
329 fn test_git_repository_with_opt_token() {
330 let repo1 = GitRepository::from("https://github.com/user/repo.git")
331 .with_opt_token(Some("token123".to_string()));
332 assert_eq!(repo1.token().as_ref(), Some(&"token123".to_string()));
333
334 let repo2 = GitRepository::from("https://github.com/user/repo.git").with_opt_token(None);
335 assert!(repo2.token().is_none());
336 }
337
338 #[test]
339 fn test_git_repository_with_opt_username() {
340 let repo1 = GitRepository::from("https://github.com/user/repo.git")
341 .with_opt_username(Some("user".to_string()));
342 assert_eq!(repo1.username().as_ref(), Some(&"user".to_string()));
343
344 let repo2 = GitRepository::from("https://github.com/user/repo.git").with_opt_username(None);
345 assert!(repo2.username().is_none());
346 }
347
348 #[test]
349 fn test_git_repository_with_github_token() {
350 let repo =
351 GitRepository::from("https://github.com/user/repo.git").with_github_token("ghp_token");
352 assert_eq!(repo.username().as_ref(), Some(&"git".to_string()));
353 assert_eq!(repo.token().as_ref(), Some(&"ghp_token".to_string()));
354 }
355
356 #[test]
357 fn test_git_repository_with_gitlab_token() {
358 let repo = GitRepository::from("https://gitlab.com/user/repo.git")
359 .with_gitlab_token("glpat_token");
360 assert_eq!(repo.username().as_ref(), Some(&"oauth2".to_string()));
361 assert_eq!(repo.token().as_ref(), Some(&"glpat_token".to_string()));
362 }
363
364 #[test]
365 fn test_git_repository_with_gitea_token() {
366 let repo =
367 GitRepository::from("https://gitea.com/user/repo.git").with_gitea_token("gitea_token");
368 assert_eq!(repo.username().as_ref(), Some(&"git".to_string()));
369 assert_eq!(repo.token().as_ref(), Some(&"gitea_token".to_string()));
370 }
371
372 #[test]
373 fn test_read_git_credentials_valid_file() {
374 let mut temp_file = NamedTempFile::new().unwrap();
375 writeln!(temp_file, "https://user:token@github.com").unwrap();
376 writeln!(temp_file, "https://oauth2:token@gitlab.com/user/repo.git").unwrap();
377 writeln!(temp_file, "# This is a comment").unwrap();
378 temp_file.flush().unwrap();
380
381 let credentials = GitRepository::read_git_credentials_from_path(temp_file.path());
382 assert!(credentials.is_some());
383 let creds = credentials.unwrap();
384 assert_eq!(creds.len(), 2);
385
386 assert_eq!(creds[0].0, "https://github.com/");
387 assert_eq!(creds[0].1, "user");
388 assert_eq!(creds[0].2, "token");
389
390 assert_eq!(creds[1].0, "https://gitlab.com/user/repo.git");
391 assert_eq!(creds[1].1, "oauth2");
392 assert_eq!(creds[1].2, "token");
393 }
394
395 #[test]
396 fn test_read_git_credentials_empty_file() {
397 let temp_file = NamedTempFile::new().unwrap();
398 let credentials = GitRepository::read_git_credentials_from_path(temp_file.path());
399 assert!(credentials.is_none());
400 }
401
402 #[test]
403 fn test_read_git_credentials_invalid_url() {
404 let mut temp_file = NamedTempFile::new().unwrap();
405 writeln!(temp_file, "invalid-url").unwrap();
406 temp_file.flush().unwrap();
407
408 let credentials = GitRepository::read_git_credentials_from_path(temp_file.path());
409 assert!(credentials.is_none());
410 }
411
412 #[test]
413 fn test_git_repository_with_git_credentials() {
414 let mut temp_file = NamedTempFile::new().unwrap();
415 writeln!(temp_file, "https://user:token@github.com/user/repo.git").unwrap();
416 temp_file.flush().unwrap();
417
418 let repo = GitRepository::from("https://github.com/user/repo.git")
419 .with_git_credentials_from_path(temp_file.path());
420
421 assert_eq!(repo.username().as_ref(), Some(&"user".to_string()));
422 assert_eq!(repo.token().as_ref(), Some(&"token".to_string()));
423 }
424
425 #[test]
426 fn test_git_repository_with_git_credentials_no_match() {
427 let mut temp_file = NamedTempFile::new().unwrap();
428 writeln!(temp_file, "https://user:token@gitlab.com/user/repo.git").unwrap();
429 temp_file.flush().unwrap();
430
431 let repo = GitRepository::from("https://github.com/user/repo.git")
432 .with_git_credentials_from_path(temp_file.path());
433
434 assert!(repo.username().is_none());
435 assert!(repo.token().is_none());
436 }
437
438 impl GitRepository {
440 fn read_git_credentials_from_path(
441 path: &std::path::Path,
442 ) -> Option<Vec<(String, String, String)>> {
443 Self::read_git_credentials_at(path)
444 }
445
446 fn with_git_credentials_from_path(mut self, path: &std::path::Path) -> Self {
447 if let Some(credentials) = Self::read_git_credentials_at(path) {
448 for (url, username, token) in credentials {
449 if self.repo.starts_with(&url) {
450 self.username = Some(username);
451 self.token = Some(token);
452 break;
453 }
454 }
455 }
456 self
457 }
458 }
459}