use super::commit::{git_text, remove_settled, wrapped_commit};
use super::held_lock_support::{Ended, run_acting_on_line};
use super::merge_isolation::{copy_of, diverged, snapshot, stage_unrelated_work};
use super::support::{Fixture, Observed, direct, fixture, git, observe, repository, without_wait_notices};
use std::path::{Path, PathBuf};
fn diverged_with_remote(fixture: &Fixture) -> PathBuf {
let repo: PathBuf = diverged(fixture, "dirty");
let remote: PathBuf = fixture.root.join("remote.git");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", "remote.git"]);
git(fixture, repo.as_path(), &["remote", "add", "origin", remote.to_str().expect("UTF-8")]);
git(fixture, repo.as_path(), &["push", "--quiet", "origin", "feature:main"]);
git(fixture, repo.as_path(), &["config", "branch.main.remote", "origin"]);
git(fixture, repo.as_path(), &["config", "branch.main.merge", "refs/heads/main"]);
return repo;
}
fn run(fixture: &Fixture, repo: &Path, arguments: &[&str]) -> Observed {
let observed: Observed = observe(&mut wrapped_commit(fixture, repo, arguments), b"");
return Observed { stderr: without_wait_notices(observed.stderr.as_slice()), ..observed };
}
fn native(fixture: &Fixture, repo: &Path, arguments: &[&str]) -> Observed {
return observe(direct(fixture).current_dir(repo).args(arguments), b"");
}
fn commit_shape(fixture: &Fixture, repo: &Path) -> Vec<String> {
return vec![
git_text(fixture, repo, &["rev-parse", "HEAD^{tree}"]),
git_text(fixture, repo, &["rev-parse", "HEAD^1"]),
git_text(fixture, repo, &["rev-parse", "HEAD^2"]),
git_text(fixture, repo, &["log", "-1", "--format=%an <%ae>|%cn <%ce>|%B"]),
];
}
#[test]
fn a_pull_that_merges_is_completed_over_staged_work_and_equals_native_gits() {
let fixture: Fixture = fixture("pull-isolated-oracle");
let dirty: PathBuf = diverged_with_remote(&fixture);
let clean: PathBuf = copy_of(&fixture, dirty.as_path(), "clean");
stage_unrelated_work(&fixture, dirty.as_path());
let staged: Vec<String> = git_text(&fixture, dirty.as_path(), &["diff", "--cached", "--name-status"]).lines().map(String::from).collect();
let arguments: [&str; 3] = ["pull", "--no-rebase", "--no-edit"];
let theirs: Observed = native(&fixture, clean.as_path(), &arguments);
assert_eq!(theirs.code, Some(0), "native Git pulls into the clean copy: {}", String::from_utf8_lossy(&theirs.stderr));
let ours: Observed = run(&fixture, dirty.as_path(), &arguments);
assert_eq!(ours.code, Some(0), "{}", String::from_utf8_lossy(&ours.stderr));
assert_eq!(String::from_utf8_lossy(&ours.stdout), String::from_utf8_lossy(&theirs.stdout), "output");
assert_eq!(String::from_utf8_lossy(&ours.stderr), String::from_utf8_lossy(&theirs.stderr), "diagnostics");
assert_eq!(commit_shape(&fixture, dirty.as_path()), commit_shape(&fixture, clean.as_path()));
let after: Vec<String> = git_text(&fixture, dirty.as_path(), &["diff", "--cached", "--name-status", "HEAD"]).lines().map(String::from).collect();
assert_eq!(after, staged, "the other session's staged work is still staged and nothing else is");
assert!(!snapshot(&fixture, dirty.as_path()).status.is_empty(), "the staged work is still there");
remove_settled(&fixture);
}
#[test]
fn a_pull_into_a_clean_index_equals_native_gits() {
let fixture: Fixture = fixture("pull-isolated-clean");
let ours_repo: PathBuf = diverged_with_remote(&fixture);
let theirs_repo: PathBuf = copy_of(&fixture, ours_repo.as_path(), "clean");
let arguments: [&str; 3] = ["pull", "--no-rebase", "--no-edit"];
let theirs: Observed = native(&fixture, theirs_repo.as_path(), &arguments);
let ours: Observed = run(&fixture, ours_repo.as_path(), &arguments);
assert_eq!((ours.code, theirs.code), (Some(0), Some(0)), "{}", String::from_utf8_lossy(&ours.stderr));
assert_eq!(String::from_utf8_lossy(&ours.stdout), String::from_utf8_lossy(&theirs.stdout), "output");
assert_eq!(String::from_utf8_lossy(&ours.stderr), String::from_utf8_lossy(&theirs.stderr), "diagnostics");
assert_eq!(commit_shape(&fixture, ours_repo.as_path()), commit_shape(&fixture, theirs_repo.as_path()));
remove_settled(&fixture);
}
#[test]
fn a_pull_native_git_refuses_or_the_wrapper_does_not_read_is_native_gits() {
let cases: [(&str, &[&str]); 4] = [
("advice", &["pull", "--no-edit"]),
("ff-only", &["pull", "--ff-only"]),
("rebase", &["pull", "--rebase"]),
("abbreviation", &["pull", "--no-reb", "--no-edit"]),
];
for (name, arguments) in cases {
let fixture: Fixture = fixture(format!("pull-native-{name}").as_str());
let ours_repo: PathBuf = diverged_with_remote(&fixture);
let theirs_repo: PathBuf = copy_of(&fixture, ours_repo.as_path(), "clean");
let theirs: Observed = native(&fixture, theirs_repo.as_path(), arguments);
let ours: Observed = run(&fixture, ours_repo.as_path(), arguments);
assert_eq!(ours.code, theirs.code, "{name}: {}", String::from_utf8_lossy(&ours.stderr));
assert_eq!(String::from_utf8_lossy(&ours.stdout), String::from_utf8_lossy(&theirs.stdout), "{name}: output");
assert_eq!(String::from_utf8_lossy(&ours.stderr), String::from_utf8_lossy(&theirs.stderr), "{name}: diagnostics");
assert_eq!(git_text(&fixture, ours_repo.as_path(), &["rev-parse", "HEAD"]), git_text(&fixture, theirs_repo.as_path(), &["rev-parse", "HEAD"]), "{name}: head");
remove_settled(&fixture);
}
}
fn behind_with_mirror_refspec(fixture: &Fixture) -> PathBuf {
let repo: PathBuf = repository(fixture, std::ffi::OsStr::new("repo"));
let remote: PathBuf = fixture.root.join("remote.git");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", "remote.git"]);
git(fixture, repo.as_path(), &["remote", "add", "origin", remote.to_str().expect("UTF-8")]);
std::fs::write(repo.join("f.txt"), b"one\n").expect("file");
git(fixture, repo.as_path(), &["add", "--", "f.txt"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=one"]);
std::fs::write(repo.join("f.txt"), b"two\n").expect("file");
git(fixture, repo.as_path(), &["commit", "--quiet", "--all", "--message=two"]);
git(fixture, repo.as_path(), &["push", "--quiet", "origin", "main"]);
git(fixture, repo.as_path(), &["reset", "--quiet", "--hard", "HEAD~1"]);
git(fixture, repo.as_path(), &["config", "remote.origin.fetch", "+refs/heads/*:refs/heads/*"]);
git(fixture, repo.as_path(), &["config", "branch.main.remote", "origin"]);
git(fixture, repo.as_path(), &["config", "branch.main.merge", "refs/heads/main"]);
return repo;
}
#[test]
fn a_fetch_that_moves_the_checked_out_branch_updates_the_working_tree_as_native_git_does() {
let fixture: Fixture = fixture("pull-moved-head");
let repo: PathBuf = behind_with_mirror_refspec(&fixture);
let clean: PathBuf = copy_of(&fixture, repo.as_path(), "clean");
let arguments: [&str; 3] = ["pull", "--no-rebase", "--no-edit"];
let theirs: Observed = native(&fixture, clean.as_path(), &arguments);
let ours: Observed = run(&fixture, repo.as_path(), &arguments);
assert_eq!((ours.code, theirs.code), (Some(0), Some(0)), "{}", String::from_utf8_lossy(&ours.stderr));
assert_eq!(String::from_utf8_lossy(&ours.stdout), String::from_utf8_lossy(&theirs.stdout), "output");
assert_eq!(String::from_utf8_lossy(&ours.stderr), String::from_utf8_lossy(&theirs.stderr), "diagnostics");
assert_eq!(std::fs::read_to_string(repo.join("f.txt")).expect("file"), "two\n", "the working tree followed the fetch");
assert_eq!(git_text(&fixture, repo.as_path(), &["status", "--porcelain=v1"]), "", "nothing is left modified");
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]), git_text(&fixture, clean.as_path(), &["rev-parse", "HEAD"]));
remove_settled(&fixture);
}
#[test]
fn a_pull_whose_fetch_moved_the_branch_waits_for_a_held_index_lock() {
let fixture: Fixture = fixture("pull-moved-head-lock");
let repo: PathBuf = behind_with_mirror_refspec(&fixture);
let lock: PathBuf = repo.join(".git/index.lock");
std::fs::write(&lock, b"").expect("another program holds the lock");
let mut command = wrapped_commit(&fixture, repo.as_path(), &["pull", "--no-rebase", "--no-edit"]);
command.env("CLI_GIT_WAIT_NOTICE_SECONDS", "1");
let ended: Ended = run_acting_on_line(&mut command, "waits for the index lock", &mut || {
std::fs::remove_file(&lock).expect("the other program lets go");
});
assert_eq!(ended.code, Some(0), "{}", ended.stderr);
assert_eq!(std::fs::read_to_string(repo.join("f.txt")).expect("file"), "two\n", "the working tree followed the fetch");
assert_eq!(git_text(&fixture, repo.as_path(), &["status", "--porcelain=v1"]), "", "nothing is left modified");
remove_settled(&fixture);
}
fn sorted_milliseconds(times: &[std::time::Duration]) -> Vec<u128> {
let mut milliseconds: Vec<u128> = times.iter().map(std::time::Duration::as_millis).collect();
milliseconds.sort_unstable();
return milliseconds;
}
#[test]
#[ignore = "a measurement for the handover, not a control"]
fn measure_the_split_pull_against_native_git() {
const ROUNDS: usize = 9;
const ARMS: [&str; 5] = ["native first", "wrapper split", "wrapper leaves it to native", "native second", "wrapper typed merge"];
for scenario in ["merging", "fast-forwarding"] {
let fixture: Fixture = fixture(format!("pull-measure-{scenario}").as_str());
let template: PathBuf = diverged_with_remote(&fixture);
if scenario == "fast-forwarding" {
git(&fixture, template.as_path(), &["reset", "--quiet", "--hard", "feature~1"]);
}
git(&fixture, template.as_path(), &["fetch", "--quiet", "origin"]);
let mut times: Vec<Vec<std::time::Duration>> = vec![Vec::new(); ARMS.len()];
for round in 0..ROUNDS {
for arm in 0..ARMS.len() {
let copy: PathBuf = copy_of(&fixture, template.as_path(), format!("copy-{scenario}-{round}-{arm}").as_str());
let own_remote: PathBuf = copy_of(&fixture, fixture.root.join("remote.git").as_path(), format!("remote-{scenario}-{round}-{arm}.git").as_str());
git(&fixture, copy.as_path(), &["remote", "set-url", "origin", own_remote.to_str().expect("UTF-8")]);
let started = std::time::Instant::now();
let observed: Observed = match arm {
0 | 3 => native(&fixture, copy.as_path(), &["pull", "--no-rebase", "--no-edit"]),
1 => observe(&mut wrapped_commit(&fixture, copy.as_path(), &["pull", "--no-rebase", "--no-edit"]), b""),
2 => observe(&mut wrapped_commit(&fixture, copy.as_path(), &["pull", "--no-rebase", "--autostash", "--no-edit"]), b""),
_ => observe(&mut wrapped_commit(&fixture, copy.as_path(), &["merge", "--no-edit", "origin/main"]), b""),
};
times[arm].push(started.elapsed());
assert_eq!(observed.code, Some(0), "{scenario} arm {arm}: {}", String::from_utf8_lossy(&observed.stderr));
if round == 0 {
eprintln!("MEASURE {scenario} {} reflog: {}", ARMS[arm], git_text(&fixture, copy.as_path(), &["reflog", "-1", "--format=%gs"]));
}
}
}
for arm in 0..ARMS.len() {
eprintln!("MEASURE {scenario} {} {:?}", ARMS[arm], sorted_milliseconds(times[arm].as_slice()));
}
remove_settled(&fixture);
}
}