error if force push was rejected (#810)

* error if force push was rejected
This commit is contained in:
Stephan Dilly 2021-07-10 14:11:01 +02:00 committed by GitHub
parent b6ab0cfa34
commit 23944dc608
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 252 additions and 142 deletions

View file

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- new `undo-last-commit` command [[@remique](https://github.com/remique)] ([#758](https://github.com/extrawurst/gitui/issues/758))
- taglist: show arrow-symbol on tags not present on origin [[@cruessler](https://github.com/cruessler)] ([#776](https://github.com/extrawurst/gitui/issues/776))
- new quit key `[q]` ([#771](https://github.com/extrawurst/gitui/issues/771))
- proper error message if remote rejects force push ([#801](https://github.com/extrawurst/gitui/issues/801))
## Fixed
- openssl vendoring broken on macos ([#772](https://github.com/extrawurst/gitui/issues/772))

View file

@ -0,0 +1,223 @@
#![allow(dead_code)]
use super::push::ProgressNotification;
use crate::{error::Result, sync::cred::BasicAuthCredential};
use crossbeam_channel::Sender;
use git2::{Cred, Error as GitError, RemoteCallbacks};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
///
#[derive(Default, Clone)]
pub struct CallbackStats {
pub push_rejected_msg: Option<(String, String)>,
}
///
#[derive(Clone)]
pub struct Callbacks {
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
stats: Arc<Mutex<CallbackStats>>,
first_call_to_credentials: Arc<AtomicBool>,
}
impl Callbacks {
///
pub fn new(
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
) -> Self {
let stats = Arc::new(Mutex::new(CallbackStats::default()));
Self {
sender,
basic_credential,
stats,
first_call_to_credentials: Arc::new(AtomicBool::new(
true,
)),
}
}
///
pub fn get_stats(&self) -> Result<CallbackStats> {
let stats = self.stats.lock()?;
Ok(stats.clone())
}
///
pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
let mut callbacks = RemoteCallbacks::new();
let this = self.clone();
callbacks.push_transfer_progress(
move |current, total, bytes| {
this.push_transfer_progress(current, total, bytes);
},
);
let this = self.clone();
callbacks.update_tips(move |name, a, b| {
this.update_tips(name, a, b);
true
});
let this = self.clone();
callbacks.transfer_progress(move |p| {
this.transfer_progress(&p);
true
});
let this = self.clone();
callbacks.pack_progress(move |stage, current, total| {
this.pack_progress(stage, total, current);
});
let this = self.clone();
callbacks.push_update_reference(move |reference, msg| {
this.push_update_reference(reference, msg);
Ok(())
});
let this = self.clone();
callbacks.credentials(
move |url, username_from_url, allowed_types| {
this.credentials(
url,
username_from_url,
allowed_types,
)
},
);
callbacks
}
fn push_update_reference(
&self,
reference: &str,
msg: Option<&str>,
) {
log::debug!(
"push_update_reference: '{}' {:?}",
reference,
msg
);
if let Ok(mut stats) = self.stats.lock() {
stats.push_rejected_msg = msg
.map(|msg| (reference.to_string(), msg.to_string()));
}
}
fn pack_progress(
&self,
stage: git2::PackBuilderStage,
total: usize,
current: usize,
) {
log::debug!("packing: {:?} - {}/{}", stage, current, total);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::Packing {
stage,
total,
current,
})
});
}
fn transfer_progress(&self, p: &git2::Progress) {
log::debug!(
"transfer: {}/{}",
p.received_objects(),
p.total_objects()
);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::Transfer {
objects: p.received_objects(),
total_objects: p.total_objects(),
})
});
}
fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
log::debug!("update tips: '{}' [{}] [{}]", name, a, b);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::UpdateTips {
name: name.to_string(),
a: a.into(),
b: b.into(),
})
});
}
fn push_transfer_progress(
&self,
current: usize,
total: usize,
bytes: usize,
) {
log::debug!("progress: {}/{} ({} B)", current, total, bytes,);
self.sender.clone().map(|sender| {
sender.send(ProgressNotification::PushTransfer {
current,
total,
bytes,
})
});
}
// If credentials are bad, we don't ask the user to re-fill their creds. We push an error and they will be able to restart their action (for example a push) and retype their 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/
fn credentials(
&self,
url: &str,
username_from_url: Option<&str>,
allowed_types: git2::CredentialType,
) -> std::result::Result<Cred, GitError> {
log::debug!(
"creds: '{}' {:?} ({:?})",
url,
username_from_url,
allowed_types
);
// This boolean is used to avoid multiple calls to credentials callback.
if self.first_call_to_credentials.load(Ordering::Relaxed) {
self.first_call_to_credentials
.store(false, Ordering::Relaxed);
} else {
return Err(GitError::from_str("Bad credentials."));
}
match &self.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")),
}
}
}

View file

