Support force push (#421)

This commit is contained in:
Richard Menzies 2021-02-23 10:02:16 +00:00 committed by GitHub
parent 301a3a1b87
commit 6c974ee1d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 348 additions and 23 deletions

View file

@ -68,6 +68,7 @@
select_branch: ( code: Char('b'), modifiers: ( bits: 0,),),
delete_branch: ( code: Char('D'), modifiers: ( bits: 1,),),
push: ( code: Char('p'), modifiers: ( bits: 0,),),
force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
fetch: ( code: Char('f'), modifiers: ( bits: 0,),),
//removed in 0.11

View file

@ -90,6 +90,8 @@ pub struct PushRequest {
///
pub branch: String,
///
pub force: bool,
///
pub basic_credential: Option<BasicAuthCredential>,
}
@ -164,8 +166,9 @@ impl AsyncPush {
CWD,
params.remote.as_str(),
params.branch.as_str(),
params.force,
params.basic_credential,
progress_sender.clone(),
Some(progress_sender.clone()),
);
progress_sender

View file

@ -124,6 +124,13 @@ mod tests {
Ok((td, repo))
}
/// Same as repo_init, but the repo is a bare repo (--bare)
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
let tmp_repo_dir = TempDir::new()?;
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
Ok((tmp_repo_dir, bare_repo))
}
/// helper returning amount of files with changes in the (wd,stage)
pub fn get_statuses(repo_path: &str) -> (usize, usize) {
(

View file

@ -111,8 +111,9 @@ pub fn push(
repo_path: &str,
remote: &str,
branch: &str,
force: bool,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Sender<ProgressNotification>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<()> {
scope_time!("push");
@ -122,15 +123,20 @@ pub fn push(
let mut options = PushOptions::new();
options.remote_callbacks(remote_callbacks(
Some(progress_sender),
progress_sender,
basic_credential,
));
options.packbuilder_parallelism(0);
let branch_name = format!("refs/heads/{}", branch);
remote.push(&[branch_name.as_str()], Some(&mut options))?;
if force {
remote.push(
&[String::from("+") + &branch_name],
Some(&mut options),
)?;
} else {
remote.push(&[branch_name.as_str()], Some(&mut options))?;
}
branch_set_upstream(&repo, branch)?;
Ok(())
@ -306,4 +312,237 @@ mod tests {
.unwrap();
assert_eq!(first, String::from("origin"));
}
#[test]
fn test_force_push() {
use super::push;
use std::fs::File;
use std::io::Write;
use crate::sync::commit::commit;
use crate::sync::tests::{repo_init, repo_init_bare};
// This test mimics the scenario of 2 people having 2
// local branches and both modifying the same file then
// both pushing, sequentially
let (tmp_repo_dir, repo) = repo_init().unwrap();
let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
let (tmp_upstream_dir, _) = repo_init_bare().unwrap();
repo.remote(
"origin",
tmp_upstream_dir.path().to_str().unwrap(),
)
.unwrap();
other_repo
.remote(
"origin",
tmp_upstream_dir.path().to_str().unwrap(),
)
.unwrap();
let tmp_repo_file_path =
tmp_repo_dir.path().join("temp_file.txt");
let mut tmp_repo_file =
File::create(tmp_repo_file_path).unwrap();
writeln!(tmp_repo_file, "TempSomething").unwrap();
commit(
tmp_repo_dir.path().to_str().unwrap(),
"repo_1_commit",
)
.unwrap();
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.unwrap();
let tmp_other_repo_file_path =
tmp_other_repo_dir.path().join("temp_file.txt");
let mut tmp_other_repo_file =
File::create(tmp_other_repo_file_path).unwrap();
writeln!(tmp_other_repo_file, "TempElse").unwrap();
commit(
tmp_other_repo_dir.path().to_str().unwrap(),
"repo_2_commit",
)
.unwrap();
// Attempt a normal push,
// should fail as branches diverged
assert_eq!(
push(
tmp_other_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.is_err(),
true
);
// Attempt force push,
// should work as it forces the push through
assert_eq!(
push(
tmp_other_repo_dir.path().to_str().unwrap(),
"origin",
"master",
true,
None,
None,
)
.is_err(),
false
);
}
#[test]
fn test_force_push_rewrites_history() {
use super::push;
use std::fs::File;
use std::io::Write;
use crate::sync::commit::commit;
use crate::sync::tests::{repo_init, repo_init_bare};
use crate::sync::LogWalker;
// This test mimics the scenario of 2 people having 2
// local branches and both modifying the same file then
// both pushing, sequentially
let (tmp_repo_dir, repo) = repo_init().unwrap();
let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
let (tmp_upstream_dir, upstream) = repo_init_bare().unwrap();
repo.remote(
"origin",
tmp_upstream_dir.path().to_str().unwrap(),
)
.unwrap();
other_repo
.remote(
"origin",
tmp_upstream_dir.path().to_str().unwrap(),
)
.unwrap();
let tmp_repo_file_path =
tmp_repo_dir.path().join("temp_file.txt");
let mut tmp_repo_file =
File::create(tmp_repo_file_path).unwrap();
writeln!(tmp_repo_file, "TempSomething").unwrap();
commit(
tmp_repo_dir.path().to_str().unwrap(),
"repo_1_commit",
)
.unwrap();
let mut repo_commit_ids = Vec::<CommitId>::new();
LogWalker::new(&repo).read(&mut repo_commit_ids, 1).unwrap();
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.unwrap();
let upstream_parent = upstream
.find_commit((repo_commit_ids[0]).into())
.unwrap()
.parents()
.next()
.unwrap()
.id();
let tmp_other_repo_file_path =
tmp_other_repo_dir.path().join("temp_file.txt");
let mut tmp_other_repo_file =
File::create(tmp_other_repo_file_path).unwrap();
writeln!(tmp_other_repo_file, "TempElse").unwrap();
commit(
tmp_other_repo_dir.path().to_str().unwrap(),
"repo_2_commit",
)
.unwrap();
let mut other_repo_commit_ids = Vec::<CommitId>::new();
LogWalker::new(&other_repo)
.read(&mut other_repo_commit_ids, 1)
.unwrap();
// Attempt a normal push,
// should fail as branches diverged
assert_eq!(
push(
tmp_other_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
None,
None,
)
.is_err(),
true
);
// Check that the other commit is not in upstream,
// a normal push would not rewrite history
let mut commit_ids = Vec::<CommitId>::new();
LogWalker::new(&upstream).read(&mut commit_ids, 1).unwrap();
assert_eq!(commit_ids.contains(&repo_commit_ids[0]), true);
// Attempt force push,
// should work as it forces the push through
assert_eq!(
push(
tmp_other_repo_dir.path().to_str().unwrap(),
"origin",
"master",
true,
None,
None,
)
.is_err(),
false
);
commit_ids.clear();
LogWalker::new(&upstream).read(&mut commit_ids, 1).unwrap();
// Check that only the other repo commit is now in upstream
assert_eq!(
commit_ids.contains(&other_repo_commit_ids[0]),
true
);
assert_eq!(
upstream
.find_commit((commit_ids[0]).into())
.unwrap()
.parents()
.next()
.unwrap()
.id()
== upstream_parent,
true
);
}
}

View file

@ -493,6 +493,10 @@ impl App {
self.select_branch_popup.hide();
}
}
Action::ForcePush(branch, force) => self
.queue
.borrow_mut()
.push_back(InternalEvent::Push(branch, force)),
},
InternalEvent::ConfirmAction(action) => {
self.reset.open(action)?;
@ -533,8 +537,8 @@ impl App {
self.file_to_open = path;
flags.insert(NeedsUpdate::COMMANDS)
}
InternalEvent::Push(branch) => {
self.push_popup.push(branch)?;
InternalEvent::Push(branch, force) => {
self.push_popup.push(branch, force)?;
flags.insert(NeedsUpdate::ALL)
}
};

View file

@ -33,6 +33,7 @@ use tui::{
///
pub struct PushComponent {
visible: bool,
force: bool,
git_push: AsyncPush,
progress: Option<PushProgress>,
pending: bool,
@ -53,6 +54,7 @@ impl PushComponent {
) -> Self {
Self {
queue: queue.clone(),
force: false,
pending: false,
visible: false,
branch: String::new(),
@ -68,8 +70,13 @@ impl PushComponent {
}
///
pub fn push(&mut self, branch: String) -> Result<()> {
pub fn push(
&mut self,
branch: String,
force: bool,
) -> Result<()> {
self.branch = branch;
self.force = force;
self.show()?;
if need_username_password()? {
let cred =
@ -77,25 +84,27 @@ impl PushComponent {
BasicAuthCredential::new(None, None)
});
if cred.is_complete() {
self.push_to_remote(Some(cred))
self.push_to_remote(Some(cred), force)
} else {
self.input_cred.set_cred(cred);
self.input_cred.show()
}
} else {
self.push_to_remote(None)
self.push_to_remote(None, force)
}
}
fn push_to_remote(
&mut self,
cred: Option<BasicAuthCredential>,
force: bool,
) -> Result<()> {
self.pending = true;
self.progress = None;
self.git_push.request(PushRequest {
remote: get_first_remote(CWD)?,
branch: self.branch.clone(),
force,
basic_credential: cred,
})?;
Ok(())
@ -181,7 +190,11 @@ impl DrawableComponent for PushComponent {
.block(
Block::default()
.title(Span::styled(
strings::PUSH_POPUP_MSG,
if self.force {
strings::FORCE_PUSH_POPUP_MSG
} else {
strings::PUSH_POPUP_MSG
},
self.theme.title(true),
))
.borders(Borders::ALL)
@ -233,9 +246,10 @@ impl Component for PushComponent {
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.push_to_remote(
Some(self.input_cred.get_cred().clone()),
self.force,
)?;
self.input_cred.hide();
} else {
self.hide();

View file

@ -38,7 +38,7 @@ impl DrawableComponent for ResetComponent {
self.theme.text_danger(),
);
let area = ui::centered_rect(30, 20, f.size());
let area = ui::centered_rect(50, 20, f.size());
f.render_widget(Clear, area);
f.render_widget(
popup_paragraph(&title, txt, &self.theme, true),
@ -160,6 +160,15 @@ impl ResetComponent {
branch_ref,
),
),
Action::ForcePush(branch, _force) => (
strings::confirm_title_force_push(
&self.key_config,
),
strings::confirm_msg_force_push(
&self.key_config,
branch.rsplit('/').next().expect("There was no / in the head reference which is impossible in git"),
),
),
};
}

View file

@ -63,6 +63,7 @@ pub struct KeyConfig {
pub select_branch: KeyEvent,
pub delete_branch: KeyEvent,
pub push: KeyEvent,
pub force_push: KeyEvent,
pub fetch: KeyEvent,
}
@ -116,6 +117,7 @@ impl Default for KeyConfig {
select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::NONE},
delete_branch: KeyEvent{code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()},
force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT},
fetch: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
}
}

View file

@ -29,6 +29,7 @@ pub enum Action {
ResetHunk(String, u64),
StashDrop(CommitId),
DeleteBranch(String),
ForcePush(String, bool),
}
///
@ -60,7 +61,7 @@ pub enum InternalEvent {
///
OpenExternalEditor(Option<String>),
///
Push(String),
Push(String, bool),
}
///

View file

@ -5,6 +5,7 @@ pub mod order {
}
pub static PUSH_POPUP_MSG: &str = "Push";
pub static FORCE_PUSH_POPUP_MSG: &str = "Force Push";
pub static PUSH_POPUP_PROGRESS_NONE: &str = "preparing...";
pub static PUSH_POPUP_STATES_ADDING: &str = "adding objects (1/3)";
pub static PUSH_POPUP_STATES_DELTAS: &str = "deltas (2/3)";
@ -110,6 +111,20 @@ pub fn confirm_msg_delete_branch(
) -> String {
format!("Confirm deleting branch: '{}' ?", branch_ref)
}
pub fn confirm_title_force_push(
_key_config: &SharedKeyConfig,
) -> String {
"Force Push".to_string()
}
pub fn confirm_msg_force_push(
_key_config: &SharedKeyConfig,
branch_ref: &str,
) -> String {
format!(
"Confirm force push to branch '{}' ? This may rewrite history.",
branch_ref
)
}
pub fn log_title(_key_config: &SharedKeyConfig) -> String {
"Commit".to_string()
}
@ -809,4 +824,16 @@ pub mod commands {
CMD_GROUP_GENERAL,
)
}
pub fn status_force_push(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Force Push [{}]",
key_config.get_hint(key_config.force_push),
),
"force push to origin",
CMD_GROUP_GENERAL,
)
}
}

View file

@ -6,7 +6,7 @@ use crate::{
DiffComponent, DrawableComponent, FileTreeItemKind,
},
keys::SharedKeyConfig,
queue::{InternalEvent, Queue, ResetItem},
queue::{Action, InternalEvent, Queue, ResetItem},
strings::{self, order},
ui::style::SharedTheme,
};
@ -375,11 +375,19 @@ impl Status {
}
}
fn push(&self) {
fn push(&self, force: bool) {
if let Some(branch) = self.git_branch_name.last() {
self.queue
.borrow_mut()
.push_back(InternalEvent::Push(branch));
if force {
self.queue.borrow_mut().push_back(
InternalEvent::ConfirmAction(Action::ForcePush(
branch, force,
)),
);
} else {
self.queue
.borrow_mut()
.push_back(InternalEvent::Push(branch, force));
}
}
}
@ -447,6 +455,13 @@ impl Component for Status {
self.can_push(),
true,
));
out.push(CommandInfo::new(
strings::commands::status_force_push(
&self.key_config,
),
self.can_push(),
true,
));
}
{
@ -553,8 +568,11 @@ impl Component for Status {
.borrow_mut()
.push_back(InternalEvent::SelectBranch);
Ok(true)
} else if k == self.key_config.force_push {
self.push(true);
Ok(true)
} else if k == self.key_config.push {
self.push();
self.push(false);
Ok(true)
} else if k == self.key_config.fetch {
self.fetch();