Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions asyncgit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rayon-core = "1.8"
crossbeam-channel = "0.5"
log = "0.4"
thiserror = "1.0"
url = "2.1.1"

[dev-dependencies]
tempfile = "3.1"
Expand Down
3 changes: 3 additions & 0 deletions asyncgit/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ pub enum Error {
#[error("git: no head found")]
NoHead,

#[error("git: remote url not found")]
NoRemote,

#[error("io error:{0}")]
Io(#[from] std::io::Error),

Expand Down
4 changes: 4 additions & 0 deletions asyncgit/src/push.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::sync::cred::BasicAuthCredential;
use crate::{
error::{Error, Result},
sync, AsyncNotification, CWD,
Expand Down Expand Up @@ -88,6 +89,8 @@ pub struct PushRequest {
pub remote: String,
///
pub branch: String,
///
pub basic_credential: Option<BasicAuthCredential>,
}

#[derive(Default, Clone, Debug)]
Expand Down Expand Up @@ -161,6 +164,7 @@ impl AsyncPush {
CWD,
params.remote.as_str(),
params.branch.as_str(),
params.basic_credential,
progress_sender.clone(),
);

Expand Down
80 changes: 80 additions & 0 deletions asyncgit/src/sync/cred.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! credentials git helper

use git2::{Config, CredentialHelper};

use crate::error::{Error, Result};
use crate::CWD;

/// basic Authentication Credentials
#[derive(Debug, Clone, Default)]
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::NoRemote)?
.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::NoRemote)?
.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)
}
}
2 changes: 2 additions & 0 deletions asyncgit/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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};
Expand Down
85 changes: 66 additions & 19 deletions asyncgit/src/sync/remotes.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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");
Expand All @@ -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),
});
Expand All @@ -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");
Expand All @@ -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),
},
Expand All @@ -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();
Expand Down Expand Up @@ -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)
}
Expand All @@ -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();
}
Expand Down
Loading