use super::*;
use crate::fs_btrfs_test_support::btrfs_fixture;
use crate::fs_kind::is_subvolume_root;
use crate::repo_read_index::parse_index;
use crate::repo_read_layout::ObjectFormat;
use crate::test_support::{age, git, git_context, remove};
use crate::worktree_ladder_test_support::{main_identity, plan_to, subvolume_repository};
use crate::worktree_snapshot_source::choose_snapshot_source;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use crate::worktree_add_plan::AddPlan;
fn take(source: &SnapshotSource, plan: &AddPlan, stage: &Path, admin: &Path, pointer: &[u8]) -> Result<(), String> {
let held: File = open_root(admin).expect("administrative directory");
return take_snapshot(source, plan, stage, &held, pointer).map(|_made: Snapshotted| {}).map_err(|failure: SnapshotFailure| { return failure.reason; });
}
fn id_of_directory(path: &Path) -> ObjectId {
let parent: File = open_root(path.parent().expect("parent")).expect("parent handle");
return identity_at(&parent, path.file_name().expect("name")).expect("identity");
}
fn no_checkout_worktree(repo: &Path, name: &str) -> (PathBuf, PathBuf, Vec<u8>) {
git(repo, &["worktree", "add", "--quiet", "--no-checkout", "-b", name, format!("../{name}").as_str()]);
let destination: PathBuf = std::fs::canonicalize(repo.join("..").join(name)).expect("destination");
let pointer: Vec<u8> = std::fs::read(destination.join(".git")).expect("pointer");
let admin: PathBuf = repo.join(".git/worktrees").join(name);
return (destination, admin, pointer);
}
#[test]
fn btrfs_the_stage_becomes_a_worktree_with_the_new_pointer_and_the_sources_index() {
let Some(root) = btrfs_fixture("snapshot-stage") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination.clone());
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect("snapshot");
assert!(is_subvolume_root(stage.as_path()).expect("probe"));
assert_eq!(std::fs::metadata(stage.as_path()).expect("metadata").mode() & 0o777, 0o700, "private until placed");
assert_eq!(std::fs::read(stage.join(".git")).expect("pointer"), pointer);
assert_eq!(std::fs::read(stage.join("ignored/pkg/index.js")).expect("ignored state"), b"module.exports = 1;\n");
let index: Vec<u8> = std::fs::read(admin.join("index")).expect("index");
let entries = parse_index(index.as_slice(), ObjectFormat::Sha1).expect("a plain index");
assert_eq!(entries.len(), source.clean.tracked);
git(stage.as_path(), &["--git-dir", admin.to_str().expect("utf8"), "--work-tree", stage.to_str().expect("utf8"), "status", "--porcelain=v1"]);
remove(root.as_path());
}
#[test]
fn btrfs_a_source_that_changed_after_the_check_is_refused() {
let Some(root) = btrfs_fixture("snapshot-changed") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination);
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
std::fs::write(repo.join("shared.txt"), b"SECOND, LONGER\n").expect("edit after the check");
age(repo.join("shared.txt").as_path());
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
let reason: String = take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect_err("refused");
assert!(reason.contains("the source changed while it was copied"), "{reason}");
assert!(!admin.join("index").exists(), "no index is installed for a refused snapshot");
remove(root.as_path());
}
#[test]
fn btrfs_the_swap_puts_the_stage_at_the_destination() {
let Some(root) = btrfs_fixture("snapshot-swap") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination.clone());
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect("snapshot");
let made: ObjectId = id_of_directory(destination.as_path());
put_in_place(stage.as_path(), destination.as_path(), 0o750, made).expect("swap");
assert!(is_subvolume_root(destination.as_path()).expect("probe"));
assert_eq!(std::fs::metadata(destination.as_path()).expect("metadata").mode() & 0o7777, 0o750);
assert_eq!(std::fs::read(destination.join("shared.txt")).expect("tracked"), b"second, longer\n");
assert!(stage.join(".git").is_file() && !stage.join("shared.txt").exists(), "Git's directory is at the stage name");
assert_eq!(settle_displaced(stage.as_path(), destination.as_path(), made, pointer.as_slice()), Vec::<String>::new());
assert!(!stage.exists());
assert_eq!(git(destination.as_path(), &["status", "--porcelain=v1"]).stdout, b"");
remove(root.as_path());
}
fn stage_for_stubs(name: &str) -> (PathBuf, File) {
let root: PathBuf = crate::test_support::fixture(name);
std::fs::create_dir_all(root.join("stage")).expect("stage");
let held: File = open_root(std::fs::canonicalize(root.join("stage")).expect("canonical").as_path()).expect("handle");
return (root, held);
}
#[test]
fn a_stub_reached_through_a_link_removes_nothing_the_link_points_at() {
let (root, stage) = stage_for_stubs("snapshot-stub-link");
let victim: PathBuf = root.join("elsewhere/moved/wt");
std::fs::create_dir_all(victim.as_path()).expect("the original nested worktree");
std::fs::write(victim.join(".git"), b"gitdir: /original\n").expect("its pointer");
std::fs::create_dir_all(root.join("stage/ignored")).expect("ignored");
std::os::unix::fs::symlink(root.join("elsewhere/moved"), root.join("stage/ignored/group")).expect("the link the snapshot carries");
let source_root: &Path = Path::new("/src");
for pointer in [false, true] {
let error: String = remove_stub(&stage, source_root, Path::new("/src/ignored/group/wt"), pointer).expect_err("refused");
assert!(error.contains("link"), "{error}");
assert_eq!(std::fs::read(victim.join(".git")).expect("the original's pointer"), b"gitdir: /original\n", "pointer {pointer}");
}
std::fs::create_dir_all(root.join("stage/ignored/real/wt")).expect("a real stub");
std::fs::create_dir_all(root.join("stage/ignored/real/dest")).expect("a real destination");
std::fs::write(root.join("stage/ignored/real/dest/.git"), b"gitdir: x\n").expect("Git's pointer");
remove_stub(&stage, source_root, Path::new("/src/ignored/real/wt"), false).expect("empty stub removed");
remove_stub(&stage, source_root, Path::new("/src/ignored/real/dest"), true).expect("destination removed");
assert!(!root.join("stage/ignored/real/wt").exists() && !root.join("stage/ignored/real/dest").exists());
remove(root.as_path());
}
#[test]
fn a_nested_stub_is_removed_only_as_an_empty_directory() {
let (root, stage) = stage_for_stubs("snapshot-stub-full");
std::fs::create_dir_all(root.join("stage/nested")).expect("nested");
std::fs::write(root.join("stage/nested/.git"), b"gitdir: y\n").expect("a pointer file in it");
let error: String = remove_stub(&stage, Path::new("/src"), Path::new("/src/nested"), false).expect_err("not empty");
assert!(error.contains("empty directory"), "{error}");
assert!(root.join("stage/nested/.git").exists());
remove_stub(&stage, Path::new("/src"), Path::new("/elsewhere/nested"), false).expect("outside the source");
remove_stub(&stage, Path::new("/src"), Path::new("/src/missing/deep"), true).expect("absent");
remove(root.as_path());
}
#[test]
fn btrfs_a_destination_swapped_before_the_exchange_is_refused() {
let Some(root) = btrfs_fixture("snapshot-swap-destination") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination.clone());
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect("snapshot");
let identity: ObjectId = id_of_directory(destination.as_path());
std::fs::rename(destination.as_path(), root.join("topic.moved")).expect("moved away");
std::fs::create_dir(destination.as_path()).expect("another directory");
let reason: String = put_in_place(stage.as_path(), destination.as_path(), 0o750, identity).expect_err("refused").reason;
assert!(reason.contains("not the directory Git made"), "{reason}");
assert!(stage.join("shared.txt").exists() && std::fs::read_dir(destination.as_path()).expect("listing").count() == 0, "nothing moved");
remove(root.as_path());
}
#[test]
fn btrfs_a_destination_replaced_by_a_link_is_refused() {
let Some(root) = btrfs_fixture("snapshot-link-destination") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination.clone());
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect("snapshot");
let identity: ObjectId = id_of_directory(destination.as_path());
std::fs::rename(destination.as_path(), root.join("topic.moved")).expect("moved away");
std::os::unix::fs::symlink(root.join("topic.moved"), destination.as_path()).expect("a link, the very same inode behind it");
assert!(put_in_place(stage.as_path(), destination.as_path(), 0o750, identity).is_err(), "a link is not the directory, whatever it points at");
remove(root.as_path());
}
#[test]
fn btrfs_a_held_index_lock_leaves_the_index_alone() {
let Some(root) = btrfs_fixture("snapshot-index-lock") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination);
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
std::fs::write(admin.join("index.lock"), b"held by another Git").expect("a lock");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
let reason: String = take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect_err("refused");
assert!(reason.contains("index.lock"), "{reason}");
assert!(!admin.join("index").exists(), "no index was written before the lock was held");
assert_eq!(std::fs::read(admin.join("index.lock")).expect("lock"), b"held by another Git", "the other's lock stays");
std::fs::write(admin.join("index"), b"sentinel").expect("an index");
take(&source, &plan, stage.with_file_name(".cli-git-worktree-copy-test-two").as_path(), admin.as_path(), pointer.as_slice()).expect_err("refused again");
assert_eq!(std::fs::read(admin.join("index")).expect("index"), b"sentinel");
remove(root.as_path());
}
#[test]
fn btrfs_the_snapshot_is_checked_against_the_frozen_index() {
let Some(root) = btrfs_fixture("snapshot-frozen") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
crate::worktree_ladder_test_support::tracked(repo.as_path(), "later.txt", b"later\n");
crate::worktree_ladder_test_support::commit_all(repo.as_path(), "later");
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination);
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
git(repo.as_path(), &["reset", "--quiet", "--hard", "HEAD~1"]);
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
let reason: String = take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect_err("refused");
assert!(reason.contains("later.txt"), "{reason}");
assert!(!admin.join("index").exists());
remove(root.as_path());
}
fn ready_to_swap(root: &Path, name: &str) -> (PathBuf, PathBuf, ObjectId, Vec<u8>) {
let repo: PathBuf = subvolume_repository(root);
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), name);
let plan = plan_to(destination.clone());
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
take(&source, &plan, stage.as_path(), admin.as_path(), pointer.as_slice()).expect("snapshot");
let made: ObjectId = id_of_directory(destination.as_path());
return (destination, stage, made, pointer);
}
#[test]
fn btrfs_files_added_to_the_destination_before_the_exchange_are_kept() {
let Some(root) = btrfs_fixture("snapshot-added-before-swap") else { return };
let (destination, stage, made, pointer) = ready_to_swap(root.as_path(), "topic");
let mut add = || {
std::fs::write(destination.join("notes"), b"written while the worktree was made").expect("a new file");
std::fs::write(destination.join("shared.txt"), b"a file the snapshot also has").expect("a colliding file");
};
put_in_place_with(stage.as_path(), destination.as_path(), 0o750, made, &mut PlaceSeams { after_check: &mut add, before_swap_back: &mut || {} }).expect("swap");
let notes: Vec<String> = settle_displaced(stage.as_path(), destination.as_path(), made, pointer.as_slice());
assert_eq!(std::fs::read(destination.join("shared.txt")).expect("tracked"), b"second, longer\n", "the snapshot's file was not replaced");
assert_eq!(std::fs::read(stage.join("shared.txt")).expect("kept in place"), b"a file the snapshot also has");
let kept_notes: bool = stage.join("notes").exists();
assert_eq!(kept_notes, !destination.join("notes").exists(), "the file is in exactly one place");
assert_eq!(std::fs::read(if kept_notes { stage.join("notes") } else { destination.join("notes") }).expect("kept"), b"written while the worktree was made");
assert!(notes.iter().any(|note: &String| { return note.contains("shared.txt") && note.contains("stays"); }), "{notes:?}");
assert!(stage.exists() && !stage.join(".git").exists(), "the displaced directory stays with the files, and Git's pointer went");
remove(root.as_path());
}
#[test]
fn btrfs_a_displaced_directory_with_only_gits_pointer_is_removed() {
let Some(root) = btrfs_fixture("snapshot-displaced-clean") else { return };
let (destination, stage, made, pointer) = ready_to_swap(root.as_path(), "topic");
put_in_place(stage.as_path(), destination.as_path(), 0o750, made).expect("swap");
assert_eq!(settle_displaced(stage.as_path(), destination.as_path(), made, pointer.as_slice()), Vec::<String>::new());
assert!(!stage.exists());
remove(root.as_path());
}
#[test]
fn btrfs_a_displaced_directory_that_is_not_gits_is_left_alone() {
let Some(root) = btrfs_fixture("snapshot-displaced-foreign") else { return };
let (destination, stage, made, pointer) = ready_to_swap(root.as_path(), "topic");
put_in_place(stage.as_path(), destination.as_path(), 0o750, made).expect("swap");
std::fs::write(stage.join(".git"), b"gitdir: somewhere else\n").expect("another pointer");
let notes: Vec<String> = settle_displaced(stage.as_path(), destination.as_path(), made, pointer.as_slice());
assert!(notes.iter().any(|note: &String| { return note.contains("is not the pointer Git wrote"); }), "{notes:?}");
assert!(stage.join(".git").exists(), "the file stays");
let other: ObjectId = ObjectId { device: made.device, inode: made.inode + 1_000_000, birth: made.birth };
let refused: Vec<String> = settle_displaced(stage.as_path(), destination.as_path(), other, pointer.as_slice());
assert!(refused.iter().any(|note: &String| { return note.contains("is not the directory Git made"); }), "{refused:?}");
assert!(stage.exists());
remove(root.as_path());
}
#[test]
fn btrfs_a_destination_replaced_at_the_exchange_is_swapped_back() {
let Some(root) = btrfs_fixture("snapshot-replaced-at-swap") else { return };
let (destination, stage, made, _pointer) = ready_to_swap(root.as_path(), "topic");
let mut replace = || {
std::fs::rename(destination.as_path(), root.join("topic.moved")).expect("moved away");
std::fs::create_dir(destination.as_path()).expect("another directory");
std::fs::write(destination.join("theirs"), b"someone's directory").expect("their file");
};
let error: PlaceError = put_in_place_with(stage.as_path(), destination.as_path(), 0o750, made, &mut PlaceSeams { after_check: &mut replace, before_swap_back: &mut || {} }).expect_err("refused");
assert!(error.stage_is_ours, "{error:?}");
assert_eq!(std::fs::read(destination.join("theirs")).expect("their directory is back"), b"someone's directory");
assert!(stage.join("shared.txt").exists(), "the snapshot is at the stage name again");
remove(root.as_path());
}
#[test]
fn btrfs_a_failed_swap_back_does_not_claim_the_stage() {
let Some(root) = btrfs_fixture("snapshot-failed-swap-back") else { return };
let (destination, stage, made, _pointer) = ready_to_swap(root.as_path(), "topic");
let mut replace = || {
std::fs::rename(destination.as_path(), root.join("topic.moved")).expect("moved away");
std::fs::create_dir(destination.as_path()).expect("another directory");
std::fs::write(destination.join("theirs"), b"someone's directory").expect("their file");
};
let mut remove_published = || {
std::fs::rename(destination.as_path(), root.join("topic.gone")).expect("the published destination moves away");
};
let error: PlaceError = put_in_place_with(stage.as_path(), destination.as_path(), 0o750, made, &mut PlaceSeams { after_check: &mut replace, before_swap_back: &mut remove_published }).expect_err("refused");
assert!(!error.stage_is_ours, "{error:?}");
assert_eq!(std::fs::read(stage.join("theirs")).expect("their directory is at the stage name"), b"someone's directory");
remove(root.as_path());
}
#[test]
fn btrfs_the_stage_a_snapshot_made_is_identified_even_when_the_snapshot_fails() {
let Some(root) = btrfs_fixture("snapshot-stage-identity") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let (destination, admin, pointer) = no_checkout_worktree(repo.as_path(), "topic");
let plan = plan_to(destination);
let source = choose_snapshot_source(&git_context(), &main_identity(repo.as_path()), &plan).expect("a source");
std::fs::write(admin.join("index.lock"), b"held by another Git").expect("a lock");
let stage: PathBuf = root.join(".cli-git-worktree-copy-test");
let held: File = open_root(admin.as_path()).expect("administrative directory");
let failure: SnapshotFailure = take_snapshot(&source, &plan, stage.as_path(), &held, pointer.as_slice()).expect_err("refused");
assert_eq!(failure.stage, Some(id_of_directory(stage.as_path())), "the failed run names the stage it made");
std::fs::remove_file(admin.join("index.lock")).expect("lock gone");
let stage_two: PathBuf = root.join(".cli-git-worktree-copy-test-two");
let made: ObjectId = take_snapshot(&source, &plan, stage_two.as_path(), &held, pointer.as_slice()).expect("snapshot").stage;
assert_eq!(made, id_of_directory(stage_two.as_path()));
remove(root.as_path());
}