gnostr_asyncgit/sync/
cred.rs

1//! credentials git helper
2
3use git2::CredentialHelper;
4
5use super::{
6	RepoPath,
7	remotes::{
8		get_default_remote_for_fetch_in_repo,
9		get_default_remote_for_push_in_repo,
10		get_default_remote_in_repo,
11	},
12	repository::repo,
13};
14use crate::error::{Error, Result};
15
16/// basic Authentication Credentials
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct BasicAuthCredential {
19	///
20	pub username: Option<String>,
21	///
22	pub password: Option<String>,
23}
24
25impl BasicAuthCredential {
26	///
27	pub const fn is_complete(&self) -> bool {
28		self.username.is_some() && self.password.is_some()
29	}
30	///
31	pub const fn new(
32		username: Option<String>,
33		password: Option<String>,
34	) -> Self {
35		Self { username, password }
36	}
37}
38
39/// know if username and password are needed for this url
40pub fn need_username_password(repo_path: &RepoPath) -> Result<bool> {
41	let repo = repo(repo_path)?;
42	let remote =
43		repo.find_remote(&get_default_remote_in_repo(&repo)?)?;
44	let url = remote
45		.pushurl()
46		.or_else(|| remote.url())
47		.ok_or(Error::UnknownRemote)?
48		.to_owned();
49	let is_http = url.starts_with("http");
50	Ok(is_http)
51}
52
53/// know if username and password are needed for this url
54/// TODO: Very similar to `need_username_password_for_fetch`. Can be
55/// refactored. See also `need_username_password`.
56pub fn need_username_password_for_fetch(
57	repo_path: &RepoPath,
58) -> Result<bool> {
59	let repo = repo(repo_path)?;
60	let remote = repo
61		.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?;
62	let url = remote
63		.url()
64		.or_else(|| remote.url())
65		.ok_or(Error::UnknownRemote)?
66		.to_owned();
67	let is_http = url.starts_with("http");
68	Ok(is_http)
69}
70
71/// know if username and password are needed for this url
72/// TODO: Very similar to `need_username_password_for_fetch`. Can be
73/// refactored. See also `need_username_password`.
74pub fn need_username_password_for_push(
75	repo_path: &RepoPath,
76) -> Result<bool> {
77	let repo = repo(repo_path)?;
78	let remote = repo
79		.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?;
80	let url = remote
81		.pushurl()
82		.or_else(|| remote.url())
83		.ok_or(Error::UnknownRemote)?
84		.to_owned();
85	let is_http = url.starts_with("http");
86	Ok(is_http)
87}
88
89/// extract username and password
90pub fn extract_username_password(
91	repo_path: &RepoPath,
92) -> Result<BasicAuthCredential> {
93	let repo = repo(repo_path)?;
94	let url = repo
95		.find_remote(&get_default_remote_in_repo(&repo)?)?
96		.url()
97		.ok_or(Error::UnknownRemote)?
98		.to_owned();
99	let mut helper = CredentialHelper::new(&url);
100
101	//TODO: look at Cred::credential_helper,
102	//if the username is in the url we need to set it here,
103	//I dont think `config` will pick it up
104
105	if let Ok(config) = repo.config() {
106		helper.config(&config);
107	}
108
109	Ok(match helper.execute() {
110		Some((username, password)) => {
111			BasicAuthCredential::new(Some(username), Some(password))
112		}
113		None => extract_cred_from_url(&url),
114	})
115}
116
117/// extract username and password
118/// TODO: Very similar to `extract_username_password_for_fetch`. Can
119/// be refactored.
120pub fn extract_username_password_for_fetch(
121	repo_path: &RepoPath,
122) -> Result<BasicAuthCredential> {
123	let repo = repo(repo_path)?;
124	let url = repo
125		.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?
126		.url()
127		.ok_or(Error::UnknownRemote)?
128		.to_owned();
129	let mut helper = CredentialHelper::new(&url);
130
131	//TODO: look at Cred::credential_helper,
132	//if the username is in the url we need to set it here,
133	//I dont think `config` will pick it up
134
135	if let Ok(config) = repo.config() {
136		helper.config(&config);
137	}
138
139	Ok(match helper.execute() {
140		Some((username, password)) => {
141			BasicAuthCredential::new(Some(username), Some(password))
142		}
143		None => extract_cred_from_url(&url),
144	})
145}
146
147/// extract username and password
148/// TODO: Very similar to `extract_username_password_for_fetch`. Can
149/// be refactored.
150pub fn extract_username_password_for_push(
151	repo_path: &RepoPath,
152) -> Result<BasicAuthCredential> {
153	let repo = repo(repo_path)?;
154	let url = repo
155		.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?
156		.url()
157		.ok_or(Error::UnknownRemote)?
158		.to_owned();
159	let mut helper = CredentialHelper::new(&url);
160
161	//TODO: look at Cred::credential_helper,
162	//if the username is in the url we need to set it here,
163	//I dont think `config` will pick it up
164
165	if let Ok(config) = repo.config() {
166		helper.config(&config);
167	}
168
169	Ok(match helper.execute() {
170		Some((username, password)) => {
171			BasicAuthCredential::new(Some(username), Some(password))
172		}
173		None => extract_cred_from_url(&url),
174	})
175}
176
177/// extract credentials from url
178pub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {
179	url::Url::parse(url).map_or_else(
180		|_| BasicAuthCredential::new(None, None),
181		|url| {
182			BasicAuthCredential::new(
183				if url.username() == "" {
184					None
185				} else {
186					Some(url.username().to_owned())
187				},
188				url.password().map(std::borrow::ToOwned::to_owned),
189			)
190		},
191	)
192}
193
194#[cfg(test)]
195mod tests {
196	use serial_test::serial;
197
198	use crate::sync::{
199		RepoPath,
200		cred::{
201			BasicAuthCredential, extract_cred_from_url,
202			extract_username_password, need_username_password,
203		},
204		remotes::DEFAULT_REMOTE_NAME,
205		tests::repo_init,
206	};
207
208	#[test]
209	fn test_credential_complete() {
210		assert_eq!(
211			BasicAuthCredential::new(
212				Some("username".to_owned()),
213				Some("password".to_owned())
214			)
215			.is_complete(),
216			true
217		);
218	}
219
220	#[test]
221	fn test_credential_not_complete() {
222		assert_eq!(
223			BasicAuthCredential::new(
224				None,
225				Some("password".to_owned())
226			)
227			.is_complete(),
228			false
229		);
230		assert_eq!(
231			BasicAuthCredential::new(
232				Some("username".to_owned()),
233				None
234			)
235			.is_complete(),
236			false
237		);
238		assert_eq!(
239			BasicAuthCredential::new(None, None).is_complete(),
240			false
241		);
242	}
243
244	#[test]
245	fn test_extract_username_from_url() {
246		assert_eq!(
247			extract_cred_from_url("https://user@github.com"),
248			BasicAuthCredential::new(Some("user".to_owned()), None)
249		);
250	}
251
252	#[test]
253	fn test_extract_username_password_from_url() {
254		assert_eq!(
255			extract_cred_from_url("https://user:pwd@github.com"),
256			BasicAuthCredential::new(
257				Some("user".to_owned()),
258				Some("pwd".to_owned())
259			)
260		);
261	}
262
263	#[test]
264	fn test_extract_nothing_from_url() {
265		assert_eq!(
266			extract_cred_from_url("https://github.com"),
267			BasicAuthCredential::new(None, None)
268		);
269	}
270
271	#[test]
272	#[serial]
273	fn test_need_username_password_if_https() {
274		let (_td, repo) = repo_init().unwrap();
275		let root = repo.path().parent().unwrap();
276		let repo_path: &RepoPath =
277			&root.as_os_str().to_str().unwrap().into();
278
279		repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
280			.unwrap();
281
282		assert_eq!(need_username_password(repo_path).unwrap(), true);
283	}
284
285	#[test]
286	#[serial]
287	fn test_dont_need_username_password_if_ssh() {
288		let (_td, repo) = repo_init().unwrap();
289		let root = repo.path().parent().unwrap();
290		let repo_path: &RepoPath =
291			&root.as_os_str().to_str().unwrap().into();
292
293		repo.remote(DEFAULT_REMOTE_NAME, "git@github.com:user/repo")
294			.unwrap();
295
296		assert_eq!(need_username_password(repo_path).unwrap(), false);
297	}
298
299	#[test]
300	#[serial]
301	fn test_dont_need_username_password_if_pushurl_ssh() {
302		let (_td, repo) = repo_init().unwrap();
303		let root = repo.path().parent().unwrap();
304		let repo_path: &RepoPath =
305			&root.as_os_str().to_str().unwrap().into();
306
307		repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
308			.unwrap();
309		repo.remote_set_pushurl(
310			DEFAULT_REMOTE_NAME,
311			Some("git@github.com:user/repo"),
312		)
313		.unwrap();
314
315		assert_eq!(need_username_password(repo_path).unwrap(), false);
316	}
317
318	#[test]
319	#[serial]
320	#[should_panic]
321	fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password()
322	 {
323		let (_td, repo) = repo_init().unwrap();
324		let root = repo.path().parent().unwrap();
325		let repo_path: &RepoPath =
326			&root.as_os_str().to_str().unwrap().into();
327
328		need_username_password(repo_path).unwrap();
329	}
330
331	#[test]
332	#[serial]
333	fn test_extract_username_password_from_repo() {
334		let (_td, repo) = repo_init().unwrap();
335		let root = repo.path().parent().unwrap();
336		let repo_path: &RepoPath =
337			&root.as_os_str().to_str().unwrap().into();
338
339		repo.remote(
340			DEFAULT_REMOTE_NAME,
341			"http://user:pass@github.com",
342		)
343		.unwrap();
344
345		assert_eq!(
346			extract_username_password(repo_path).unwrap(),
347			BasicAuthCredential::new(
348				Some("user".to_owned()),
349				Some("pass".to_owned())
350			)
351		);
352	}
353
354	#[test]
355	#[serial]
356	fn test_extract_username_from_repo() {
357		let (_td, repo) = repo_init().unwrap();
358		let root = repo.path().parent().unwrap();
359		let repo_path: &RepoPath =
360			&root.as_os_str().to_str().unwrap().into();
361
362		repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
363			.unwrap();
364
365		assert_eq!(
366			extract_username_password(repo_path).unwrap(),
367			BasicAuthCredential::new(Some("user".to_owned()), None)
368		);
369	}
370
371	#[test]
372	#[serial]
373	#[should_panic]
374	fn test_error_if_no_remote_when_trying_to_extract_username_password()
375	 {
376		let (_td, repo) = repo_init().unwrap();
377		let root = repo.path().parent().unwrap();
378		let repo_path: &RepoPath =
379			&root.as_os_str().to_str().unwrap().into();
380
381		extract_username_password(repo_path).unwrap();
382	}
383}