use super::*;
use crate::fs_btrfs_test_support::btrfs_fixture;
use crate::fs_kind::is_subvolume_root;
use crate::test_support::{REAL_GIT, git, git_context, git_text, remove, repository};
use crate::worktree_copy::GitStep;
use crate::worktree_copy_error::WorktreeCopyError;
use crate::worktree_ladder_run::begin_ladder;
use crate::worktree_add_binding::Bound;
use crate::worktree_copy_journal::journal_root;
use crate::worktree_ladder_test_support::{commit_all, main_identity, plan_to, subvolume_repository, tracked};
use std::cell::RefCell;
use std::process::{Command, Stdio};
type Steps = RefCell<Vec<Vec<String>>>;
fn recording_runner(steps: &Steps, fail_reset: bool) -> impl FnMut(Option<&str>, &GitStep<'_>) -> Result<GitStatus, WorktreeCopyError> + '_ {
return move |_token, step| {
let GitStep::Other { arguments, environment } = step else { panic!("only the ladder's own steps are expected") };
steps.borrow_mut().push(arguments.iter().map(|word: &OsString| { return word.to_string_lossy().into_owned(); }).collect());
if fail_reset && arguments.iter().any(|word: &OsString| { return word == "reset"; }) {
return Ok(GitStatus { code: Some(1) });
}
let status = Command::new(REAL_GIT)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.envs(environment.iter().cloned())
.args(arguments.iter())
.stdin(Stdio::null())
.status()
.expect("Git starts");
return Ok(GitStatus { code: status.code() });
};
}
fn add_through_the_ladder(repo: &Path, root: &Path, extra: &[&str], fail_reset: bool, spoil: impl FnOnce(&Path)) -> (LadderResult, Vec<Vec<String>>, String) {
let destination: PathBuf = root.join("topic");
let mut plan = plan_to(destination.clone());
plan.quiet = extra.contains(&"--quiet");
let ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo));
assert_eq!(ladder.first_step(), GitStep::WithoutCheckout(plan.insert_at), "the source is eligible");
let mut words: Vec<&str> = vec!["worktree", "add", "--no-checkout"];
words.extend_from_slice(extra);
words.extend(["-b", "topic", "../topic"]);
git(repo, words.as_slice());
spoil(destination.as_path());
let steps: Steps = RefCell::new(Vec::new());
let mut printed: Vec<u8> = Vec::new();
let result: LadderResult = {
let mut runner = recording_runner(&steps, fail_reset);
ladder.finish(GitStatus { code: Some(0) }, None, &mut runner, &git_context(), &mut printed)
};
return (result, steps.into_inner(), String::from_utf8_lossy(printed.as_slice()).into_owned());
}
#[test]
fn btrfs_a_source_at_the_target_commit_is_served_with_no_git_step() {
let Some(root) = btrfs_fixture("finish-same") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (result, steps, printed) = add_through_the_ladder(repo.as_path(), root.as_path(), &[], false, |_| {});
assert_eq!(result, LadderResult { git: GitStatus { code: Some(0) }, carried: Some(root.join("topic")) });
assert!(steps.is_empty(), "{steps:?}");
let head: String = git_text(repo.as_path(), &["log", "-1", "--format=HEAD is now at %h %s"]);
assert_eq!(printed, format!("{head}\n"));
assert!(is_subvolume_root(root.join("topic").as_path()).expect("probe"));
assert!(root.join("topic/ignored/pkg/index.js").is_file());
remove(root.as_path());
}
#[test]
fn btrfs_quiet_prints_no_line() {
let Some(root) = btrfs_fixture("finish-quiet") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (result, _steps, printed) = add_through_the_ladder(repo.as_path(), root.as_path(), &["--quiet"], false, |_| {});
assert!(result.carried.is_some());
assert_eq!(printed, "");
remove(root.as_path());
}
#[test]
fn btrfs_a_refused_step_destroys_the_stage_and_completes_the_checkout_like_git() {
let Some(root) = btrfs_fixture("finish-fallback") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (result, steps, printed) = add_through_the_ladder(repo.as_path(), root.as_path(), &[], false, |destination| {
std::fs::write(destination.join("stray"), b"x").expect("a stray file");
});
assert_eq!(result, LadderResult { git: GitStatus { code: Some(0) }, carried: None });
assert_eq!(printed, "", "Git's own reset prints the line, not the ladder");
assert_eq!(steps.len(), 1, "{steps:?}");
assert_eq!(&steps[0][2..], ["reset", "--hard", "--no-recurse-submodules"], "{steps:?}");
assert!(root.join("topic/shared.txt").is_file(), "the checkout completed");
assert!(!is_subvolume_root(root.join("topic").as_path()).expect("probe"), "the stage was not swapped in");
let leftovers: Vec<String> = std::fs::read_dir(root.as_path()).expect("listing").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).filter(|name: &String| { return name.starts_with(".cli-git"); }).collect();
assert_eq!(leftovers, Vec::<String>::new(), "the stage is gone");
remove(root.as_path());
}
#[test]
fn btrfs_a_failing_checkout_step_gives_its_status() {
let Some(root) = btrfs_fixture("finish-reset-fails") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (result, _steps, _printed) = add_through_the_ladder(repo.as_path(), root.as_path(), &["--quiet"], true, |destination| {
std::fs::write(destination.join("stray"), b"x").expect("a stray file");
});
assert_eq!(result, LadderResult { git: GitStatus { code: Some(1) }, carried: None });
assert!(root.join("topic/.git").exists() && root.join("topic/stray").exists(), "nothing was deleted");
assert!(repo.join(".git/worktrees/topic").exists(), "nothing was unregistered");
remove(root.as_path());
}
fn ordinary_repository(root: &Path) -> PathBuf {
let repo: PathBuf = std::fs::canonicalize(repository(root, "repo")).expect("canonical");
tracked(repo.as_path(), "shared.txt", b"first\n");
commit_all(repo.as_path(), "first");
tracked(repo.as_path(), "shared.txt", b"second, longer\n");
commit_all(repo.as_path(), "second");
return repo;
}
#[test]
fn btrfs_a_refused_clone_rung_leaves_a_file_it_did_not_create() {
let Some(root) = btrfs_fixture("finish-clone-stray") else { return };
let repo: PathBuf = ordinary_repository(root.as_path());
let (result, steps, _printed) = add_through_the_ladder(repo.as_path(), root.as_path(), &[], false, |destination| {
std::fs::write(destination.join("notes"), b"mine\n").expect("a file of someone else");
});
assert_eq!(result.git, GitStatus { code: Some(0) });
assert_eq!(steps.len(), 1, "{steps:?}");
assert_eq!(std::fs::read(root.join("topic/notes")).expect("kept"), b"mine\n", "a non-conflicting file is not deleted");
assert!(root.join("topic/shared.txt").is_file(), "the checkout completed");
remove(root.as_path());
}
#[test]
fn btrfs_a_failing_checkout_after_a_refused_clone_rung_leaves_the_worktree_and_its_registration() {
let Some(root) = btrfs_fixture("finish-clone-rollback") else { return };
let repo: PathBuf = ordinary_repository(root.as_path());
let (result, _steps, _printed) = add_through_the_ladder(repo.as_path(), root.as_path(), &["--quiet"], true, |destination| {
std::fs::write(destination.join("notes"), b"x").expect("a stray file");
});
assert_eq!(result.git, GitStatus { code: Some(1) });
assert_eq!(std::fs::read(root.join("topic/notes")).expect("kept"), b"x");
assert!(repo.join(".git/worktrees/topic").exists(), "the registration stays");
assert!(!crate::worktree_copy_stage_claim::stage_claims_exist(repo.join(".git").as_path()).expect("claims"), "reported once, then released");
remove(root.as_path());
}
struct MarkerProbe {
claims: PathBuf,
marker_seen: Option<bool>,
bound_seen: Option<bool>,
}
impl Write for MarkerProbe {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
let marker: bool = std::fs::read_dir(self.claims.as_path()).is_ok_and(|listing| { return listing.flatten().any(|entry| { return entry.file_name().to_string_lossy().ends_with(".done"); }); });
self.marker_seen.get_or_insert(marker);
let bound: bool = std::fs::read_dir(self.claims.as_path()).is_ok_and(|listing| {
return listing.flatten().filter(|entry| { return entry.file_name().to_string_lossy().ends_with(".add"); }).any(|entry| { return std::fs::read_to_string(entry.path()).is_ok_and(|text: String| { return text.contains("phase=registered"); }); });
});
self.bound_seen.get_or_insert(bound);
return Ok(bytes.len());
}
fn flush(&mut self) -> std::io::Result<()> {
return Ok(());
}
}
#[test]
fn btrfs_each_rung_marks_the_add_complete_before_it_prints_and_releases_the_records_after() {
for (name, ordinary) in [("finish-marker-clone", true), ("finish-marker-snapshot", false)] {
let Some(root) = btrfs_fixture(name) else { return };
let repo: PathBuf = if ordinary { ordinary_repository(root.as_path()) } else { subvolume_repository(root.as_path()) };
let plan = plan_to(root.join("topic"));
let claims: PathBuf = journal_root(repo.join(".git").as_path());
let ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
let records: Vec<String> = std::fs::read_dir(claims.as_path()).expect("claims").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
assert!(records.iter().any(|record: &String| { return record.ends_with(".add"); }), "{name}: the add is recorded before Git runs: {records:?}");
git(repo.as_path(), &["worktree", "add", "--quiet", "--no-checkout", "-b", "topic", "../topic"]);
let mut probe: MarkerProbe = MarkerProbe { claims: claims.clone(), marker_seen: None, bound_seen: None };
let steps: Steps = RefCell::new(Vec::new());
let result: LadderResult = {
let mut runner = recording_runner(&steps, false);
ladder.finish(GitStatus { code: Some(0) }, None, &mut runner, &git_context(), &mut probe)
};
assert_eq!(result.git, GitStatus { code: Some(0) }, "{name}");
assert_eq!(probe.marker_seen, Some(true), "{name}: the marker is on disk when the line is printed");
assert_eq!(probe.bound_seen, Some(true), "{name}: the record names the directories Git made when the line is printed");
let left: Vec<String> = std::fs::read_dir(claims.as_path()).expect("claims").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
assert_eq!(left, Vec::<String>::new(), "{name}: the records are released");
remove(root.as_path());
}
}
#[test]
fn an_error_from_a_failed_swap_back_does_not_own_the_stage() {
let foreign: Ended = Ended::from(PlaceError { reason: String::from("x"), stage_is_ours: false });
let own: Ended = Ended::from(PlaceError { reason: String::from("y"), stage_is_ours: true });
assert!(!foreign.stage_is_ours && own.stage_is_ours);
assert!(Ended::from(String::from("a plain reason")).stage_is_ours, "an error before the exchange left the stage ours");
}
#[test]
fn btrfs_an_add_whose_records_cannot_be_written_still_succeeds_and_keeps_everything() {
let Some(root) = btrfs_fixture("finish-records-unwritable") else { return };
let repo: PathBuf = ordinary_repository(root.as_path());
let plan = plan_to(root.join("topic"));
let claims: PathBuf = journal_root(repo.join(".git").as_path());
let ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
git(repo.as_path(), &["worktree", "add", "--quiet", "--no-checkout", "-b", "topic", "../topic"]);
std::fs::set_permissions(claims.as_path(), std::os::unix::fs::PermissionsExt::from_mode(0o500)).expect("read-only records");
let steps: Steps = RefCell::new(Vec::new());
let mut printed: Vec<u8> = Vec::new();
let result: LadderResult = {
let mut runner = recording_runner(&steps, false);
ladder.finish(GitStatus { code: Some(0) }, None, &mut runner, &git_context(), &mut printed)
};
std::fs::set_permissions(claims.as_path(), std::os::unix::fs::PermissionsExt::from_mode(0o700)).expect("writable again");
assert_eq!(result.git, GitStatus { code: Some(0) });
assert!(!printed.is_empty(), "Git's line is printed");
assert!(root.join("topic/shared.txt").is_file() && repo.join(".git/worktrees/topic").exists());
remove(root.as_path());
}
#[test]
fn btrfs_a_fallback_never_resets_a_replacement_at_the_destination() {
let Some(root) = btrfs_fixture("finish-fallback-replaced") else { return };
let repo: PathBuf = ordinary_repository(root.as_path());
let plan = plan_to(root.join("topic"));
let ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
git(repo.as_path(), &["worktree", "add", "--quiet", "--no-checkout", "--detach", "../topic", "HEAD~1"]);
let replaced: std::cell::Cell<bool> = std::cell::Cell::new(false);
let steps: Steps = RefCell::new(Vec::new());
let mut inner = recording_runner(&steps, false);
let mut swapping = |token: Option<&str>, step: &GitStep<'_>| {
if !replaced.replace(true) {
git(repo.as_path(), &["worktree", "move", "../topic", "../moved"]);
git(repo.as_path(), &["worktree", "add", "--quiet", "--detach", "../topic", "HEAD"]);
std::fs::write(root.join("topic/edited.txt"), b"work in the replacement").expect("their edit");
return Ok(GitStatus { code: Some(1) });
}
return inner(token, step);
};
let mut printed: Vec<u8> = Vec::new();
let result: LadderResult = ladder.finish(GitStatus { code: Some(0) }, None, &mut swapping, &git_context(), &mut printed);
assert_eq!(result.git, GitStatus { code: Some(1) });
assert!(printed.is_empty());
assert_eq!(std::fs::read(root.join("topic/edited.txt")).expect("the replacement is untouched"), b"work in the replacement");
assert!(root.join("moved").exists(), "the moved worktree stays");
assert!(steps.borrow().iter().all(|words: &Vec<String>| { return !words.iter().any(|word: &String| { return word == "reset"; }); }), "no reset was run: {:?}", steps.borrow());
remove(root.as_path());
}
#[test]
fn btrfs_a_checkout_that_ran_against_a_replacement_is_not_taken_for_the_adds() {
let Some(root) = btrfs_fixture("finish-checkout-replaced") else { return };
let repo: PathBuf = ordinary_repository(root.as_path());
let plan = plan_to(root.join("topic"));
let ladder = begin_ladder(Some(&plan), &git_context(), &main_identity(repo.as_path()));
git(repo.as_path(), &["worktree", "add", "--quiet", "--no-checkout", "-b", "topic", "../topic"]);
std::fs::write(root.join("topic/notes"), b"x").expect("a stray file, so the clone rung is refused");
let steps: Steps = RefCell::new(Vec::new());
let mut inner = recording_runner(&steps, false);
let mut swapping = |token: Option<&str>, step: &GitStep<'_>| {
let outcome = inner(token, step);
git(repo.as_path(), &["worktree", "move", "../topic", "../moved"]);
git(repo.as_path(), &["worktree", "add", "--quiet", "--detach", "../topic", "HEAD"]);
return outcome;
};
let mut printed: Vec<u8> = Vec::new();
let result: LadderResult = ladder.finish(GitStatus { code: Some(0) }, None, &mut swapping, &git_context(), &mut printed);
assert_eq!(result.git, GitStatus { code: Some(1) }, "the reset succeeded, but not against the bound worktree");
assert!(root.join("moved/shared.txt").exists() && root.join("topic/shared.txt").exists(), "both worktrees stand");
remove(root.as_path());
}
#[test]
fn btrfs_the_clone_rung_writes_nothing_when_the_registration_was_replaced() {
let Some(root) = btrfs_fixture("finish-clone-registration-replaced") else { return };
let repo: PathBuf = ordinary_repository(root.as_path());
let plan = plan_to(root.join("topic"));
git(repo.as_path(), &["worktree", "add", "--quiet", "--no-checkout", "-b", "topic", "../topic"]);
let source = crate::worktree_snapshot_source::common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
let bound: Bound = crate::worktree_add_binding::bind_registration(source.common_dir.as_path(), plan.destination.as_path()).expect("bound");
let admin: PathBuf = repo.join(".git/worktrees/topic");
std::fs::rename(admin.as_path(), admin.with_file_name("topic.moved")).expect("registration moved");
std::fs::create_dir(admin.as_path()).expect("another registration");
let steps: Steps = RefCell::new(Vec::new());
let mut runner = recording_runner(&steps, false);
let reason: String = serve_clones(&source, &plan, &bound, None, &mut runner, &git_context()).expect_err("refused");
assert!(reason.contains("no longer the one Git made"), "{reason}");
let entries: Vec<String> = std::fs::read_dir(root.join("topic")).expect("listing").flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect();
assert_eq!(entries, vec![String::from(".git")], "nothing was written into the worktree");
remove(root.as_path());
}