Skip to content

Commit 6c974ee

Browse files
authored
Support force push (#421)
1 parent 301a3a1 commit 6c974ee

File tree

11 files changed

+348
-23
lines changed

11 files changed

+348
-23
lines changed

assets/vim_style_key_config.ron

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
select_branch: ( code: Char('b'), modifiers: ( bits: 0,),),
6969
delete_branch: ( code: Char('D'), modifiers: ( bits: 1,),),
7070
push: ( code: Char('p'), modifiers: ( bits: 0,),),
71+
force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
7172
fetch: ( code: Char('f'), modifiers: ( bits: 0,),),
7273

7374
//removed in 0.11

asyncgit/src/push.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ pub struct PushRequest {
9090
///
9191
pub branch: String,
9292
///
93+
pub force: bool,
94+
///
9395
pub basic_credential: Option<BasicAuthCredential>,
9496
}
9597

@@ -164,8 +166,9 @@ impl AsyncPush {
164166
CWD,
165167
params.remote.as_str(),
166168
params.branch.as_str(),
169+
params.force,
167170
params.basic_credential,
168-
progress_sender.clone(),
171+
Some(progress_sender.clone()),
169172
);
170173

171174
progress_sender

asyncgit/src/sync/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ mod tests {
124124
Ok((td, repo))
125125
}
126126

127+
/// Same as repo_init, but the repo is a bare repo (--bare)
128+
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
129+
let tmp_repo_dir = TempDir::new()?;
130+
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
131+
Ok((tmp_repo_dir, bare_repo))
132+
}
133+
127134
/// helper returning amount of files with changes in the (wd,stage)
128135
pub fn get_statuses(repo_path: &str) -> (usize, usize) {
129136
(

asyncgit/src/sync/remotes.rs

Lines changed: 244 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ pub fn push(
111111
repo_path: &str,
112112
remote: &str,
113113
branch: &str,
114+
force: bool,
114115
basic_credential: Option<BasicAuthCredential>,
115-
progress_sender: Sender<ProgressNotification>,
116+
progress_sender: Option<Sender<ProgressNotification>>,
116117
) -> Result<()> {
117118
scope_time!("push");
118119

@@ -122,15 +123,20 @@ pub fn push(
122123
let mut options = PushOptions::new();
123124

124125
options.remote_callbacks(remote_callbacks(
125-
Some(progress_sender),
126+
progress_sender,
126127
basic_credential,
127128
));
128129
options.packbuilder_parallelism(0);
129130

130131
let branch_name = format!("refs/heads/{}", branch);
131-
132-
remote.push(&[branch_name.as_str()], Some(&mut options))?;
133-
132+
if force {
133+
remote.push(
134+
&[String::from("+") + &branch_name],
135+
Some(&mut options),
136+
)?;
137+
} else {
138+
remote.push(&[branch_name.as_str()], Some(&mut options))?;
139+
}
134140
branch_set_upstream(&repo, branch)?;
135141

136142
Ok(())
@@ -306,4 +312,237 @@ mod tests {
306312
.unwrap();
307313
assert_eq!(first, String::from("origin"));
308314
}
315+
316+
#[test]
317+
fn test_force_push() {
318+
use super::push;
319+
use std::fs::File;
320+
use std::io::Write;
321+
322+
use crate::sync::commit::commit;
323+
use crate::sync::tests::{repo_init, repo_init_bare};
324+
325+
// This test mimics the scenario of 2 people having 2
326+
// local branches and both modifying the same file then
327+
// both pushing, sequentially
328+
329+
let (tmp_repo_dir, repo) = repo_init().unwrap();
330+
let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
331+
let (tmp_upstream_dir, _) = repo_init_bare().unwrap();
332+
333+
repo.remote(
334+
"origin",
335+
tmp_upstream_dir.path().to_str().unwrap(),
336+
)
337+
.unwrap();
338+
339+
other_repo
340+
.remote(
341+
"origin",
342+
tmp_upstream_dir.path().to_str().unwrap(),
343+
)
344+
.unwrap();
345+
346+
let tmp_repo_file_path =
347+
tmp_repo_dir.path().join("temp_file.txt");
348+
let mut tmp_repo_file =
349+
File::create(tmp_repo_file_path).unwrap();
350+
writeln!(tmp_repo_file, "TempSomething").unwrap();
351+
352+
commit(
353+
tmp_repo_dir.path().to_str().unwrap(),
354+
"repo_1_commit",
355+
)
356+
.unwrap();
357+
358+
push(
359+
tmp_repo_dir.path().to_str().unwrap(),
360+
"origin",
361+
"master",
362+
false,
363+
None,
364+
None,
365+
)
366+
.unwrap();
367+
368+
let tmp_other_repo_file_path =
369+
tmp_other_repo_dir.path().join("temp_file.txt");
370+
let mut tmp_other_repo_file =
371+
File::create(tmp_other_repo_file_path).unwrap();
372+
writeln!(tmp_other_repo_file, "TempElse").unwrap();
373+
374+
commit(
375+
tmp_other_repo_dir.path().to_str().unwrap(),
376+
"repo_2_commit",
377+
)
378+
.unwrap();
379+
380+
// Attempt a normal push,
381+
// should fail as branches diverged
382+
assert_eq!(
383+
push(
384+
tmp_other_repo_dir.path().to_str().unwrap(),
385+
"origin",
386+
"master",
387+
false,
388+
None,
389+
None,
390+
)
391+
.is_err(),
392+
true
393+
);
394+
395+
// Attempt force push,
396+
// should work as it forces the push through
397+
assert_eq!(
398+
push(
399+
tmp_other_repo_dir.path().to_str().unwrap(),
400+
"origin",
401+
"master",
402+
true,
403+
None,
404+
None,
405+
)
406+
.is_err(),
407+
false
408+
);
409+
}
410+
411+
#[test]
412+
fn test_force_push_rewrites_history() {
413+
use super::push;
414+
use std::fs::File;
415+
use std::io::Write;
416+
417+
use crate::sync::commit::commit;
418+
use crate::sync::tests::{repo_init, repo_init_bare};
419+
use crate::sync::LogWalker;
420+
421+
// This test mimics the scenario of 2 people having 2
422+
// local branches and both modifying the same file then
423+
// both pushing, sequentially
424+
425+
let (tmp_repo_dir, repo) = repo_init().unwrap();
426+
let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
427+
let (tmp_upstream_dir, upstream) = repo_init_bare().unwrap();
428+
429+
repo.remote(
430+
"origin",
431+
tmp_upstream_dir.path().to_str().unwrap(),
432+
)
433+
.unwrap();
434+
435+
other_repo
436+
.remote(
437+
"origin",
438+
tmp_upstream_dir.path().to_str().unwrap(),
439+
)
440+
.unwrap();
441+
442+
let tmp_repo_file_path =
443+
tmp_repo_dir.path().join("temp_file.txt");
444+
let mut tmp_repo_file =
445+
File::create(tmp_repo_file_path).unwrap();
446+
writeln!(tmp_repo_file, "TempSomething").unwrap();
447+
448+
commit(
449+
tmp_repo_dir.path().to_str().unwrap(),
450+
"repo_1_commit",
451+
)
452+
.unwrap();
453+
454+
let mut repo_commit_ids = Vec::<CommitId>::new();
455+
LogWalker::new(&repo).read(&mut repo_commit_ids, 1).unwrap();
456+
457+
push(
458+
tmp_repo_dir.path().to_str().unwrap(),
459+
"origin",
460+
"master",
461+
false,
462+
None,
463+
None,
464+
)
465+
.unwrap();
466+
467+
let upstream_parent = upstream
468+
.find_commit((repo_commit_ids[0]).into())
469+
.unwrap()
470+
.parents()
471+
.next()
472+
.unwrap()
473+
.id();
474+
475+
let tmp_other_repo_file_path =
476+
tmp_other_repo_dir.path().join("temp_file.txt");
477+
let mut tmp_other_repo_file =
478+
File::create(tmp_other_repo_file_path).unwrap();
479+
writeln!(tmp_other_repo_file, "TempElse").unwrap();
480+
481+
commit(
482+
tmp_other_repo_dir.path().to_str().unwrap(),
483+
"repo_2_commit",
484+
)
485+
.unwrap();
486+
let mut other_repo_commit_ids = Vec::<CommitId>::new();
487+
LogWalker::new(&other_repo)
488+
.read(&mut other_repo_commit_ids, 1)
489+
.unwrap();
490+
491+
// Attempt a normal push,
492+
// should fail as branches diverged
493+
assert_eq!(
494+
push(
495+
tmp_other_repo_dir.path().to_str().unwrap(),
496+
"origin",
497+
"master",
498+
false,
499+
None,
500+
None,
501+
)
502+
.is_err(),
503+
true
504+
);
505+
506+
// Check that the other commit is not in upstream,
507+
// a normal push would not rewrite history
508+
let mut commit_ids = Vec::<CommitId>::new();
509+
LogWalker::new(&upstream).read(&mut commit_ids, 1).unwrap();
510+
assert_eq!(commit_ids.contains(&repo_commit_ids[0]), true);
511+
512+
// Attempt force push,
513+
// should work as it forces the push through
514+
assert_eq!(
515+
push(
516+
tmp_other_repo_dir.path().to_str().unwrap(),
517+
"origin",
518+
"master",
519+
true,
520+
None,
521+
None,
522+
)
523+
.is_err(),
524+
false
525+
);
526+
527+
commit_ids.clear();
528+
LogWalker::new(&upstream).read(&mut commit_ids, 1).unwrap();
529+
530+
// Check that only the other repo commit is now in upstream
531+
assert_eq!(
532+
commit_ids.contains(&other_repo_commit_ids[0]),
533+
true
534+
);
535+
536+
assert_eq!(
537+
upstream
538+
.find_commit((commit_ids[0]).into())
539+
.unwrap()
540+
.parents()
541+
.next()
542+
.unwrap()
543+
.id()
544+
== upstream_parent,
545+
true
546+
);
547+
}
309548
}

src/app.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,10 @@ impl App {
493493
self.select_branch_popup.hide();
494494
}
495495
}
496+
Action::ForcePush(branch, force) => self
497+
.queue
498+
.borrow_mut()
499+
.push_back(InternalEvent::Push(branch, force)),
496500
},
497501
InternalEvent::ConfirmAction(action) => {
498502
self.reset.open(action)?;
@@ -533,8 +537,8 @@ impl App {
533537
self.file_to_open = path;
534538
flags.insert(NeedsUpdate::COMMANDS)
535539
}
536-
InternalEvent::Push(branch) => {
537-
self.push_popup.push(branch)?;
540+
InternalEvent::Push(branch, force) => {
541+
self.push_popup.push(branch, force)?;
538542
flags.insert(NeedsUpdate::ALL)
539543
}
540544
};

0 commit comments

Comments
 (0)