use super::merge_isolation::{diverged, records_in, snapshot, stage_unrelated_work};
use super::commit::{git_text, remove_settled, wrapped_commit};
use super::support::{Fixture, TIMED_OUT_SIGNAL, fixture, git, without_hint_fields};
use git_policy_cli::transaction_phase::PHASE_SIGNAL_VARIABLE;
use std::io::{BufRead, Read};
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const PATIENCE: std::time::Duration = std::time::Duration::from_secs(60);
struct Ended {
code: Option<i32>,
stderr: String,
}
fn run_acting_on_line(command: &mut Command, needle: &str, act: &mut dyn FnMut()) -> Ended {
let mut child = command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("process starts");
let mut standard_output = child.stdout.take().expect("piped stdout");
let output_reader = std::thread::spawn(move || {
let mut bytes: Vec<u8> = Vec::new();
let _read = standard_output.read_to_end(&mut bytes);
return bytes;
});
let mut errors = std::io::BufReader::new(child.stderr.take().expect("piped stderr"));
let mut stderr: Vec<u8> = Vec::new();
let mut acted: bool = false;
loop {
let line_start: usize = stderr.len();
if errors.read_until(b'\n', &mut stderr).expect("read standard error") == 0 {
break;
}
if !acted && String::from_utf8_lossy(&stderr[line_start..]).contains(needle) {
acted = true;
act();
}
}
let status = child.wait().expect("process finishes");
assert_ne!(status.signal(), Some(TIMED_OUT_SIGNAL), "the command hit its bound before it ended");
let _output = output_reader.join().expect("standard output reader");
return Ended { code: status.code(), stderr: String::from_utf8_lossy(without_hint_fields(stderr.as_slice()).as_slice()).into_owned() };
}
fn parents(fixture: &Fixture, repo: &Path) -> Vec<String> {
return git_text(fixture, repo, &["rev-list", "--parents", "-n", "1", "HEAD"]).split_whitespace().skip(1).map(String::from).collect();
}
#[test]
fn a_merge_waits_for_a_held_index_lock_and_then_lands() {
for staged in [false, true] {
let fixture: Fixture = fixture(format!("merge-waits-lock-{staged}").as_str());
let repo: PathBuf = diverged(&fixture, "repo");
if staged {
stage_unrelated_work(&fixture, repo.as_path());
}
let before = snapshot(&fixture, repo.as_path());
let lock: PathBuf = repo.join(".git/index.lock");
std::fs::write(&lock, b"").expect("another program holds the lock");
let mut command: Command = wrapped_commit(&fixture, repo.as_path(), &["merge", "--no-edit", "feature"]);
command.env("CLI_GIT_WAIT_NOTICE_SECONDS", "1");
let ended: Ended = run_acting_on_line(&mut command, "waits for the index lock", &mut || {
assert_eq!(snapshot_head(&fixture, repo.as_path()), before.head, "nothing was merged while the lock was held");
std::fs::remove_file(&lock).expect("the other program lets go");
});
assert_eq!(ended.code, Some(0), "staged={staged}: {}", ended.stderr);
assert!(ended.stderr.contains("git merge has changed nothing yet and waits for the index lock"), "{}", ended.stderr);
assert!(ended.stderr.contains("index.lock"), "the lock is named: {}", ended.stderr);
assert_eq!(parents(&fixture, repo.as_path()).len(), 2, "staged={staged}: a merge commit");
assert!(!repo.join(".git/MERGE_HEAD").exists());
assert_eq!(records_in(repo.join(".git/cli-git-merges").as_path()), Vec::<PathBuf>::new());
git(&fixture, repo.as_path(), &["fsck", "--strict", "--no-dangling"]);
remove_settled(&fixture);
}
}
fn snapshot_head(fixture: &Fixture, repo: &Path) -> String {
return git_text(fixture, repo, &["rev-parse", "HEAD"]);
}
#[test]
fn a_merge_whose_branch_moved_underneath_it_lands_on_the_new_head() {
for staged in [true, false] {
let fixture: Fixture = fixture(format!("merge-moved-{staged}").as_str());
let repo: PathBuf = diverged(&fixture, "repo");
if staged {
stage_unrelated_work(&fixture, repo.as_path());
}
let markers: PathBuf = fixture.root.join("markers");
std::fs::create_dir(&markers).expect("marker directory");
let mut command: Command = wrapped_commit(&fixture, repo.as_path(), &["merge", "--no-edit", "feature"]);
command.env(PHASE_SIGNAL_VARIABLE, format!("merge-intent:pause:{}", markers.display()));
let child = command.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("the merge starts");
let began = std::time::Instant::now();
while !markers.join("merge-intent.reached").exists() {
assert!(began.elapsed() < PATIENCE, "staged={staged}: the merge never reached the point between computing and applying");
std::thread::sleep(std::time::Duration::from_millis(20));
}
std::fs::write(repo.join("z.txt"), b"committed meanwhile\n").expect("file");
git(&fixture, repo.as_path(), &["add", "--", "z.txt"]);
git(&fixture, repo.as_path(), &["commit", "--quiet", "--message=meanwhile", "--", "z.txt"]);
let moved: String = git_text(&fixture, repo.as_path(), &["rev-parse", "HEAD"]).trim().to_owned();
std::fs::write(markers.join("merge-intent.release"), b"").expect("release the merge");
let output = child.wait_with_output().expect("the merge ends");
let stderr: String = String::from_utf8_lossy(&output.stderr).into_owned();
assert_eq!(output.status.code(), Some(0), "staged={staged}: {stderr}");
let parents: Vec<String> = parents(&fixture, repo.as_path());
assert_eq!(parents.len(), 2, "staged={staged}: a merge commit");
assert_eq!(parents[0], moved, "staged={staged}: the merge sits on the head that moved");
assert_eq!(parents[1], git_text(&fixture, repo.as_path(), &["rev-parse", "feature"]).trim());
assert!(repo.join("z.txt").exists() && repo.join("dir/new.txt").exists(), "staged={staged}: both sides are in the checkout");
assert!(!repo.join(".git/MERGE_HEAD").exists());
assert_eq!(records_in(repo.join(".git/cli-git-merges").as_path()), Vec::<PathBuf>::new());
let status: String = git_text(&fixture, repo.as_path(), &["--no-optional-locks", "status", "--porcelain=v1"]);
assert_eq!(status.lines().count(), if staged { 3 } else { 0 }, "staged={staged}: only the other session's work is staged: {status}");
git(&fixture, repo.as_path(), &["fsck", "--strict", "--no-dangling"]);
remove_settled(&fixture);
}
}