@ -1,5 +1,6 @@
//!
mod callbacks;
pub(crate) mod push;
pub(crate) mod tags;
@ -12,10 +13,10 @@ use crate::{
};
use crossbeam_channel::Sender;
use git2::{BranchType, FetchOptions, Repository};
use push::remote_callbacks;
use scopetime::scope_time;
use utils::bytes2string;
pub use callbacks::Callbacks;
pub use tags::tags_missing_remote;
/// origin
@ -93,10 +94,8 @@ pub(crate) fn fetch(
let mut remote = repo.find_remote(&remote_name)?;
let mut options = FetchOptions::new();
options.remote_callbacks(remote_callbacks(
progress_sender,
basic_credential,
));
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.remote_callbacks(callbacks.callbacks());
remote.fetch(&[branch], Some(&mut options), None)?;

View file

@ -1,17 +1,14 @@
use super::utils;
use crate::{
error::Result,
error::{Error, Result},
progress::ProgressPercent,
sync::{
branch::branch_set_upstream, cred::BasicAuthCredential,
CommitId,
remotes::Callbacks, CommitId,
},
};
use crossbeam_channel::Sender;
use git2::{
Cred, Error as GitError, PackBuilderStage, PushOptions,
RemoteCallbacks,
};
use git2::{PackBuilderStage, PushOptions};
use scopetime::scope_time;
///
@ -108,10 +105,8 @@ pub(crate) fn push(
let mut options = PushOptions::new();
options.remote_callbacks(remote_callbacks(
progress_sender,
basic_credential,
));
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.remote_callbacks(callbacks.callbacks());
options.packbuilder_parallelism(0);
let branch_name = format!("refs/heads/{}", branch);
@ -123,127 +118,21 @@ pub(crate) fn push(
} else {
remote.push(&[branch_name.as_str()], Some(&mut options))?;
}
if let Some((reference, msg)) =
callbacks.get_stats()?.push_rejected_msg
{
return Err(Error::Generic(format!(
"push to '{}' rejected: {}",
reference, msg
)));
}
branch_set_upstream(&repo, branch)?;
Ok(())
}
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn remote_callbacks<'a>(
sender: Option<Sender<ProgressNotification>>,
basic_credential: Option<BasicAuthCredential>,
) -> RemoteCallbacks<'a> {
let mut callbacks = RemoteCallbacks::new();
let sender_clone = sender.clone();
callbacks.push_transfer_progress(move |current, total, bytes| {
log::debug!("progress: {}/{} ({} B)", current, total, bytes,);
sender_clone.clone().map(|sender| {
sender.send(ProgressNotification::PushTransfer {
current,
total,
bytes,
})
});
});
let sender_clone = sender.clone();
callbacks.update_tips(move |name, a, b| {
log::debug!("update tips: '{}' [{}] [{}]", name, a, b);
sender_clone.clone().map(|sender| {
sender.send(ProgressNotification::UpdateTips {
name: name.to_string(),
a: a.into(),
b: b.into(),
})
});
true
});
let sender_clone = sender.clone();
callbacks.transfer_progress(move |p| {
log::debug!(
"transfer: {}/{}",
p.received_objects(),
p.total_objects()
);
sender_clone.clone().map(|sender| {
sender.send(ProgressNotification::Transfer {
objects: p.received_objects(),
total_objects: p.total_objects(),
})
});
true
});
callbacks.pack_progress(move |stage, current, total| {
log::debug!("packing: {:?} - {}/{}", stage, current, total);
sender.clone().map(|sender| {
sender.send(ProgressNotification::Packing {
stage,
total,
current,
})
});
});
let mut first_call_to_credentials = true;
// This boolean is used to avoid multiple calls to credentials callback.
// If credentials are bad, we don't ask the user to re-fill their creds. We push an error and they will be able to restart their action (for example a push) and retype their 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",
)),
}
},
);
callbacks
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,12 +1,10 @@
//!
use super::{
push::{remote_callbacks, AsyncProgress},
utils,
};
use super::{push::AsyncProgress, utils};
use crate::{
error::Result, progress::ProgressPercent,
sync::cred::BasicAuthCredential,
error::Result,
progress::ProgressPercent,
sync::{cred::BasicAuthCredential, remotes::Callbacks},
};
use crossbeam_channel::Sender;
use git2::{Direction, PushOptions};
@ -54,9 +52,10 @@ fn remote_tag_refs(
let repo = utils::repo(repo_path)?;
let mut remote = repo.find_remote(remote)?;
let callbacks = Callbacks::new(None, basic_credential);
let conn = remote.connect_auth(
Direction::Fetch,
Some(remote_callbacks(None, basic_credential)),
Some(callbacks.callbacks()),
None,
)?;
@ -127,10 +126,9 @@ pub fn push_tags(
for (idx, tag) in tags_missing.into_iter().enumerate() {
let mut options = PushOptions::new();
options.remote_callbacks(remote_callbacks(
None,
basic_credential.clone(),
));
let callbacks =
Callbacks::new(None, basic_credential.clone());
options.remote_callbacks(callbacks.callbacks());
options.packbuilder_parallelism(0);
remote.push(&[tag.as_str()], Some(&mut options))?;