use super::*;
use crate::fs_btrfs_test_support::btrfs_fixture;
use crate::test_support::{git, git_context, git_text, remove, repository};
use crate::worktree_ladder_test_support::{commit_all, main_identity, tracked};
use crate::worktree_snapshot_source::common_source;
use crate::worktree_clean::stored_stats;
use crate::subvolume_index::StatFields;
fn clone_fixture(root: &Path) -> (PathBuf, PathBuf, PathBuf) {
let repo: PathBuf = std::fs::canonicalize(repository(root, "repo")).expect("canonical");
tracked(repo.as_path(), ".gitignore", b"ignored/\n");
tracked(repo.as_path(), "plain.txt", b"plain\n");
tracked(repo.as_path(), "bin/tool.sh", b"#!/bin/sh\n");
std::fs::set_permissions(repo.join("bin/tool.sh"), std::fs::Permissions::from_mode(0o755)).expect("executable");
tracked(repo.as_path(), "empty.txt", b"");
std::os::unix::fs::symlink("plain.txt", repo.join("link")).expect("link");
crate::test_support::age(repo.join("link").as_path());
commit_all(repo.as_path(), "files");
git(repo.as_path(), &["worktree", "add", "--quiet", "--no-checkout", "-b", "topic", "../topic"]);
let destination: PathBuf = std::fs::canonicalize(root.join("topic")).expect("destination");
let admin: PathBuf = repo.join(".git/worktrees/topic");
return (repo, destination, admin);
}
#[test]
fn only_plain_paths_become_entries() {
assert_eq!(entry_names(b"a/b.txt"), Ok(vec![OsString::from("a"), OsString::from("b.txt")]));
for path in [b"../x".as_slice(), b"/etc/passwd", b"", b".git/config", b"a/../../b", b"a/.git/x"] {
assert!(entry_names(path).is_err(), "{}", String::from_utf8_lossy(path));
}
}
#[test]
fn btrfs_files_links_modes_and_times_are_cloned_and_the_index_is_installed() {
let Some(root) = btrfs_fixture("clone-files") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
clone_into(&source, destination.as_path(), admin.as_path()).expect("clones");
assert_eq!(std::fs::read(destination.join("plain.txt")).expect("plain"), b"plain\n");
assert_eq!(std::fs::read_link(destination.join("link")).expect("link"), PathBuf::from("plain.txt"));
for name in ["plain.txt", "bin/tool.sh", "empty.txt"] {
let (from, to) = (std::fs::metadata(repo.join(name)).expect("source"), std::fs::metadata(destination.join(name)).expect("clone"));
assert_eq!((to.mode() & 0o7777, to.mtime(), to.mtime_nsec(), to.len()), (from.mode() & 0o7777, from.mtime(), from.mtime_nsec(), from.len()), "{name}");
}
let status: String = git_text(destination.as_path(), &["status", "--porcelain=v1"]);
assert_eq!(status, "", "the first status of the clone is clean");
remove(root.as_path());
}
#[test]
fn btrfs_a_changed_source_is_refused_and_the_partial_destination_is_left_for_the_checkout() {
let Some(root) = btrfs_fixture("clone-changed") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
std::fs::write(repo.join("plain.txt"), b"PLAIN\n").expect("edit after the check");
crate::test_support::age(repo.join("plain.txt").as_path());
let reason: String = clone_into(&source, destination.as_path(), admin.as_path()).expect_err("refused");
assert!(reason.contains("the source changed while it was cloned"), "{reason}");
assert!(!admin.join("index").exists(), "no index for a refused clone");
assert!(reason.contains("entries it created stay"), "{reason}");
assert!(destination.join(".gitignore").exists() || destination.join("bin").exists(), "what was cloned stays");
remove(root.as_path());
}
#[test]
fn btrfs_a_missing_source_file_ends_the_rung() {
let Some(root) = btrfs_fixture("clone-missing") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
std::fs::remove_file(repo.join("plain.txt")).expect("remove");
let reason: String = clone_into(&source, destination.as_path(), admin.as_path()).expect_err("refused");
assert!(reason.contains("plain.txt"), "{reason}");
remove(root.as_path());
}
fn clone_into(source: &SnapshotSource, destination: &Path, admin: &Path) -> Result<Cloned, String> {
let held: File = open_root(destination).expect("destination");
let administrative: File = open_root(admin).expect("administrative directory");
return clone_tracked_files(source, &held, &administrative);
}
fn entry_through(handles: (&File, &File), components: &[&str], entry: &StatEntry, source: &SnapshotSource, created: &mut Vec<Created>, seams: &mut Seams<'_>) -> Result<(), String> {
let names: Vec<OsString> = components.iter().map(OsString::from).collect();
let work: Work<'_> = Work { entry, names: names.as_slice(), index_seconds: source.clean.index_seconds };
let mut stats: HashMap<Vec<u8>, StatFields> = HashMap::new();
return clone_entry(handles.0, handles.1, &work, (created, &mut stats), seams);
}
fn handles(source: &SnapshotSource, repo: &Path, destination: &Path) -> (Vec<StatEntry>, File, File) {
let entries: Vec<StatEntry> = stored_stats(source.clean.index_bytes.as_slice(), source.clean.hash_length).expect("entries");
return (entries, open_root(repo).expect("source root"), open_root(destination).expect("destination"));
}
fn entry_of<'a>(entries: &'a [StatEntry], path: &str) -> &'a StatEntry {
return entries.iter().find(|entry: &&StatEntry| { return entry.path == path.as_bytes(); }).expect("a tracked path");
}
#[test]
fn btrfs_a_file_swapped_between_the_look_and_the_open_is_not_cloned() {
let Some(root) = btrfs_fixture("clone-swap-file") else { return };
let (repo, destination, _admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
let (entries, source_root, destination_root) = handles(&source, repo.as_path(), destination.as_path());
let modified = std::fs::metadata(repo.join("plain.txt")).expect("metadata").modified().expect("time");
let mut swap = || {
std::fs::write(repo.join("swap.tmp"), b"PLAIN\n").expect("the other file, one byte of difference per letter, same size");
std::fs::File::options().write(true).open(repo.join("swap.tmp")).expect("open").set_modified(modified).expect("the same time");
std::fs::rename(repo.join("swap.tmp"), repo.join("plain.txt")).expect("swap");
};
let mut created: Vec<Created> = Vec::new();
let reason: String = entry_through((&source_root, &destination_root), &["plain.txt"], entry_of(entries.as_slice(), "plain.txt"), &source, &mut created, &mut Seams { after_lookup: &mut swap, after_clone: &mut || {}, before_index: &mut || {} })
.expect_err("refused");
assert!(reason.contains("the source changed while it was cloned") && reason.contains("differs"), "{reason}");
assert!(!destination.join("plain.txt").exists() && created.is_empty(), "the other file's bytes were not cloned");
remove(root.as_path());
}
#[test]
fn btrfs_a_file_written_during_the_clone_is_refused_and_listed() {
let Some(root) = btrfs_fixture("clone-write-during") else { return };
let (repo, destination, _admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
let (entries, source_root, destination_root) = handles(&source, repo.as_path(), destination.as_path());
let mut append = || {
let mut file = std::fs::OpenOptions::new().append(true).open(repo.join("plain.txt")).expect("open");
std::io::Write::write_all(&mut file, b"more\n").expect("append");
};
let mut created: Vec<Created> = Vec::new();
let reason: String = entry_through((&source_root, &destination_root), &["plain.txt"], entry_of(entries.as_slice(), "plain.txt"), &source, &mut created, &mut Seams { after_lookup: &mut || {}, after_clone: &mut append, before_index: &mut || {} })
.expect_err("refused");
assert!(reason.contains("written during the clone"), "{reason}");
assert_eq!(created.iter().map(|entry: &Created| { return entry.names.clone(); }).collect::<Vec<_>>(), vec![vec![OsString::from("plain.txt")]]);
remove(root.as_path());
}
#[test]
fn btrfs_a_parent_directory_swapped_after_it_was_opened_is_read_through_its_handle() {
let Some(root) = btrfs_fixture("clone-swap-parent") else { return };
let (repo, destination, _admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
let (entries, _source_root, destination_root) = handles(&source, repo.as_path(), destination.as_path());
let held_parent: File = open_root(repo.join("bin").as_path()).expect("the parent, opened first");
std::fs::rename(repo.join("bin"), repo.join("bin.orig")).expect("renamed away");
std::fs::create_dir(repo.join("bin")).expect("stand-in");
std::fs::write(repo.join("bin/tool.sh"), b"#!/bin/sh\n").expect("a stand-in of the same size");
let mut created: Vec<Created> = Vec::new();
let mut seams: Seams<'_> = Seams { after_lookup: &mut || {}, after_clone: &mut || {}, before_index: &mut || {} };
let mut tool: StatEntry = entry_of(entries.as_slice(), "bin/tool.sh").clone();
tool.path = b"tool.sh".to_vec();
let outcome = entry_through((&held_parent, &destination_root), &["tool.sh"], &tool, &source, &mut created, &mut seams);
assert_eq!(outcome, Ok(()), "the original inode matches its entry although the path now names another");
assert_ne!(std::fs::metadata(destination.join("tool.sh")).expect("clone").ino(), std::fs::metadata(repo.join("bin/tool.sh")).expect("stand-in").ino());
assert_eq!(std::fs::read(destination.join("tool.sh")).expect("clone"), std::fs::read(repo.join("bin.orig/tool.sh")).expect("original"));
remove(root.as_path());
}
#[test]
fn btrfs_a_failed_run_removes_nothing_and_counts_what_stays() {
let Some(root) = btrfs_fixture("clone-created-only") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
std::fs::write(destination.join("keep.txt"), b"mine").expect("a file that was there");
std::fs::create_dir(destination.join("bin")).expect("a directory that was there");
std::fs::write(destination.join("bin/mine"), b"mine").expect("its content");
std::fs::write(repo.join("plain.txt"), b"PLAIN\n").expect("edit after the check");
crate::test_support::age(repo.join("plain.txt").as_path());
let reason: String = clone_into(&source, destination.as_path(), admin.as_path()).expect_err("refused");
assert!(reason.contains("the source changed while it was cloned") && reason.contains("entries it created stay"), "{reason}");
let mut left: Vec<String> = walk(destination.as_path());
left.sort();
for kept in [".git", "bin", "bin/mine", "keep.txt"] {
assert!(left.iter().any(|name: &String| { return name == kept; }), "{kept} stays: {left:?}");
}
assert!(left.iter().any(|name: &String| { return name == "bin/tool.sh" || name == ".gitignore"; }), "the clones that were made stay: {left:?}");
remove(root.as_path());
}
fn walk(top: &Path) -> Vec<String> {
let mut found: Vec<String> = Vec::new();
let mut pending: Vec<PathBuf> = vec![top.to_path_buf()];
while let Some(directory) = pending.pop() {
for entry in std::fs::read_dir(directory.as_path()).expect("listing").flatten() {
found.push(entry.path().strip_prefix(top).expect("below").to_string_lossy().into_owned());
if entry.file_type().is_ok_and(|kind: std::fs::FileType| { return kind.is_dir(); }) {
pending.push(entry.path());
}
}
}
return found;
}
#[test]
fn btrfs_the_frozen_index_is_used_not_a_second_read_of_the_file() {
let Some(root) = btrfs_fixture("clone-frozen") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
tracked(repo.as_path(), "later.txt", b"later\n");
commit_all(repo.as_path(), "later");
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
git(repo.as_path(), &["reset", "--quiet", "--hard", "HEAD~1"]);
let reason: String = clone_into(&source, destination.as_path(), admin.as_path()).expect_err("refused");
assert!(reason.contains("later.txt"), "{reason}");
assert!(!admin.join("index").exists());
remove(root.as_path());
}
#[test]
fn the_report_of_what_stays_names_the_count_the_examples_and_the_command() {
let created: Vec<Created> = vec![
Created { names: vec![OsString::from("dir")], directory: true },
Created { names: vec![OsString::from("dir"), OsString::from("a.txt")], directory: false },
Created { names: vec![OsString::from("b.txt")], directory: false },
];
let report: String = leftover_report(created.as_slice(), Path::new("/base/topic"));
assert!(report.starts_with("3 entries") && report.contains("dir/a.txt, b.txt") && report.contains("git -C /base/topic status"), "{report}");
}
#[test]
fn btrfs_a_destination_file_replaced_before_the_index_is_not_certified() {
let Some(root) = btrfs_fixture("clone-replaced-before-index") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
let modified = std::fs::metadata(repo.join("plain.txt")).expect("metadata").modified().expect("time");
let mut replace = || {
std::fs::rename(destination.join("plain.txt"), destination.join("plain.orig")).expect("moved");
std::fs::write(destination.join("plain.txt"), b"PLAIN\n").expect("a replacement of the same size");
std::fs::File::options().write(true).open(destination.join("plain.txt")).expect("open").set_modified(modified).expect("the same time");
};
let mut directories: Directories = HashMap::new();
let held: File = open_root(destination.as_path()).expect("destination");
directories.insert(Vec::new(), held.try_clone().expect("clone handle"));
let administrative: File = open_root(admin.as_path()).expect("admin");
let mut created: Vec<Created> = Vec::new();
clone_with(&source, &mut directories, &administrative, &mut created, &mut Seams { after_lookup: &mut || {}, after_clone: &mut || {}, before_index: &mut replace }).expect("clones");
let entries = stored_stats(std::fs::read(admin.join("index")).expect("index").as_slice(), source.clean.hash_length).expect("entries");
let recorded = entry_of(entries.as_slice(), "plain.txt").stored.ino;
assert_eq!(recorded, std::fs::metadata(destination.join("plain.orig")).expect("the clone").ino() as u32, "the inode that was cloned");
assert_ne!(recorded, std::fs::metadata(destination.join("plain.txt")).expect("the replacement").ino() as u32);
remove(root.as_path());
}
#[test]
fn btrfs_a_destination_directory_on_another_device_is_refused() {
let Some(root) = btrfs_fixture("clone-device") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
crate::fs_btrfs_ioctl::subvolume_create_at(destination.join("bin").as_path()).expect("a subvolume where bin goes");
let reason: String = clone_into(&source, destination.as_path(), admin.as_path()).expect_err("refused");
assert!(reason.contains("another device"), "{reason}");
assert!(!destination.join("bin/tool.sh").exists(), "nothing was created in it");
remove(root.as_path());
}
#[test]
fn btrfs_a_destination_link_replaced_before_the_index_is_not_certified() {
let Some(root) = btrfs_fixture("clone-link-replaced-before-index") else { return };
let (repo, destination, admin) = clone_fixture(root.as_path());
let source = common_source(&git_context(), &main_identity(repo.as_path())).expect("a source");
let mut replace = || {
std::fs::remove_file(destination.join("link")).expect("the clone's link goes");
std::os::unix::fs::symlink("plainXtxt", destination.join("link")).expect("a link of the same length");
};
let mut directories: Directories = HashMap::new();
let held: File = open_root(destination.as_path()).expect("destination");
directories.insert(Vec::new(), held.try_clone().expect("clone handle"));
let administrative: File = open_root(admin.as_path()).expect("admin");
let mut created: Vec<Created> = Vec::new();
clone_with(&source, &mut directories, &administrative, &mut created, &mut Seams { after_lookup: &mut || {}, after_clone: &mut || {}, before_index: &mut replace }).expect("clones");
let entries = stored_stats(std::fs::read(admin.join("index")).expect("index").as_slice(), source.clean.hash_length).expect("entries");
let seed = stored_stats(source.clean.index_bytes.as_slice(), source.clean.hash_length).expect("seed");
let recorded = entry_of(entries.as_slice(), "link").stored;
assert_eq!(recorded.ino, entry_of(seed.as_slice(), "link").stored.ino, "the stored fields are the source's, not the live name's");
assert_ne!(recorded.ino, std::fs::symlink_metadata(destination.join("link")).expect("replacement").ino() as u32);
remove(root.as_path());
}