Skip to content

Commit 9439114

Browse files
authored
feat: support https push (#353)
1 parent acccbfa commit 9439114

File tree

11 files changed

+609
-36
lines changed

11 files changed

+609
-36
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

asyncgit/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ rayon-core = "1.9"
1818
crossbeam-channel = "0.5"
1919
log = "0.4"
2020
thiserror = "1.0"
21+
url = "2.1.1"
2122

2223
[dev-dependencies]
2324
tempfile = "3.1"
24-
invalidstring = { path = "../invalidstring", version = "0.1" }
25+
invalidstring = { path = "../invalidstring", version = "0.1" }
26+
serial_test = "0.5.0"

asyncgit/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ pub enum Error {
99
#[error("git: no head found")]
1010
NoHead,
1111

12+
#[error("git: remote url not found")]
13+
UnknownRemote,
14+
1215
#[error("io error:{0}")]
1316
Io(#[from] std::io::Error),
1417

asyncgit/src/push.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::sync::cred::BasicAuthCredential;
12
use crate::{
23
error::{Error, Result},
34
sync, AsyncNotification, CWD,
@@ -88,6 +89,8 @@ pub struct PushRequest {
8889
pub remote: String,
8990
///
9091
pub branch: String,
92+
///
93+
pub basic_credential: Option<BasicAuthCredential>,
9194
}
9295

9396
#[derive(Default, Clone, Debug)]
@@ -161,6 +164,7 @@ impl AsyncPush {
161164
CWD,
162165
params.remote.as_str(),
163166
params.branch.as_str(),
167+
params.basic_credential,
164168
progress_sender.clone(),
165169
);
166170

asyncgit/src/sync/cred.rs

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
//! credentials git helper
2+
3+
use git2::{Config, CredentialHelper};
4+
5+
use crate::error::{Error, Result};
6+
use crate::CWD;
7+
8+
/// basic Authentication Credentials
9+
#[derive(Debug, Clone, Default, PartialEq)]
10+
pub struct BasicAuthCredential {
11+
///
12+
pub username: Option<String>,
13+
///
14+
pub password: Option<String>,
15+
}
16+
17+
impl BasicAuthCredential {
18+
///
19+
pub fn is_complete(&self) -> bool {
20+
self.username.is_some() && self.password.is_some()
21+
}
22+
///
23+
pub fn new(
24+
username: Option<String>,
25+
password: Option<String>,
26+
) -> Self {
27+
BasicAuthCredential { username, password }
28+
}
29+
}
30+
31+
/// know if username and password are needed for this url
32+
pub fn need_username_password(remote: &str) -> Result<bool> {
33+
let repo = crate::sync::utils::repo(CWD)?;
34+
let url = repo
35+
.find_remote(remote)?
36+
.url()
37+
.ok_or(Error::UnknownRemote)?
38+
.to_owned();
39+
let is_http = url.starts_with("http");
40+
Ok(is_http)
41+
}
42+
43+
/// extract username and password
44+
pub fn extract_username_password(
45+
remote: &str,
46+
) -> Result<BasicAuthCredential> {
47+
let repo = crate::sync::utils::repo(CWD)?;
48+
let url = repo
49+
.find_remote(remote)?
50+
.url()
51+
.ok_or(Error::UnknownRemote)?
52+
.to_owned();
53+
let mut helper = CredentialHelper::new(&url);
54+
55+
if let Ok(config) = Config::open_default() {
56+
helper.config(&config);
57+
}
58+
Ok(match helper.execute() {
59+
Some((username, password)) => {
60+
BasicAuthCredential::new(Some(username), Some(password))
61+
}
62+
None => extract_cred_from_url(&url),
63+
})
64+
}
65+
66+
/// extract credentials from url
67+
pub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {
68+
if let Ok(url) = url::Url::parse(url) {
69+
BasicAuthCredential::new(
70+
if url.username() == "" {
71+
None
72+
} else {
73+
Some(url.username().to_owned())
74+
},
75+
url.password().map(|pwd| pwd.to_owned()),
76+
)
77+
} else {
78+
BasicAuthCredential::new(None, None)
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use crate::sync::cred::{
85+
extract_cred_from_url, extract_username_password,
86+
need_username_password, BasicAuthCredential,
87+
};
88+
use crate::sync::tests::repo_init;
89+
use crate::sync::DEFAULT_REMOTE_NAME;
90+
use serial_test::serial;
91+
use std::env;
92+
93+
#[test]
94+
fn test_credential_complete() {
95+
assert_eq!(
96+
BasicAuthCredential::new(
97+
Some("username".to_owned()),
98+
Some("password".to_owned())
99+
)
100+
.is_complete(),
101+
true
102+
);
103+
}
104+
105+
#[test]
106+
fn test_credential_not_complete() {
107+
assert_eq!(
108+
BasicAuthCredential::new(
109+
None,
110+
Some("password".to_owned())
111+
)
112+
.is_complete(),
113+
false
114+
);
115+
assert_eq!(
116+
BasicAuthCredential::new(
117+
Some("username".to_owned()),
118+
None
119+
)
120+
.is_complete(),
121+
false
122+
);
123+
assert_eq!(
124+
BasicAuthCredential::new(None, None).is_complete(),
125+
false
126+
);
127+
}
128+
129+
#[test]
130+
fn test_extract_username_from_url() {
131+
assert_eq!(
132+
extract_cred_from_url("https://[email protected]"),
133+
BasicAuthCredential::new(Some("user".to_owned()), None)
134+
);
135+
}
136+
137+
#[test]
138+
fn test_extract_username_password_from_url() {
139+
assert_eq!(
140+
extract_cred_from_url("https://user:[email protected]"),
141+
BasicAuthCredential::new(
142+
Some("user".to_owned()),
143+
Some("pwd".to_owned())
144+
)
145+
);
146+
}
147+
148+
#[test]
149+
fn test_extract_nothing_from_url() {
150+
assert_eq!(
151+
extract_cred_from_url("https://github.com"),
152+
BasicAuthCredential::new(None, None)
153+
);
154+
}
155+
156+
#[test]
157+
#[serial]
158+
fn test_need_username_password_if_https() {
159+
let (_td, repo) = repo_init().unwrap();
160+
let root = repo.path().parent().unwrap();
161+
let repo_path = root.as_os_str().to_str().unwrap();
162+
163+
env::set_current_dir(repo_path).unwrap();
164+
repo.remote(DEFAULT_REMOTE_NAME, "http://[email protected]")
165+
.unwrap();
166+
167+
assert_eq!(
168+
need_username_password(DEFAULT_REMOTE_NAME).unwrap(),
169+
true
170+
);
171+
}
172+
173+
#[test]
174+
#[serial]
175+
fn test_dont_need_username_password_if_ssh() {
176+
let (_td, repo) = repo_init().unwrap();
177+
let root = repo.path().parent().unwrap();
178+
let repo_path = root.as_os_str().to_str().unwrap();
179+
180+
env::set_current_dir(repo_path).unwrap();
181+
repo.remote(DEFAULT_REMOTE_NAME, "[email protected]:user/repo")
182+
.unwrap();
183+
184+
assert_eq!(
185+
need_username_password(DEFAULT_REMOTE_NAME).unwrap(),
186+
false
187+
);
188+
}
189+
190+
#[test]
191+
#[serial]
192+
#[should_panic]
193+
fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(
194+
) {
195+
let (_td, repo) = repo_init().unwrap();
196+
let root = repo.path().parent().unwrap();
197+
let repo_path = root.as_os_str().to_str().unwrap();
198+
199+
env::set_current_dir(repo_path).unwrap();
200+
201+
need_username_password(DEFAULT_REMOTE_NAME).unwrap();
202+
}
203+
204+
#[test]
205+
#[serial]
206+
fn test_extract_username_password_from_repo() {
207+
let (_td, repo) = repo_init().unwrap();
208+
let root = repo.path().parent().unwrap();
209+
let repo_path = root.as_os_str().to_str().unwrap();
210+
211+
env::set_current_dir(repo_path).unwrap();
212+
repo.remote(
213+
DEFAULT_REMOTE_NAME,
214+
"http://user:[email protected]",
215+
)
216+
.unwrap();
217+
218+
assert_eq!(
219+
extract_username_password(DEFAULT_REMOTE_NAME).unwrap(),
220+
BasicAuthCredential::new(
221+
Some("user".to_owned()),
222+
Some("pass".to_owned())
223+
)
224+
);
225+
}
226+
227+
#[test]
228+
#[serial]
229+
fn test_extract_username_from_repo() {
230+
let (_td, repo) = repo_init().unwrap();
231+
let root = repo.path().parent().unwrap();
232+
let repo_path = root.as_os_str().to_str().unwrap();
233+
234+
env::set_current_dir(repo_path).unwrap();
235+
repo.remote(DEFAULT_REMOTE_NAME, "http://[email protected]")
236+
.unwrap();
237+
238+
assert_eq!(
239+
extract_username_password(DEFAULT_REMOTE_NAME).unwrap(),
240+
BasicAuthCredential::new(Some("user".to_owned()), None)
241+
);
242+
}
243+
244+
#[test]
245+
#[serial]
246+
#[should_panic]
247+
fn test_error_if_no_remote_when_trying_to_extract_username_password(
248+
) {
249+
let (_td, repo) = repo_init().unwrap();
250+
let root = repo.path().parent().unwrap();
251+
let repo_path = root.as_os_str().to_str().unwrap();
252+
253+
env::set_current_dir(repo_path).unwrap();
254+
255+
extract_username_password(DEFAULT_REMOTE_NAME).unwrap();
256+
}
257+
}

asyncgit/src/sync/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod commit;
55
mod commit_details;
66
mod commit_files;
77
mod commits_info;
8+
pub mod cred;
89
pub mod diff;
910
mod hooks;
1011
mod hunks;
@@ -35,6 +36,7 @@ pub use ignore::add_to_ignore;
3536
pub use logwalker::LogWalker;
3637
pub use remotes::{
3738
fetch_origin, get_remotes, push, ProgressNotification,
39+
DEFAULT_REMOTE_NAME,
3840
};
3941
pub use reset::{reset_stage, reset_workdir};
4042
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};

0 commit comments

Comments
 (0)