Skip to content

Commit 23944dc

Browse files
author
Stephan Dilly
authored
error if force push was rejected (#810)
* error if force push was rejected
1 parent b6ab0cf commit 23944dc

File tree

5 files changed

+252
-142
lines changed

5 files changed

+252
-142
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- new `undo-last-commit` command [[@remique](https://github.com/remique)] ([#758](https://github.com/extrawurst/gitui/issues/758))
1212
- taglist: show arrow-symbol on tags not present on origin [[@cruessler](https://github.com/cruessler)] ([#776](https://github.com/extrawurst/gitui/issues/776))
1313
- new quit key `[q]` ([#771](https://github.com/extrawurst/gitui/issues/771))
14+
- proper error message if remote rejects force push ([#801](https://github.com/extrawurst/gitui/issues/801))
1415

1516
## Fixed
1617
- openssl vendoring broken on macos ([#772](https://github.com/extrawurst/gitui/issues/772))
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#![allow(dead_code)]
2+
3+
use super::push::ProgressNotification;
4+
use crate::{error::Result, sync::cred::BasicAuthCredential};
5+
use crossbeam_channel::Sender;
6+
use git2::{Cred, Error as GitError, RemoteCallbacks};
7+
use std::sync::{
8+
atomic::{AtomicBool, Ordering},
9+
Arc, Mutex,
10+
};
11+
12+
///
13+
#[derive(Default, Clone)]
14+
pub struct CallbackStats {
15+
pub push_rejected_msg: Option<(String, String)>,
16+
}
17+
18+
///
19+
#[derive(Clone)]
20+
pub struct Callbacks {
21+
sender: Option<Sender<ProgressNotification>>,
22+
basic_credential: Option<BasicAuthCredential>,
23+
stats: Arc<Mutex<CallbackStats>>,
24+
first_call_to_credentials: Arc<AtomicBool>,
25+
}
26+
27+
impl Callbacks {
28+
///
29+
pub fn new(
30+
sender: Option<Sender<ProgressNotification>>,
31+
basic_credential: Option<BasicAuthCredential>,
32+
) -> Self {
33+
let stats = Arc::new(Mutex::new(CallbackStats::default()));
34+
35+
Self {
36+
sender,
37+
basic_credential,
38+
stats,
39+
first_call_to_credentials: Arc::new(AtomicBool::new(
40+
true,
41+
)),
42+
}
43+
}
44+
45+
///
46+
pub fn get_stats(&self) -> Result<CallbackStats> {
47+
let stats = self.stats.lock()?;
48+
Ok(stats.clone())
49+
}
50+
51+
///
52+
pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
53+
let mut callbacks = RemoteCallbacks::new();
54+
55+
let this = self.clone();
56+
callbacks.push_transfer_progress(
57+
move |current, total, bytes| {
58+
this.push_transfer_progress(current, total, bytes);
59+
},
60+
);
61+
62+
let this = self.clone();
63+
callbacks.update_tips(move |name, a, b| {
64+
this.update_tips(name, a, b);
65+
true
66+
});
67+
68+
let this = self.clone();
69+
callbacks.transfer_progress(move |p| {
70+
this.transfer_progress(&p);
71+
true
72+
});
73+
74+
let this = self.clone();
75+
callbacks.pack_progress(move |stage, current, total| {
76+
this.pack_progress(stage, total, current);
77+
});
78+
79+
let this = self.clone();
80+
callbacks.push_update_reference(move |reference, msg| {
81+
this.push_update_reference(reference, msg);
82+
Ok(())
83+
});
84+
85+
let this = self.clone();
86+
callbacks.credentials(
87+
move |url, username_from_url, allowed_types| {
88+
this.credentials(
89+
url,
90+
username_from_url,
91+
allowed_types,
92+
)
93+
},
94+
);
95+
96+
callbacks
97+
}
98+
99+
fn push_update_reference(
100+
&self,
101+
reference: &str,
102+
msg: Option<&str>,
103+
) {
104+
log::debug!(
105+
"push_update_reference: '{}' {:?}",
106+
reference,
107+
msg
108+
);
109+
110+
if let Ok(mut stats) = self.stats.lock() {
111+
stats.push_rejected_msg = msg
112+
.map(|msg| (reference.to_string(), msg.to_string()));
113+
}
114+
}
115+
116+
fn pack_progress(
117+
&self,
118+
stage: git2::PackBuilderStage,
119+
total: usize,
120+
current: usize,
121+
) {
122+
log::debug!("packing: {:?} - {}/{}", stage, current, total);
123+
self.sender.clone().map(|sender| {
124+
sender.send(ProgressNotification::Packing {
125+
stage,
126+
total,
127+
current,
128+
})
129+
});
130+
}
131+
132+
fn transfer_progress(&self, p: &git2::Progress) {
133+
log::debug!(
134+
"transfer: {}/{}",
135+
p.received_objects(),
136+
p.total_objects()
137+
);
138+
self.sender.clone().map(|sender| {
139+
sender.send(ProgressNotification::Transfer {
140+
objects: p.received_objects(),
141+
total_objects: p.total_objects(),
142+
})
143+
});
144+
}
145+
146+
fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
147+
log::debug!("update tips: '{}' [{}] [{}]", name, a, b);
148+
self.sender.clone().map(|sender| {
149+
sender.send(ProgressNotification::UpdateTips {
150+
name: name.to_string(),
151+
a: a.into(),
152+
b: b.into(),
153+
})
154+
});
155+
}
156+
157+
fn push_transfer_progress(
158+
&self,
159+
current: usize,
160+
total: usize,
161+
bytes: usize,
162+
) {
163+
log::debug!("progress: {}/{} ({} B)", current, total, bytes,);
164+
self.sender.clone().map(|sender| {
165+
sender.send(ProgressNotification::PushTransfer {
166+
current,
167+
total,
168+
bytes,
169+
})
170+
});
171+
}
172+
173+
// 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.
174+
// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
175+
// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
176+
// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
177+
fn credentials(
178+
&self,
179+
url: &str,
180+
username_from_url: Option<&str>,
181+
allowed_types: git2::CredentialType,
182+
) -> std::result::Result<Cred, GitError> {
183+
log::debug!(
184+
"creds: '{}' {:?} ({:?})",
185+
url,
186+
username_from_url,
187+
allowed_types
188+
);
189+
190+
// This boolean is used to avoid multiple calls to credentials callback.
191+
if self.first_call_to_credentials.load(Ordering::Relaxed) {
192+
self.first_call_to_credentials
193+
.store(false, Ordering::Relaxed);
194+
} else {
195+
return Err(GitError::from_str("Bad credentials."));
196+
}
197+
198+
match &self.basic_credential {
199+
_ if allowed_types.is_ssh_key() => {
200+
match username_from_url {
201+
Some(username) => {
202+
Cred::ssh_key_from_agent(username)
203+
}
204+
None => Err(GitError::from_str(
205+
" Couldn't extract username from url.",
206+
)),
207+
}
208+
}
209+
Some(BasicAuthCredential {
210+
username: Some(user),
211+
password: Some(pwd),
212+
}) if allowed_types.is_user_pass_plaintext() => {
213+
Cred::userpass_plaintext(user, pwd)
214+
}
215+
Some(BasicAuthCredential {
216+
username: Some(user),
217+
password: _,
218+
}) if allowed_types.is_username() => Cred::username(user),
219+
_ if allowed_types.is_default() => Cred::default(),
220+
_ => Err(GitError::from_str("Couldn't find credentials")),
221+
}
222+
}
223+
}

asyncgit/src/sync/remotes/mod.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//!
22
3+
mod callbacks;
34
pub(crate) mod push;
45
pub(crate) mod tags;
56

@@ -12,10 +13,10 @@ use crate::{
1213
};
1314
use crossbeam_channel::Sender;
1415
use git2::{BranchType, FetchOptions, Repository};
15-
use push::remote_callbacks;
1616
use scopetime::scope_time;
1717
use utils::bytes2string;
1818

19+
pub use callbacks::Callbacks;
1920
pub use tags::tags_missing_remote;
2021

2122
/// origin
@@ -93,10 +94,8 @@ pub(crate) fn fetch(
9394
let mut remote = repo.find_remote(&remote_name)?;
9495

9596
let mut options = FetchOptions::new();
96-
options.remote_callbacks(remote_callbacks(
97-
progress_sender,
98-
basic_credential,
99-
));
97+
let callbacks = Callbacks::new(progress_sender, basic_credential);
98+
options.remote_callbacks(callbacks.callbacks());
10099

101100
remote.fetch(&[branch], Some(&mut options), None)?;
102101

0 commit comments

Comments
 (0)