mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 00:48:35 +00:00
error if force push was rejected (#810)
* error if force push was rejected
This commit is contained in:
parent
b6ab0cfa34
commit
23944dc608
5 changed files with 252 additions and 142 deletions
|
|
@ -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))
|
||||
|
|
|
|||
223
asyncgit/src/sync/remotes/callbacks.rs
Normal file
223
asyncgit/src/sync/remotes/callbacks.rs
Normal 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")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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))?;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue