mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00:00
feat: support https push (#353)
This commit is contained in:
parent
acccbfa08a
commit
9439114e5f
11 changed files with 609 additions and 36 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -67,8 +67,10 @@ dependencies = [
|
|||
"log",
|
||||
"rayon-core",
|
||||
"scopetime",
|
||||
"serial_test",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1070,6 +1072,28 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b15f74add9a9d4a3eb2bf739c9a427d266d3895b53d992c3a7c234fec2ff1f1"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"parking_lot 0.10.2",
|
||||
"serial_test_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65f59259be9fc1bf677d06cc1456e97756004a1a5a577480f71430bd7c17ba33"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.16"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ rayon-core = "1.9"
|
|||
crossbeam-channel = "0.5"
|
||||
log = "0.4"
|
||||
thiserror = "1.0"
|
||||
url = "2.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1"
|
||||
invalidstring = { path = "../invalidstring", version = "0.1" }
|
||||
invalidstring = { path = "../invalidstring", version = "0.1" }
|
||||
serial_test = "0.5.0"
|
||||
|
|
@ -9,6 +9,9 @@ pub enum Error {
|
|||
#[error("git: no head found")]
|
||||
NoHead,
|
||||
|
||||
#[error("git: remote url not found")]
|
||||
UnknownRemote,
|
||||
|
||||
#[error("io error:{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::sync::cred::BasicAuthCredential;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync, AsyncNotification, CWD,
|
||||
|
|
@ -88,6 +89,8 @@ pub struct PushRequest {
|
|||
pub remote: String,
|
||||
///
|
||||
pub branch: String,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
|
|
@ -161,6 +164,7 @@ impl AsyncPush {
|
|||
CWD,
|
||||
params.remote.as_str(),
|
||||
params.branch.as_str(),
|
||||
params.basic_credential,
|
||||
progress_sender.clone(),
|
||||
);
|
||||
|
||||
|
|
|
|||
257
asyncgit/src/sync/cred.rs
Normal file
257
asyncgit/src/sync/cred.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
//! credentials git helper
|
||||
|
||||
use git2::{Config, CredentialHelper};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::CWD;
|
||||
|
||||
/// basic Authentication Credentials
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct BasicAuthCredential {
|
||||
///
|
||||
pub username: Option<String>,
|
||||
///
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl BasicAuthCredential {
|
||||
///
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.username.is_some() && self.password.is_some()
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Self {
|
||||
BasicAuthCredential { username, password }
|
||||
}
|
||||
}
|
||||
|
||||
/// know if username and password are needed for this url
|
||||
pub fn need_username_password(remote: &str) -> Result<bool> {
|
||||
let repo = crate::sync::utils::repo(CWD)?;
|
||||
let url = repo
|
||||
.find_remote(remote)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let is_http = url.starts_with("http");
|
||||
Ok(is_http)
|
||||
}
|
||||
|
||||
/// extract username and password
|
||||
pub fn extract_username_password(
|
||||
remote: &str,
|
||||
) -> Result<BasicAuthCredential> {
|
||||
let repo = crate::sync::utils::repo(CWD)?;
|
||||
let url = repo
|
||||
.find_remote(remote)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let mut helper = CredentialHelper::new(&url);
|
||||
|
||||
if let Ok(config) = Config::open_default() {
|
||||
helper.config(&config);
|
||||
}
|
||||
Ok(match helper.execute() {
|
||||
Some((username, password)) => {
|
||||
BasicAuthCredential::new(Some(username), Some(password))
|
||||
}
|
||||
None => extract_cred_from_url(&url),
|
||||
})
|
||||
}
|
||||
|
||||
/// extract credentials from url
|
||||
pub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {
|
||||
if let Ok(url) = url::Url::parse(url) {
|
||||
BasicAuthCredential::new(
|
||||
if url.username() == "" {
|
||||
None
|
||||
} else {
|
||||
Some(url.username().to_owned())
|
||||
},
|
||||
url.password().map(|pwd| pwd.to_owned()),
|
||||
)
|
||||
} else {
|
||||
BasicAuthCredential::new(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::sync::cred::{
|
||||
extract_cred_from_url, extract_username_password,
|
||||
need_username_password, BasicAuthCredential,
|
||||
};
|
||||
use crate::sync::tests::repo_init;
|
||||
use crate::sync::DEFAULT_REMOTE_NAME;
|
||||
use serial_test::serial;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_credential_complete() {
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(
|
||||
Some("username".to_owned()),
|
||||
Some("password".to_owned())
|
||||
)
|
||||
.is_complete(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_not_complete() {
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(
|
||||
None,
|
||||
Some("password".to_owned())
|
||||
)
|
||||
.is_complete(),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(
|
||||
Some("username".to_owned()),
|
||||
None
|
||||
)
|
||||
.is_complete(),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(None, None).is_complete(),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_username_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://user@github.com"),
|
||||
BasicAuthCredential::new(Some("user".to_owned()), None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_username_password_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://user:pwd@github.com"),
|
||||
BasicAuthCredential::new(
|
||||
Some("user".to_owned()),
|
||||
Some("pwd".to_owned())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_nothing_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://github.com"),
|
||||
BasicAuthCredential::new(None, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_need_username_password_if_https() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
need_username_password(DEFAULT_REMOTE_NAME).unwrap(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_dont_need_username_password_if_ssh() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "git@github.com:user/repo")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
need_username_password(DEFAULT_REMOTE_NAME).unwrap(),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic]
|
||||
fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
|
||||
need_username_password(DEFAULT_REMOTE_NAME).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_extract_username_password_from_repo() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(
|
||||
DEFAULT_REMOTE_NAME,
|
||||
"http://user:pass@github.com",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
extract_username_password(DEFAULT_REMOTE_NAME).unwrap(),
|
||||
BasicAuthCredential::new(
|
||||
Some("user".to_owned()),
|
||||
Some("pass".to_owned())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_extract_username_from_repo() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
extract_username_password(DEFAULT_REMOTE_NAME).unwrap(),
|
||||
BasicAuthCredential::new(Some("user".to_owned()), None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic]
|
||||
fn test_error_if_no_remote_when_trying_to_extract_username_password(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
|
||||
extract_username_password(DEFAULT_REMOTE_NAME).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ mod commit;
|
|||
mod commit_details;
|
||||
mod commit_files;
|
||||
mod commits_info;
|
||||
pub mod cred;
|
||||
pub mod diff;
|
||||
mod hooks;
|
||||
mod hunks;
|
||||
|
|
@ -35,6 +36,7 @@ pub use ignore::add_to_ignore;
|
|||
pub use logwalker::LogWalker;
|
||||
pub use remotes::{
|
||||
fetch_origin, get_remotes, push, ProgressNotification,
|
||||
DEFAULT_REMOTE_NAME,
|
||||
};
|
||||
pub use reset::{reset_stage, reset_workdir};
|
||||
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
//!
|
||||
|
||||
use super::CommitId;
|
||||
use crate::{error::Result, sync::utils};
|
||||
use crate::{
|
||||
error::Result, sync::cred::BasicAuthCredential, sync::utils,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::{
|
||||
Cred, Error as GitError, FetchOptions, PackBuilderStage,
|
||||
PushOptions, RemoteCallbacks,
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProgressNotification {
|
||||
|
|
@ -49,6 +52,9 @@ pub enum ProgressNotification {
|
|||
Done,
|
||||
}
|
||||
|
||||
///
|
||||
pub const DEFAULT_REMOTE_NAME: &str = "origin";
|
||||
|
||||
///
|
||||
pub fn get_remotes(repo_path: &str) -> Result<Vec<String>> {
|
||||
scope_time!("get_remotes");
|
||||
|
|
@ -66,10 +72,10 @@ pub fn fetch_origin(repo_path: &str, branch: &str) -> Result<usize> {
|
|||
scope_time!("fetch_origin");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let mut remote = repo.find_remote("origin")?;
|
||||
let mut remote = repo.find_remote(DEFAULT_REMOTE_NAME)?;
|
||||
|
||||
let mut options = FetchOptions::new();
|
||||
options.remote_callbacks(match remote_callbacks(None) {
|
||||
options.remote_callbacks(match remote_callbacks(None, None) {
|
||||
Ok(callback) => callback,
|
||||
Err(e) => return Err(e),
|
||||
});
|
||||
|
|
@ -84,6 +90,7 @@ pub fn push(
|
|||
repo_path: &str,
|
||||
remote: &str,
|
||||
branch: &str,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
progress_sender: Sender<ProgressNotification>,
|
||||
) -> Result<()> {
|
||||
scope_time!("push_origin");
|
||||
|
|
@ -94,7 +101,10 @@ pub fn push(
|
|||
let mut options = PushOptions::new();
|
||||
|
||||
options.remote_callbacks(
|
||||
match remote_callbacks(Some(progress_sender)) {
|
||||
match remote_callbacks(
|
||||
Some(progress_sender),
|
||||
basic_credential,
|
||||
) {
|
||||
Ok(callbacks) => callbacks,
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
|
|
@ -108,6 +118,7 @@ pub fn push(
|
|||
|
||||
fn remote_callbacks<'a>(
|
||||
sender: Option<Sender<ProgressNotification>>,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
) -> Result<RemoteCallbacks<'a>> {
|
||||
let mut callbacks = RemoteCallbacks::new();
|
||||
let sender_clone = sender.clone();
|
||||
|
|
@ -165,21 +176,57 @@ fn remote_callbacks<'a>(
|
|||
})
|
||||
});
|
||||
});
|
||||
callbacks.credentials(|url, username_from_url, allowed_types| {
|
||||
log::debug!(
|
||||
"creds: '{}' {:?} ({:?})",
|
||||
url,
|
||||
username_from_url,
|
||||
allowed_types
|
||||
);
|
||||
|
||||
match username_from_url {
|
||||
Some(username) => Cred::ssh_key_from_agent(username),
|
||||
None => Err(GitError::from_str(
|
||||
" Couldn't extract username from url.",
|
||||
)),
|
||||
}
|
||||
});
|
||||
let mut first_call_to_credentials = true;
|
||||
// This boolean is used to avoid multiple call to credentials callback.
|
||||
// If credentials are bad, we don't ask the user to re-fill his creds. We push an error and he will be able to restart his action (for example a push) and retype his creds.
|
||||
// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
|
||||
// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
|
||||
// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
|
||||
callbacks.credentials(
|
||||
move |url, username_from_url, allowed_types| {
|
||||
log::debug!(
|
||||
"creds: '{}' {:?} ({:?})",
|
||||
url,
|
||||
username_from_url,
|
||||
allowed_types
|
||||
);
|
||||
if first_call_to_credentials {
|
||||
first_call_to_credentials = false;
|
||||
} else {
|
||||
return Err(GitError::from_str("Bad credentials."));
|
||||
}
|
||||
|
||||
match &basic_credential {
|
||||
_ if allowed_types.is_ssh_key() => {
|
||||
match username_from_url {
|
||||
Some(username) => {
|
||||
Cred::ssh_key_from_agent(username)
|
||||
}
|
||||
None => Err(GitError::from_str(
|
||||
" Couldn't extract username from url.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
Some(BasicAuthCredential {
|
||||
username: Some(user),
|
||||
password: Some(pwd),
|
||||
}) if allowed_types.is_user_pass_plaintext() => {
|
||||
Cred::userpass_plaintext(&user, &pwd)
|
||||
}
|
||||
Some(BasicAuthCredential {
|
||||
username: Some(user),
|
||||
password: _,
|
||||
}) if allowed_types.is_username() => {
|
||||
Cred::username(user)
|
||||
}
|
||||
_ if allowed_types.is_default() => Cred::default(),
|
||||
_ => Err(GitError::from_str(
|
||||
"Couldn't find credentials",
|
||||
)),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Ok(callbacks)
|
||||
}
|
||||
|
|
@ -204,7 +251,7 @@ mod tests {
|
|||
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
|
||||
assert_eq!(remotes, vec![String::from("origin")]);
|
||||
assert_eq!(remotes, vec![String::from(DEFAULT_REMOTE_NAME)]);
|
||||
|
||||
fetch_origin(repo_path, "master").unwrap();
|
||||
}
|
||||
|
|
|
|||
162
src/components/cred.rs
Normal file
162
src/components/cred.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
use anyhow::Result;
|
||||
use crossterm::event::Event;
|
||||
use tui::{backend::Backend, layout::Rect, Frame};
|
||||
|
||||
use asyncgit::sync::cred::BasicAuthCredential;
|
||||
|
||||
use crate::components::TextInputComponent;
|
||||
use crate::{
|
||||
components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent,
|
||||
},
|
||||
keys::SharedKeyConfig,
|
||||
strings,
|
||||
ui::style::SharedTheme,
|
||||
};
|
||||
|
||||
///
|
||||
pub struct CredComponent {
|
||||
visible: bool,
|
||||
key_config: SharedKeyConfig,
|
||||
input_username: TextInputComponent,
|
||||
input_password: TextInputComponent,
|
||||
cred: BasicAuthCredential,
|
||||
}
|
||||
|
||||
impl CredComponent {
|
||||
///
|
||||
pub fn new(
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
visible: false,
|
||||
input_username: TextInputComponent::new(
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
&strings::username_popup_title(&key_config),
|
||||
&strings::username_popup_msg(&key_config),
|
||||
),
|
||||
input_password: TextInputComponent::new(
|
||||
theme,
|
||||
key_config.clone(),
|
||||
&strings::password_popup_title(&key_config),
|
||||
&strings::password_popup_msg(&key_config),
|
||||
),
|
||||
key_config,
|
||||
cred: BasicAuthCredential::new(None, None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cred(&mut self, cred: BasicAuthCredential) {
|
||||
self.cred = cred;
|
||||
}
|
||||
|
||||
pub const fn get_cred(&self) -> &BasicAuthCredential {
|
||||
&self.cred
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawableComponent for CredComponent {
|
||||
fn draw<B: Backend>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
rect: Rect,
|
||||
) -> Result<()> {
|
||||
if self.visible {
|
||||
self.input_username.draw(f, rect)?;
|
||||
self.input_password.draw(f, rect)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CredComponent {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
_force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() {
|
||||
out.clear();
|
||||
}
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::validate_msg(&self.key_config),
|
||||
true,
|
||||
self.visible,
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::close_popup(&self.key_config),
|
||||
true,
|
||||
self.visible,
|
||||
));
|
||||
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||
if self.visible {
|
||||
if let Event::Key(e) = ev {
|
||||
if e == self.key_config.exit_popup {
|
||||
self.hide();
|
||||
}
|
||||
if self.input_username.event(ev)?
|
||||
|| self.input_password.event(ev)?
|
||||
{
|
||||
return Ok(true);
|
||||
} else if e == self.key_config.enter {
|
||||
if self.input_username.is_visible() {
|
||||
self.cred = BasicAuthCredential::new(
|
||||
Some(
|
||||
self.input_username
|
||||
.get_text()
|
||||
.to_owned(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
self.input_username.hide();
|
||||
self.input_password.show()?;
|
||||
} else if self.input_password.is_visible() {
|
||||
self.cred = BasicAuthCredential::new(
|
||||
self.cred.username.clone(),
|
||||
Some(
|
||||
self.input_password
|
||||
.get_text()
|
||||
.to_owned(),
|
||||
),
|
||||
);
|
||||
self.input_password.hide();
|
||||
self.input_password.clear();
|
||||
return Ok(false);
|
||||
} else {
|
||||
self.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.cred = BasicAuthCredential::new(None, None);
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.visible = true;
|
||||
if self.cred.username.is_none() {
|
||||
self.input_username.show()
|
||||
} else if self.cred.password.is_none() {
|
||||
self.input_password.show()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ mod commit;
|
|||
mod commit_details;
|
||||
mod commitlist;
|
||||
mod create_branch;
|
||||
mod cred;
|
||||
mod diff;
|
||||
mod externaleditor;
|
||||
mod filetree;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent,
|
||||
cred::CredComponent, visibility_blocking, CommandBlocking,
|
||||
CommandInfo, Component, DrawableComponent,
|
||||
},
|
||||
keys::SharedKeyConfig,
|
||||
queue::{InternalEvent, Queue},
|
||||
|
|
@ -10,6 +10,11 @@ use crate::{
|
|||
};
|
||||
use anyhow::Result;
|
||||
use asyncgit::{
|
||||
sync::cred::{
|
||||
extract_username_password, need_username_password,
|
||||
BasicAuthCredential,
|
||||
},
|
||||
sync::DEFAULT_REMOTE_NAME,
|
||||
AsyncNotification, AsyncPush, PushProgress, PushProgressState,
|
||||
PushRequest,
|
||||
};
|
||||
|
|
@ -30,9 +35,11 @@ pub struct PushComponent {
|
|||
git_push: AsyncPush,
|
||||
progress: Option<PushProgress>,
|
||||
pending: bool,
|
||||
branch: String,
|
||||
queue: Queue,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
input_cred: CredComponent,
|
||||
}
|
||||
|
||||
impl PushComponent {
|
||||
|
|
@ -47,8 +54,13 @@ impl PushComponent {
|
|||
queue: queue.clone(),
|
||||
pending: false,
|
||||
visible: false,
|
||||
branch: "".to_string(),
|
||||
git_push: AsyncPush::new(sender),
|
||||
progress: None,
|
||||
input_cred: CredComponent::new(
|
||||
theme.clone(),
|
||||
key_config.clone(),
|
||||
),
|
||||
theme,
|
||||
key_config,
|
||||
}
|
||||
|
|
@ -56,14 +68,36 @@ impl PushComponent {
|
|||
|
||||
///
|
||||
pub fn push(&mut self, branch: String) -> Result<()> {
|
||||
self.branch = branch;
|
||||
self.show()?;
|
||||
if need_username_password(DEFAULT_REMOTE_NAME)? {
|
||||
let cred = extract_username_password(DEFAULT_REMOTE_NAME)
|
||||
.unwrap_or_else(|_| {
|
||||
BasicAuthCredential::new(None, None)
|
||||
});
|
||||
if cred.is_complete() {
|
||||
self.push_to_remote(Some(cred))
|
||||
} else {
|
||||
self.input_cred.set_cred(cred);
|
||||
self.input_cred.show()
|
||||
}
|
||||
} else {
|
||||
self.push_to_remote(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_to_remote(
|
||||
&mut self,
|
||||
cred: Option<BasicAuthCredential>,
|
||||
) -> Result<()> {
|
||||
self.pending = true;
|
||||
self.progress = None;
|
||||
self.git_push.request(PushRequest {
|
||||
//TODO: find tracking branch name
|
||||
remote: String::from("origin"),
|
||||
branch,
|
||||
remote: String::from(DEFAULT_REMOTE_NAME),
|
||||
branch: self.branch.clone(),
|
||||
basic_credential: cred,
|
||||
})?;
|
||||
self.show()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +129,6 @@ impl PushComponent {
|
|||
)),
|
||||
);
|
||||
}
|
||||
|
||||
self.hide();
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +167,7 @@ impl DrawableComponent for PushComponent {
|
|||
fn draw<B: Backend>(
|
||||
&self,
|
||||
f: &mut Frame<B>,
|
||||
_rect: Rect,
|
||||
rect: Rect,
|
||||
) -> Result<()> {
|
||||
if self.visible {
|
||||
let (state, progress) = self.get_progress();
|
||||
|
|
@ -163,6 +196,7 @@ impl DrawableComponent for PushComponent {
|
|||
.percent(u16::from(progress)),
|
||||
area,
|
||||
);
|
||||
self.input_cred.draw(f, rect)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -173,27 +207,44 @@ impl Component for PushComponent {
|
|||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
_force_all: bool,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() {
|
||||
out.clear();
|
||||
}
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::close_msg(&self.key_config),
|
||||
!self.pending,
|
||||
self.visible,
|
||||
));
|
||||
|
||||
visibility_blocking(self)
|
||||
if self.input_cred.is_visible() {
|
||||
self.input_cred.commands(out, force_all)
|
||||
} else {
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::close_msg(&self.key_config),
|
||||
!self.pending,
|
||||
self.visible,
|
||||
));
|
||||
visibility_blocking(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: Event) -> Result<bool> {
|
||||
if self.visible {
|
||||
if let Event::Key(e) = ev {
|
||||
if e == self.key_config.enter {
|
||||
if e == self.key_config.exit_popup {
|
||||
self.hide();
|
||||
}
|
||||
if self.input_cred.event(ev)? {
|
||||
return Ok(true);
|
||||
} else if e == self.key_config.enter {
|
||||
if self.input_cred.is_visible()
|
||||
&& self.input_cred.get_cred().is_complete()
|
||||
{
|
||||
self.push_to_remote(Some(
|
||||
self.input_cred.get_cred().clone(),
|
||||
))?;
|
||||
self.input_cred.hide();
|
||||
} else {
|
||||
self.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,18 @@ pub fn create_branch_popup_msg(
|
|||
) -> String {
|
||||
"type branch name".to_string()
|
||||
}
|
||||
pub fn username_popup_title(_key_config: &SharedKeyConfig) -> String {
|
||||
"Username".to_string()
|
||||
}
|
||||
pub fn username_popup_msg(_key_config: &SharedKeyConfig) -> String {
|
||||
"type username".to_string()
|
||||
}
|
||||
pub fn password_popup_title(_key_config: &SharedKeyConfig) -> String {
|
||||
"Password".to_string()
|
||||
}
|
||||
pub fn password_popup_msg(_key_config: &SharedKeyConfig) -> String {
|
||||
"type password".to_string()
|
||||
}
|
||||
|
||||
pub fn rename_branch_popup_title(
|
||||
_key_config: &SharedKeyConfig,
|
||||
|
|
@ -334,6 +346,14 @@ pub mod commands {
|
|||
)
|
||||
.hide_help()
|
||||
}
|
||||
pub fn validate_msg(key_config: &SharedKeyConfig) -> CommandText {
|
||||
CommandText::new(
|
||||
format!("Validate [{}]", get_hint(key_config.enter),),
|
||||
"validate msg",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
.hide_help()
|
||||
}
|
||||
pub fn select_staging(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
|
|
|||
Loading…
Reference in a new issue