diff --git a/assets/vim_style_key_config.ron b/assets/vim_style_key_config.ron index bd193b3e..b1939ffc 100644 --- a/assets/vim_style_key_config.ron +++ b/assets/vim_style_key_config.ron @@ -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 diff --git a/asyncgit/src/push.rs b/asyncgit/src/push.rs index de0f8b0a..42d8ebc9 100644 --- a/asyncgit/src/push.rs +++ b/asyncgit/src/push.rs @@ -90,6 +90,8 @@ pub struct PushRequest { /// pub branch: String, /// + pub force: bool, + /// pub basic_credential: Option, } @@ -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 diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index c6f5d193..96c8cbd0 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -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) { ( diff --git a/asyncgit/src/sync/remotes.rs b/asyncgit/src/sync/remotes.rs index 65dd3094..aa950664 100644 --- a/asyncgit/src/sync/remotes.rs +++ b/asyncgit/src/sync/remotes.rs @@ -111,8 +111,9 @@ pub fn push( repo_path: &str, remote: &str, branch: &str, + force: bool, basic_credential: Option, - progress_sender: Sender, + progress_sender: Option>, ) -> 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::::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::::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::::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 + ); + } } diff --git a/src/app.rs b/src/app.rs index 2ce3b6be..4d10352f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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) } }; diff --git a/src/components/push.rs b/src/components/push.rs index 41ec9cb5..f217d7c9 100644 --- a/src/components/push.rs +++ b/src/components/push.rs @@ -33,6 +33,7 @@ use tui::{ /// pub struct PushComponent { visible: bool, + force: bool, git_push: AsyncPush, progress: Option, 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, + 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(); diff --git a/src/components/reset.rs b/src/components/reset.rs index be8e76d5..e24fec4d 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -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"), + ), + ), }; } diff --git a/src/keys.rs b/src/keys.rs index 2f3bcce1..153869bf 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -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()}, } } diff --git a/src/queue.rs b/src/queue.rs index 678baf6b..dc9c6503 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -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), /// - Push(String), + Push(String, bool), } /// diff --git a/src/strings.rs b/src/strings.rs index 617a738c..1746279c 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -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, + ) + } } diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 2a61ee07..ad00a65b 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -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();