use super::*;
use crate::service_journal::{DONE_EXTENSION, batch_path, parse_intent};
use crate::service_protocol::{ALLOWED_MODES, Change};
use crate::test_support::{fixture, git, git_context, git_output, git_text, remove, repository};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
const COMMITTER: &str = "T <t@example.invalid> 1793535889 +0000";
struct World {
root: PathBuf,
repo: PathBuf,
context: LandContext,
}
fn world(name: &str) -> World {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
let journal: PathBuf = root.join("journal");
std::fs::create_dir(&journal).expect("journal");
let context: LandContext = LandContext { git: git_context(), journal, on_objects_written: None, index_wait: std::time::Duration::from_secs(5) };
return World { root, repo, context };
}
fn request(world: &World, id: &str, path: &str, content: &str) -> CommitRequest {
let file: PathBuf = world.repo.join(path);
std::fs::create_dir_all(file.parent().expect("parent")).expect("directories");
std::fs::write(&file, content).expect("write");
let blob: String = git_text(world.repo.as_path(), &["hash-object", "-w", "--", path]);
return CommitRequest {
id: String::from(id),
repository: world.repo.clone(),
common_dir: world.repo.join(".git"),
reference: String::from("refs/heads/main"),
base: git_text(world.repo.as_path(), &["rev-parse", "HEAD"]),
changes: vec![Change { path: String::from(path), mode: ALLOWED_MODES[0], blob: Some(blob) }],
message: format!("message of {id}\n"),
author: None,
committer: String::from(COMMITTER),
amend: false,
tree: None,
commit: None,
cleanup: None,
index: world.repo.join(".git/index"),
captured: 0,
};
}
fn move_branch(repo: &Path, message: &str) {
let tree: String = git_text(repo, &["rev-parse", "HEAD^{tree}"]);
let head: String = git_text(repo, &["rev-parse", "HEAD"]);
let made: String = git_text(repo, &["commit-tree", tree.as_str(), "-p", head.as_str(), "-m", message]);
git(repo, &["update-ref", "refs/heads/main", made.as_str()]);
}
fn subjects(world: &World, since: &str) -> Vec<String> {
return git_text(world.repo.as_path(), &["log", "--reverse", "--format=%s", format!("{since}..HEAD").as_str()])
.lines()
.map(str::to_string)
.collect();
}
#[test]
fn a_batch_lands_in_order_with_its_content_and_a_clean_index() {
let world: World = world("service-land-order");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let batch: Vec<CommitRequest> = ["one", "two", "three"].iter().map(|id| { return request(&world, id, format!("{id}.txt").as_str(), format!("{id}\n").as_str()); }).collect();
let outcomes = land_batch(&world.context, 1, &batch).expect("landed");
assert_eq!(subjects(&world, base.as_str()), vec!["message of one", "message of two", "message of three"]);
for (id, outcome) in ["one", "two", "three"].iter().zip(&outcomes) {
let Outcome::Landed { commit } = outcome else { panic!("{outcome:?}") };
assert_eq!(git_text(world.repo.as_path(), &["show", format!("{commit}:{id}.txt").as_str()]), *id);
}
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), match outcomes.last() { Some(Outcome::Landed { commit }) => commit.clone(), other => panic!("{other:?}") });
assert!(batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION).exists(), "the batch is journaled and finished");
remove(world.root.as_path());
}
#[test]
fn a_changed_path_is_sent_back_and_an_unrelated_one_lands() {
let world: World = world("service-land-conflict");
let stale: CommitRequest = request(&world, "stale", "shared.txt", "mine\n");
let unrelated: CommitRequest = request(&world, "unrelated", "other.txt", "other\n");
git(world.repo.as_path(), &["add", "shared.txt"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=someone else"]);
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let outcomes = land_batch(&world.context, 1, &[stale, unrelated]).expect("landed");
assert_eq!(outcomes[0], Outcome::RetryPrepare { base: head });
assert!(matches!(outcomes[1], Outcome::Landed { .. }), "{:?}", outcomes[1]);
remove(world.root.as_path());
}
#[test]
fn a_member_that_overlaps_an_earlier_one_stays_only_if_it_captured_later() {
let cases: [(u64, u64, bool); 4] = [(10, 20, true), (20, 10, false), (0, 20, false), (10, 10, false)];
for (first_stamp, second_stamp, both_land) in cases {
let world: World = world(format!("service-land-shadow-{first_stamp}-{second_stamp}").as_str());
let first: CommitRequest = CommitRequest { captured: first_stamp, ..request(&world, "first", "shared.txt", "one\n") };
let second: CommitRequest = CommitRequest { captured: second_stamp, ..request(&world, "second", "shared.txt", "two\n") };
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let outcomes = land_batch(&world.context, 1, &[first, second]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{outcomes:?}");
if both_land {
assert!(matches!(outcomes[1], Outcome::Landed { .. }), "{first_stamp} then {second_stamp}: {outcomes:?}");
} else {
assert_eq!(outcomes[1], Outcome::RetryPrepare { base: head }, "{first_stamp} then {second_stamp}");
}
remove(world.root.as_path());
}
}
#[test]
fn a_file_and_a_directory_of_one_name_overlap() {
assert!(overlaps("a", "a/b"));
assert!(overlaps("a/b", "a"));
assert!(overlaps("a/b", "a/b"));
assert!(!overlaps("a/b", "a/bc"));
assert!(!overlaps("ab", "a"));
}
#[test]
fn awkward_names_land_as_themselves() {
let world: World = world("service-land-names");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let names = ["a \"quoted\" name.txt", "back\\slash.txt", "\"leading-quote.txt", "sp ace/dir with space/f.txt", "ünï.txt"];
let batch: Vec<CommitRequest> = names.iter().enumerate().map(|(n, name)| { return request(&world, format!("n{n}").as_str(), name, "x\n"); }).collect();
land_batch(&world.context, 1, &batch).expect("landed");
let listing: String = git_text(world.repo.as_path(), &["ls-tree", "-r", "--name-only", "-z", "HEAD"]);
let _ = base;
for name in names {
assert!(listing.split('\0').any(|entry| { return entry == name; }), "{name:?} in {listing:?}");
}
remove(world.root.as_path());
}
#[test]
fn a_deletion_removes_the_path() {
let world: World = world("service-land-delete");
let add: CommitRequest = request(&world, "add", "gone.txt", "x\n");
land_batch(&world.context, 1, &[add]).expect("added");
let mut delete: CommitRequest = request(&world, "delete", "stays.txt", "y\n");
delete.changes = vec![Change { path: String::from("gone.txt"), mode: ALLOWED_MODES[0], blob: None }];
land_batch(&world.context, 2, &[delete]).expect("deleted");
assert_eq!(git_text(world.repo.as_path(), &["ls-tree", "-r", "--name-only", "HEAD"]).lines().filter(|name| { return *name == "gone.txt"; }).count(), 0);
remove(world.root.as_path());
}
#[test]
fn a_branch_that_moves_under_the_batch_is_built_again() {
let mut world: World = world("service-land-race");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let first: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let repo: PathBuf = world.repo.clone();
let moves: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let counter: Arc<AtomicUsize> = moves.clone();
world.context.on_objects_written = Some(Box::new(move || {
if counter.fetch_add(1, Ordering::SeqCst) == 0 {
move_branch(repo.as_path(), "moved by someone");
}
}));
let outcomes = land_batch(&world.context, 1, &[first]).expect("landed after a second attempt");
assert!(matches!(outcomes[0], Outcome::Landed { .. }));
assert_eq!(subjects(&world, base.as_str()), vec!["moved by someone", "message of mine"]);
remove(world.root.as_path());
}
#[test]
fn a_branch_that_never_stops_moving_fails_after_the_attempts() {
let mut world: World = world("service-land-race-forever");
let first: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let repo: PathBuf = world.repo.clone();
let moves: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let counter: Arc<AtomicUsize> = moves.clone();
world.context.on_objects_written = Some(Box::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
move_branch(repo.as_path(), "moved again");
}));
assert_eq!(land_batch(&world.context, 1, &[first]), Err(FailureCode::BranchKeepsChanging));
assert_eq!(moves.load(Ordering::SeqCst), LANDING_ATTEMPTS);
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0, "no intent is left behind");
remove(world.root.as_path());
}
fn amend_request(world: &World, id: &str, path: &str, content: &str) -> CommitRequest {
let mut amend: CommitRequest = request(world, id, path, content);
git(world.repo.as_path(), &["add", "--", path]);
let tree: String = git_text(world.repo.as_path(), &["write-tree"]);
git(world.repo.as_path(), &["reset", "--quiet"]);
amend.amend = true;
amend.tree = Some(tree);
return amend;
}
#[test]
fn an_amend_replaces_the_tip_with_the_parents_of_the_replaced_commit() {
let world: World = world("service-land-amend");
let parent: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let first: CommitRequest = request(&world, "first", "first.txt", "first\n");
land_batch(&world.context, 1, &[first]).expect("landed");
let replaced: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut amend: CommitRequest = amend_request(&world, "amended", "first.txt", "first, amended\n");
amend.author = Some(String::from("Old <old@example.invalid> 1700000000 +0200"));
let outcomes = land_batch(&world.context, 2, &[amend.clone()]).expect("landed");
let Outcome::Landed { commit } = outcomes[0].clone() else { panic!("{:?}", outcomes[0]) };
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), commit);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", format!("{commit}^").as_str()]), parent, "the replaced commit's parent");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", format!("{commit}^{{tree}}").as_str()]), amend.tree.clone().unwrap_or_default());
assert_eq!(git_text(world.repo.as_path(), &["log", "-1", "--format=%s|%an|%ad|%cn", "--date=raw"]), "message of amended|Old|1700000000 +0200|T");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert_ne!(commit, replaced);
remove(world.root.as_path());
}
#[test]
fn an_amend_of_a_root_commit_and_of_a_merge_keeps_their_parents() {
let world: World = world("service-land-amend-shapes");
git(world.repo.as_path(), &["checkout", "--quiet", "--orphan", "fresh"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=orphan root"]);
let mut amend: CommitRequest = amend_request(&world, "root", "r.txt", "r\n");
amend.reference = String::from("refs/heads/fresh");
let outcomes = land_batch(&world.context, 1, &[amend]).expect("landed");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{:?}", outcomes[0]);
assert_eq!(git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "fresh"]).split_whitespace().count(), 1, "still a root commit");
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
git(world.repo.as_path(), &["merge", "--quiet", "--no-ff", "--allow-unrelated-histories", "--message=merge", "fresh"]);
let parents: String = git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "HEAD"]);
let amend_merge: CommitRequest = amend_request(&world, "merge", "m.txt", "m\n");
let merged = land_batch(&world.context, 2, &[amend_merge]).expect("landed");
assert!(matches!(merged[0], Outcome::Landed { .. }), "{:?}", merged[0]);
let after: String = git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "HEAD"]);
assert_eq!(parents.split_whitespace().skip(1).collect::<Vec<&str>>(), after.split_whitespace().skip(1).collect::<Vec<&str>>());
remove(world.root.as_path());
}
#[test]
fn an_amend_is_not_made_when_the_branch_is_not_at_its_base() {
let world: World = world("service-land-amend-moved");
let amend: CommitRequest = amend_request(&world, "late", "late.txt", "late\n");
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=moved by someone"]);
let moved: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
assert_eq!(land_batch(&world.context, 1, &[amend]).expect("answered"), vec![Outcome::BranchMoved]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), moved);
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
remove(world.root.as_path());
}
#[test]
fn an_amend_is_not_made_when_the_branch_moves_under_it() {
let mut world: World = world("service-land-amend-race");
let amend: CommitRequest = amend_request(&world, "racing", "racing.txt", "racing\n");
let repo: PathBuf = world.repo.clone();
world.context.on_objects_written = Some(Box::new(move || {
move_branch(repo.as_path(), "moved by someone");
}));
assert_eq!(land_batch(&world.context, 1, &[amend]).expect("answered"), vec![Outcome::BranchMoved]);
assert_eq!(git_text(world.repo.as_path(), &["log", "-1", "--format=%s"]), "moved by someone");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0, "no intent is left behind");
remove(world.root.as_path());
}
#[test]
fn an_amend_in_a_batch_with_others_is_refused() {
let world: World = world("service-land-amend-mixed");
let plain: CommitRequest = request(&world, "plain", "plain.txt", "p\n");
let amend: CommitRequest = amend_request(&world, "amend", "amend.txt", "a\n");
assert_eq!(land_batch(&world.context, 1, &[plain, amend]), Err(FailureCode::Rejected));
remove(world.root.as_path());
}
fn built_request(world: &World, id: &str, path: &str, parents: &[&str]) -> CommitRequest {
let mut built: CommitRequest = request(world, id, path, format!("{id}\n").as_str());
git(world.repo.as_path(), &["add", "--", path]);
let tree: String = git_text(world.repo.as_path(), &["write-tree"]);
git(world.repo.as_path(), &["reset", "--quiet"]);
let mut arguments: Vec<String> = vec![String::from("commit-tree"), tree, String::from("-m"), format!("typed {id}")];
for parent in parents {
arguments.push(String::from("-p"));
arguments.push((*parent).to_string());
}
let words: Vec<&str> = arguments.iter().map(String::as_str).collect();
built.commit = Some(git_text(world.repo.as_path(), words.as_slice()));
return built;
}
#[test]
fn a_commit_git_built_lands_as_it_is_while_the_branch_is_at_its_base() {
let world: World = world("service-land-built");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "typed", "typed.txt", &[base.as_str()]);
let commit: String = built.commit.clone().unwrap_or_default();
let outcomes = land_batch(&world.context, 1, &[built]).expect("landed");
assert_eq!(outcomes, vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), commit);
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "");
assert!(batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION).exists(), "journaled and finished");
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_is_not_landed_when_the_branch_is_not_at_its_base() {
let world: World = world("service-land-built-moved");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let built: CommitRequest = built_request(&world, "typed", "typed.txt", &[base.as_str()]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=moved by someone"]);
let moved: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
assert_eq!(land_batch(&world.context, 1, &[built]).expect("answered"), vec![Outcome::BranchMoved]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), moved);
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_is_checked_before_the_branch_moves() {
let world: World = world("service-land-built-checked");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=second"]);
let second: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut on_other_parents: CommitRequest = built_request(&world, "other", "other.txt", &[base.as_str()]);
on_other_parents.base = second.clone();
let mut not_a_commit: CommitRequest = built_request(&world, "tree", "tree.txt", &[second.as_str()]);
not_a_commit.commit = Some(git_text(world.repo.as_path(), &["rev-parse", "HEAD^{tree}"]));
let mut absent: CommitRequest = built_request(&world, "absent", "absent.txt", &[second.as_str()]);
absent.commit = Some("1".repeat(40));
let mut wrong_amend: CommitRequest = built_request(&world, "amend", "amend.txt", &[second.as_str()]);
wrong_amend.amend = true;
for (label, refused) in [("other parents", on_other_parents), ("a tree", not_a_commit), ("an absent name", absent), ("an amend on the base itself", wrong_amend)] {
assert_eq!(land_batch(&world.context, 1, &[refused]), Err(FailureCode::Rejected), "{label}");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), second, "{label}: the branch did not move");
}
remove(world.root.as_path());
}
#[test]
fn a_landing_waits_for_a_foreign_index_lock_and_builds_on_what_the_holder_left() {
let world: World = world("service-land-foreign-lock");
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
let repo: PathBuf = world.repo.clone();
let holder_lock: PathBuf = lock.clone();
let holder = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1_700));
move_branch(repo.as_path(), "foreign commit");
std::fs::remove_file(holder_lock).expect("the holder lets go");
});
let outcomes = land_batch(&world.context, 1, &[request]).expect("landed after the holder let go");
holder.join().expect("thread");
assert!(matches!(outcomes[0], Outcome::Landed { .. }), "{outcomes:?}");
assert_eq!(subjects(&world, base.as_str()), vec!["foreign commit", "message of mine"]);
assert!(!lock.exists(), "the landing gave the lock up");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing");
remove(world.root.as_path());
}
#[test]
fn a_foreign_index_lock_that_outlives_the_limit_changes_nothing() {
let mut world: World = world("service-land-foreign-lock-limit");
world.context.index_wait = std::time::Duration::from_millis(400);
let request: CommitRequest = request(&world, "mine", "mine.txt", "mine\n");
let head: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let lock: PathBuf = world.repo.join(".git/index.lock");
std::fs::write(&lock, "foreign").expect("foreign lock");
assert_eq!(land_batch(&world.context, 1, &[request]), Err(FailureCode::IndexInUse));
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), head, "the branch did not move");
assert_eq!(std::fs::read_to_string(&lock).expect("the foreign lock stands"), "foreign");
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
remove(world.root.as_path());
}
#[test]
fn a_mixed_batch_is_refused() {
let world: World = world("service-land-mixed");
let one: CommitRequest = request(&world, "one", "one.txt", "1\n");
let mut other: CommitRequest = request(&world, "two", "two.txt", "2\n");
other.reference = String::from("refs/heads/other");
assert_eq!(land_batch(&world.context, 1, &[one, other]), Err(FailureCode::Rejected));
assert_eq!(land_batch(&world.context, 2, &[]), Err(FailureCode::Rejected));
remove(world.root.as_path());
}
#[test]
fn a_restart_finishes_drops_and_removes_what_a_kill_left() {
let world: World = world("service-land-recover");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let landed: CommitRequest = request(&world, "landed", "landed.txt", "landed\n");
land_batch(&world.context, 1, &[landed]).expect("landed");
let new: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["reset", "-q", base.as_str(), "--", "landed.txt"]);
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]).lines().count(), 2, "staged deletion and untracked file");
let done: PathBuf = batch_path(world.context.journal.as_path(), 1, DONE_EXTENSION);
std::fs::rename(&done, batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("reopen");
let mut never: Intent = parse_intent(std::fs::read_to_string(batch_path(world.context.journal.as_path(), 1, OPEN_EXTENSION)).expect("read").as_str()).expect("parse");
never.seq = 2;
never.old = new.clone();
never.new = "f".repeat(40);
crate::service_journal::write_intent(world.context.journal.as_path(), &never).expect("second intent");
std::fs::write(batch_path(world.context.journal.as_path(), 3, OPEN_EXTENSION), "seq=3\nrepos").expect("torn");
let reports = recover(&world.context);
let by_seq: Vec<(u64, Recovery)> = reports.iter().map(|report| { return (report.seq, report.decision); }).collect();
assert!(by_seq.contains(&(1, Recovery::Finish)), "{by_seq:?}");
assert!(by_seq.contains(&(2, Recovery::Drop)), "{by_seq:?}");
assert_eq!(git_text(world.repo.as_path(), &["status", "--short"]), "", "the index shows the landing again");
assert!(crate::service_journal::landed_commits(world.context.journal.as_path()).contains_key("landed"));
assert_eq!(list_intents(world.context.journal.as_path(), OPEN_EXTENSION).len(), 0);
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_with_extra_parents_lands_and_updates_the_index_from_the_repository() {
let world: World = world("service-land-built-merge");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["checkout", "--quiet", "-b", "side"]);
std::fs::write(world.repo.join("side.txt"), "side\n").expect("side");
git(world.repo.as_path(), &["add", "side.txt"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=side"]);
let side: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
let mut merged: CommitRequest = built_request(&world, "merge", "side.txt", &[base.as_str(), side.as_str()]);
merged.changes = Vec::new();
let commit: String = merged.commit.clone().unwrap_or_default();
assert_eq!(land_batch(&world.context, 1, &[merged]).expect("landed"), vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-list", "--parents", "--max-count=1", "HEAD"]), format!("{commit} {base} {side}"));
assert_eq!(git_text(world.repo.as_path(), &["diff", "--cached", "--name-status"]), "", "the index shows the commit");
remove(world.root.as_path());
}
#[test]
fn a_commit_git_built_whose_first_parent_is_not_the_tip_is_refused_even_with_the_tip_as_a_later_parent() {
let world: World = world("service-land-built-first-parent");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=other"]);
let other: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let mut wrong_order: CommitRequest = built_request(&world, "order", "order.txt", &[base.as_str(), other.as_str()]);
wrong_order.base = other.clone();
assert_eq!(land_batch(&world.context, 1, &[wrong_order]), Err(FailureCode::Rejected));
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), other);
remove(world.root.as_path());
}
#[test]
fn a_commit_on_a_branch_that_does_not_exist_lands_once_and_never_over_a_branch_that_appeared() {
let world: World = world("service-land-unborn");
let zero: String = "0".repeat(40);
let mut first: CommitRequest = built_request(&world, "root", "root.txt", &[]);
first.reference = String::from("refs/heads/fresh");
first.base = zero;
let commit: String = first.commit.clone().unwrap_or_default();
let mut raced: CommitRequest = first.clone();
raced.id = String::from("raced");
let mut with_parent: CommitRequest = first.clone();
with_parent.id = String::from("parent");
with_parent.commit = built_request(&world, "child", "child.txt", &[commit.as_str()]).commit;
assert_eq!(land_batch(&world.context, 1, &[with_parent]), Err(FailureCode::Rejected), "a root commit has no parent");
assert_eq!(land_batch(&world.context, 2, &[first]).expect("landed"), vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "refs/heads/fresh"]), commit);
assert_eq!(land_batch(&world.context, 3, &[raced]).expect("answered"), vec![Outcome::BranchMoved], "the branch exists now");
remove(world.root.as_path());
}
#[test]
fn a_detached_head_is_moved_by_a_compare_that_does_not_follow_a_branch() {
let world: World = world("service-land-detached");
let base: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
git(world.repo.as_path(), &["checkout", "--quiet", "--detach"]);
let mut built: CommitRequest = built_request(&world, "loose", "loose.txt", &[base.as_str()]);
built.reference = String::from("HEAD");
let commit: String = built.commit.clone().unwrap_or_default();
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
assert_eq!(land_batch(&world.context, 1, &[built.clone()]).expect("answered"), vec![Outcome::BranchMoved], "a head that became a branch is not landed on");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "main"]), base, "the branch did not move");
assert_eq!(git_text(world.repo.as_path(), &["symbolic-ref", "HEAD"]), "refs/heads/main", "the head still names the branch");
git(world.repo.as_path(), &["checkout", "--quiet", "--detach"]);
let outcomes = land_batch(&world.context, 2, &[built]).expect("landed");
assert_eq!(outcomes, vec![Outcome::Landed { commit: commit.clone() }]);
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), commit);
assert!(!git_output(world.repo.as_path(), &["symbolic-ref", "--quiet", "HEAD"]).status.success(), "the head is still detached");
assert_eq!(git_text(world.repo.as_path(), &["rev-parse", "main"]), base);
remove(world.root.as_path());
}