mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Support force push (#421)
This commit is contained in:
parent
301a3a1b87
commit
6c974ee1d3
11 changed files with 348 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue