gnostr_asyncgit/sync/
cred.rs

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