gitui/asyncgit/src/sync/remotes/push.rs
Stephan Dilly dad8e8d43d cargo fmt: use hardtabs
since it does not support hard-whitespaces its the only way to make whitespaces consisitent and checked
2021-08-17 14:24:25 +02:00

479 lines
9.3 KiB
Rust

use super::utils;
use crate::{
error::{Error, Result},
progress::ProgressPercent,
sync::{
branch::branch_set_upstream, cred::BasicAuthCredential,
remotes::Callbacks, CommitId,
},
};
use crossbeam_channel::Sender;
use git2::{PackBuilderStage, PushOptions};
use scopetime::scope_time;
///
pub trait AsyncProgress: Clone + Send + Sync {
///
fn is_done(&self) -> bool;
///
fn progress(&self) -> ProgressPercent;
}
///
#[derive(Debug, Clone, PartialEq)]
pub enum ProgressNotification {
///
UpdateTips {
///
name: String,
///
a: CommitId,
///
b: CommitId,
},
///
Transfer {
///
objects: usize,
///
total_objects: usize,
},
///
PushTransfer {
///
current: usize,
///
total: usize,
///
bytes: usize,
},
///
Packing {
///
stage: PackBuilderStage,
///
total: usize,
///
current: usize,
},
///
Done,
}
impl AsyncProgress for ProgressNotification {
fn is_done(&self) -> bool {
*self == Self::Done
}
fn progress(&self) -> ProgressPercent {
match *self {
Self::Packing {
stage,
current,
total,
} => match stage {
PackBuilderStage::AddingObjects
| PackBuilderStage::Deltafication => {
ProgressPercent::new(current, total)
}
},
Self::PushTransfer { current, total, .. } => {
ProgressPercent::new(current, total)
}
Self::Transfer {
objects,
total_objects,
..
} => ProgressPercent::new(objects, total_objects),
_ => ProgressPercent::full(),
}
}
}
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn push(
repo_path: &str,
remote: &str,
branch: &str,
force: bool,
delete: bool,
basic_credential: Option<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<()> {
scope_time!("push");
let repo = utils::repo(repo_path)?;
let mut remote = repo.find_remote(remote)?;
let mut options = PushOptions::new();
let callbacks = Callbacks::new(progress_sender, basic_credential);
options.remote_callbacks(callbacks.callbacks());
options.packbuilder_parallelism(0);
let branch_modifier = match (force, delete) {
(true, true) => "+:",
(false, true) => ":",
(true, false) => "+",
(false, false) => "",
};
let branch_name =
format!("{}refs/heads/{}", branch_modifier, branch);
remote.push(&[branch_name.as_str()], Some(&mut options))?;
if let Some((reference, msg)) =
callbacks.get_stats()?.push_rejected_msg
{
return Err(Error::Generic(format!(
"push to '{}' rejected: {}",
reference, msg
)));
}
if !delete {
branch_set_upstream(&repo, branch)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::{
self,
tests::{
get_commit_ids, repo_clone, repo_init, repo_init_bare,
write_commit_file,
},
};
use git2::Repository;
use std::{fs::File, io::Write, path::Path};
#[test]
fn test_force_push() {
// 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();
sync::commit(
tmp_repo_dir.path().to_str().unwrap(),
"repo_1_commit",
)
.unwrap();
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
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();
sync::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,
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,
false,
None,
None,
)
.is_err(),
false
);
}
#[test]
fn test_force_push_rewrites_history() {
// 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();
sync::stage_add_file(
tmp_repo_dir.path().to_str().unwrap(),
Path::new("temp_file.txt"),
)
.unwrap();
let repo_1_commit = sync::commit(
tmp_repo_dir.path().to_str().unwrap(),
"repo_1_commit",
)
.unwrap();
//NOTE: make sure the commit actually contains that file
assert_eq!(
sync::get_commit_files(
tmp_repo_dir.path().to_str().unwrap(),
repo_1_commit
)
.unwrap()[0]
.path,
String::from("temp_file.txt")
);
let commits = get_commit_ids(&repo, 1);
assert!(commits.contains(&repo_1_commit));
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
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();
sync::stage_add_file(
tmp_other_repo_dir.path().to_str().unwrap(),
Path::new("temp_file.txt"),
)
.unwrap();
let repo_2_commit = sync::commit(
tmp_other_repo_dir.path().to_str().unwrap(),
"repo_2_commit",
)
.unwrap();
let repo_2_parent = other_repo
.find_commit(repo_2_commit.into())
.unwrap()
.parents()
.next()
.unwrap()
.id();
let commits = get_commit_ids(&other_repo, 1);
assert!(commits.contains(&repo_2_commit));
// Attempt a normal push,
// should fail as branches diverged
assert_eq!(
push(
tmp_other_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
false,
None,
None,
)
.is_err(),
true
);
// Check that the other commit is not in upstream,
// a normal push would not rewrite history
let commits = get_commit_ids(&upstream, 1);
assert!(!commits.contains(&repo_2_commit));
// Attempt force push,
// should work as it forces the push through
push(
tmp_other_repo_dir.path().to_str().unwrap(),
"origin",
"master",
true,
false,
None,
None,
)
.unwrap();
let commits = get_commit_ids(&upstream, 1);
assert!(commits.contains(&repo_2_commit));
let new_upstream_parent =
Repository::init_bare(tmp_upstream_dir.path())
.unwrap()
.find_commit(repo_2_commit.into())
.unwrap()
.parents()
.next()
.unwrap()
.id();
assert_eq!(new_upstream_parent, repo_2_parent,);
}
#[test]
fn test_delete_remote_branch() {
// This test mimics the scenario of a user creating a branch, push it, and then remove it on the remote
let (upstream_dir, upstream_repo) = repo_init_bare().unwrap();
let (tmp_repo_dir, repo) =
repo_clone(upstream_dir.path().to_str().unwrap())
.unwrap();
// You need a commit before being able to branch !
let commit_1 = write_commit_file(
&repo,
"temp_file.txt",
"SomeContent",
"Initial commit",
);
let commits = get_commit_ids(&repo, 1);
assert!(commits.contains(&commit_1));
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"master",
false,
false,
None,
None,
)
.unwrap();
// Create the local branch
sync::create_branch(
tmp_repo_dir.path().to_str().unwrap(),
"test_branch",
)
.unwrap();
// Push the local branch
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"test_branch",
false,
false,
None,
None,
)
.unwrap();
// Test if the branch exits on the remote
assert_eq!(
upstream_repo
.branches(None)
.unwrap()
.map(|i| i.unwrap())
.map(|(i, _)| i.name().unwrap().unwrap().to_string())
.filter(|i| i == "test_branch")
.next()
.is_some(),
true
);
// Delete the remote branch
assert_eq!(
push(
tmp_repo_dir.path().to_str().unwrap(),
"origin",
"test_branch",
false,
true,
None,
None,
)
.is_ok(),
true
);
// Test that the branch has be remove from the remote
assert_eq!(
upstream_repo
.branches(None)
.unwrap()
.map(|i| i.unwrap())
.map(|(i, _)| i.name().unwrap().unwrap().to_string())
.filter(|i| i == "test_branch")
.next()
.is_some(),
false
);
}
}