use core::sync::atomic::{AtomicBool, Ordering};
use std::fs::{self, File, FileTimes};
use std::io::ErrorKind;
use std::process::{Command, Stdio};
use std::sync::{Arc, LazyLock, Mutex, PoisonError};
use std::time::SystemTime;
use camino::{Utf8Path, Utf8PathBuf};
use ignore::{WalkBuilder, WalkState};
use crate::error::{Error, error};
use crate::{HashMap, Result};
pub(super) const VCS_DIRS: [&str; 7] = [".git", ".hg", ".bzr", ".svn", "_darcs", ".jj", ".pijul"];
pub(super) fn visible_vcs_metadata(path: &Utf8Path) -> Vec<Utf8PathBuf> {
let mut found = Vec::new();
let mut directory = crate::paths::physical(path).unwrap_or_else(|_unresolved| path.to_path_buf());
loop {
for name in VCS_DIRS {
let marker = directory.join(name);
if fs::symlink_metadata(marker.as_std_path()).is_ok() {
found.push(crate::paths::physical(&marker).unwrap_or(marker));
}
}
let Some(parent) = directory.parent() else {
break;
};
if parent == directory {
break;
}
directory = parent.to_path_buf();
}
found.sort();
found.dedup();
found
}
#[derive(Clone, Debug)]
pub(super) struct Reflinks {
works: Arc<AtomicBool>,
}
static CAPABILITIES: LazyLock<Mutex<HashMap<Utf8PathBuf, Arc<AtomicBool>>>> = LazyLock::new(|| Mutex::new(HashMap::default()));
impl Reflinks {
pub(super) fn for_destination(destination: &Utf8Path) -> Self {
let mut known = CAPABILITIES.lock().unwrap_or_else(PoisonError::into_inner);
let works = Arc::clone(
known
.entry(destination.to_owned())
.or_insert_with(|| Arc::new(AtomicBool::new(true))),
);
Self { works }
}
#[cfg(test)]
pub(super) fn isolated() -> Self {
Self {
works: Arc::new(AtomicBool::new(true)),
}
}
pub(super) fn worth_trying(&self) -> bool {
reflink_supported() && self.works.load(Ordering::Relaxed)
}
pub(super) fn unsupported(&self) {
self.works.store(false, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct CopyOptions {
pub(super) copy_ignored: bool,
}
#[cfg(test)]
pub(super) fn copy_tree(from: &Utf8Path, to: &Utf8Path, skip: &Utf8Path) -> Result<()> {
copy_tree_with(from, to, skip, CopyOptions::default(), &Reflinks::isolated())
}
pub(super) fn copy_tree_with(from: &Utf8Path, to: &Utf8Path, skip: &Utf8Path, options: CopyOptions, reflinks: &Reflinks) -> Result<()> {
fs::create_dir_all(to.as_std_path()).map_err(|cause| error!("could not create the scratch tree at `{to}`").caused_by(cause))?;
let failure: Mutex<Option<Error>> = Mutex::new(None);
let mut builder = WalkBuilder::new(from.as_std_path());
let _builder = builder
.hidden(false)
.parents(false)
.require_git(true)
.git_ignore(!options.copy_ignored)
.git_exclude(!options.copy_ignored)
.git_global(false)
.ignore(false)
.follow_links(false);
let root = from.to_owned();
let destination = to.to_owned();
let excluded = skip.to_owned();
builder.build_parallel().run(|| {
let root = root.clone();
let destination = destination.clone();
let excluded = excluded.clone();
let failure = &failure;
let reflinks = reflinks.clone();
Box::new(move |entry| {
let entry = match entry {
Ok(entry) => entry,
Err(cause) => {
record(failure, error!("could not read the source tree").caused_by(cause));
return WalkState::Quit;
}
};
let Some(source) = Utf8Path::from_path(entry.path()) else {
record(
failure,
error!("`{}` is not valid UTF-8 and cannot be copied", entry.path().display()),
);
return WalkState::Quit;
};
let Ok(relative) = source.strip_prefix(&root) else {
return WalkState::Continue;
};
if relative.as_str().is_empty() {
return WalkState::Continue;
}
if is_pruned(source, relative, &excluded) {
return WalkState::Skip;
}
match copy_entry(source, &destination.join(relative), &reflinks) {
Ok(()) => WalkState::Continue,
Err(cause) => {
record(failure, cause);
WalkState::Quit
}
}
})
});
match failure.into_inner() {
Ok(Some(cause)) => return Err(cause),
Ok(None) => {}
Err(poisoned) => {
if let Some(cause) = poisoned.into_inner() {
return Err(cause);
}
}
}
copy_tracked(from, to, skip, reflinks)
}
fn copy_tracked(from: &Utf8Path, to: &Utf8Path, skip: &Utf8Path, reflinks: &Reflinks) -> Result<()> {
let Some(tracked) = tracked_files(from)? else {
return Ok(());
};
for relative in tracked {
if is_pruned_anywhere(from, &relative, skip) {
continue;
}
let source = from.join(&relative);
let destination = to.join(&relative);
if fs::symlink_metadata(destination.as_std_path()).is_ok() {
continue;
}
if fs::symlink_metadata(source.as_std_path()).is_err() {
continue;
}
copy_entry(&source, &destination, reflinks)?;
}
Ok(())
}
pub(super) fn tracked_files(root: &Utf8Path) -> Result<Option<Vec<Utf8PathBuf>>> {
let output = Command::new("git")
.arg("-C")
.arg(root.as_std_path())
.args(["ls-files", "-z"])
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok();
let Some(output) = output else {
return Ok(None);
};
if !output.status.success() {
return Ok(None);
}
let mut tracked = Vec::new();
for name in output.stdout.split(|byte| *byte == 0).filter(|name| !name.is_empty()) {
let name = str::from_utf8(name)
.map_err(|cause| error!("git reported a tracked path that is not valid UTF-8 in `{root}`").caused_by(cause))?;
tracked.push(Utf8PathBuf::from(name));
}
Ok(Some(tracked))
}
fn is_pruned_anywhere(root: &Utf8Path, relative: &Utf8Path, excluded: &Utf8Path) -> bool {
let mut prefix = Utf8PathBuf::new();
for component in relative.components() {
prefix.push(component);
if is_pruned(&root.join(&prefix), &prefix, excluded) {
return true;
}
}
false
}
fn record(failure: &Mutex<Option<Error>>, cause: Error) {
if let Ok(mut held) = failure.lock()
&& held.is_none()
{
*held = Some(cause);
}
}
pub(super) fn is_pruned(source: &Utf8Path, relative: &Utf8Path, excluded: &Utf8Path) -> bool {
if source == excluded {
return true;
}
let Some(name) = relative.file_name() else {
return false;
};
if VCS_DIRS.contains(&name) {
return true;
}
name == "target" && (relative.parent() == Some(Utf8Path::new("")) || source.join("CACHEDIR.TAG").as_std_path().exists())
}
fn copy_entry(source: &Utf8Path, destination: &Utf8Path, reflinks: &Reflinks) -> Result<()> {
let metadata = fs::symlink_metadata(source.as_std_path()).map_err(|cause| error!("could not read `{source}`").caused_by(cause))?;
if metadata.is_dir() {
return fs::create_dir_all(destination.as_std_path()).map_err(|cause| error!("could not create `{destination}`").caused_by(cause));
}
if place(&metadata, source, destination, reflinks).is_err() {
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent.as_std_path()).map_err(|cause| error!("could not create `{parent}`").caused_by(cause))?;
}
return place(&metadata, source, destination, reflinks);
}
Ok(())
}
fn place(metadata: &fs::Metadata, source: &Utf8Path, destination: &Utf8Path, reflinks: &Reflinks) -> Result<()> {
if metadata.is_symlink() {
copy_symlink(source, destination)
} else {
copy_file(source, destination, reflinks)
}
}
fn copy_symlink(source: &Utf8Path, destination: &Utf8Path) -> Result<()> {
let target = fs::read_link(source.as_std_path()).map_err(|cause| error!("could not read the link `{source}`").caused_by(cause))?;
#[cfg(unix)]
let created = std::os::unix::fs::symlink(&target, destination.as_std_path());
#[cfg(windows)]
let created = if source
.parent()
.map_or_else(|| target.is_dir(), |parent| parent.as_std_path().join(&target).is_dir())
{
std::os::windows::fs::symlink_dir(&target, destination.as_std_path())
} else {
std::os::windows::fs::symlink_file(&target, destination.as_std_path())
};
created.map_err(|cause| error!("could not recreate the link `{destination}`").caused_by(cause))
}
fn copy_file(source: &Utf8Path, destination: &Utf8Path, reflinks: &Reflinks) -> Result<()> {
if reflinks.worth_trying() {
match reflink_copy::reflink(source.as_std_path(), destination.as_std_path()) {
Ok(()) => {
freshen(destination);
return Ok(());
}
Err(cause) if cause.kind() == ErrorKind::NotFound => {
return Err(error!("could not copy `{source}` to `{destination}`").caused_by(cause));
}
Err(_unsupported) => {
reflinks.unsupported();
let _removed = fs::remove_file(destination.as_std_path());
}
}
}
let _bytes = fs::copy(source.as_std_path(), destination.as_std_path())
.map_err(|cause| error!("could not copy `{source}` to `{destination}`").caused_by(cause))?;
Ok(())
}
const fn reflink_supported() -> bool {
!cfg!(target_env = "musl")
}
fn freshen(destination: &Utf8Path) {
if let Ok(file) = File::options().write(true).open(destination.as_std_path()) {
let _stamped = file.set_times(FileTimes::new().set_modified(SystemTime::now()));
}
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use super::*;
fn tree() -> (tempfile::TempDir, Utf8PathBuf, Utf8PathBuf) {
let temporary = tempfile::tempdir().unwrap();
let from = Utf8PathBuf::from_path_buf(temporary.path().join("from")).unwrap();
let to = Utf8PathBuf::from_path_buf(temporary.path().join("to")).unwrap();
fs::create_dir_all(from.as_std_path()).unwrap();
(temporary, from, to)
}
fn git(root: &Utf8Path, arguments: &[&str]) -> bool {
Command::new("git")
.arg("-C")
.arg(root.as_std_path())
.args(arguments)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
fn ignored_repository(from: &Utf8Path) -> bool {
if !git(from, &["init"]) {
return false;
}
fs::write(from.join(".gitignore").as_std_path(), "**/[Bb]in/*\n").unwrap();
fs::create_dir_all(from.join("src").join("bin").as_std_path()).unwrap();
fs::write(from.join("src").join("bin").join("helper.rs").as_std_path(), "fn tracked() {}").unwrap();
fs::write(from.join("src").join("bin").join("scratch.rs").as_std_path(), "fn untracked() {}").unwrap();
git(from, &["add", "-f", "src/bin/helper.rs"])
}
#[test]
fn a_tracked_file_is_copied_even_when_an_ignore_rule_matches_it() {
let (_temporary, from, to) = tree();
if !ignored_repository(&from) {
return;
}
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert_eq!(
fs::read_to_string(to.join("src").join("bin").join("helper.rs").as_std_path()).unwrap(),
"fn tracked() {}"
);
}
#[cfg(unix)]
#[test]
fn a_tracked_ignored_file_with_a_non_utf8_name_is_reported() {
use std::os::unix::ffi::OsStrExt as _;
let (_temporary, from, to) = tree();
if !git(&from, &["init"]) {
return;
}
let name = std::ffi::OsStr::from_bytes(b"ignored-\xff.rs");
fs::write(from.join(".gitignore").as_std_path(), "ignored-*\n").expect("ignore rule");
fs::write(from.as_std_path().join(name), "fn tracked() {}").expect("tracked file");
let added = Command::new("git")
.arg("-C")
.arg(from.as_std_path())
.args(["add", "-f", "--"])
.arg(name)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success());
if !added {
return;
}
let error = copy_tree(&from, &to, Utf8Path::new("/nowhere")).expect_err("the path cannot be represented in the scratch tree");
assert!(error.to_string().contains("not valid UTF-8"), "{error}");
}
#[test]
fn an_untracked_ignored_file_is_not_copied() {
let (_temporary, from, to) = tree();
if !ignored_repository(&from) {
return;
}
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert!(!to.join("src").join("bin").join("scratch.rs").as_std_path().exists());
}
#[test]
fn copying_ignored_files_takes_the_untracked_ones_too() {
let (_temporary, from, to) = tree();
if !ignored_repository(&from) {
return;
}
copy_tree_with(
&from,
&to,
Utf8Path::new("/nowhere"),
CopyOptions { copy_ignored: true },
&Reflinks::isolated(),
)
.unwrap();
assert!(to.join("src").join("bin").join("helper.rs").as_std_path().exists());
assert_eq!(
fs::read_to_string(to.join("src").join("bin").join("scratch.rs").as_std_path()).unwrap(),
"fn untracked() {}"
);
}
#[test]
fn a_directory_that_is_not_a_repository_is_copied_whole() {
let (_temporary, from, to) = tree();
fs::write(from.join(".gitignore").as_std_path(), "**/[Bb]in/*\n").unwrap();
fs::create_dir_all(from.join("src").join("bin").as_std_path()).unwrap();
fs::write(from.join("src").join("bin").join("helper.rs").as_std_path(), "fn f() {}").unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert!(to.join("src").join("bin").join("helper.rs").as_std_path().exists());
}
#[test]
fn a_tracked_file_under_a_pruned_directory_is_still_skipped() {
assert!(is_pruned_anywhere(
Utf8Path::new("/workspace"),
Utf8Path::new("target/keep.rs"),
Utf8Path::new("/workspace/scratch"),
));
assert!(is_pruned_anywhere(
Utf8Path::new("/workspace"),
Utf8Path::new("scratch/tree/keep.rs"),
Utf8Path::new("/workspace/scratch"),
));
assert!(!is_pruned_anywhere(
Utf8Path::new("/workspace"),
Utf8Path::new("src/keep.rs"),
Utf8Path::new("/workspace/scratch"),
));
}
#[test]
fn asking_a_non_repository_for_its_tracked_files_yields_nothing() {
let (_temporary, from, _to) = tree();
assert!(tracked_files(&from).expect("not being a repository is not an error").is_none());
}
#[test]
fn build_output_and_version_control_are_skipped() {
let (_temporary, from, to) = tree();
for directory in ["src", "target", ".git", ".jj", "_darcs"] {
fs::create_dir_all(from.join(directory).as_std_path()).unwrap();
fs::write(from.join(directory).join("f").as_std_path(), "x").unwrap();
}
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert!(to.join("src").join("f").as_std_path().exists());
for skipped in ["target", ".git", ".jj", "_darcs"] {
assert!(!to.join(skipped).as_std_path().exists(), "{skipped} was copied");
}
}
#[test]
fn a_nested_target_module_survives() {
let (_temporary, from, to) = tree();
fs::create_dir_all(from.join("src").join("target").as_std_path()).unwrap();
fs::write(from.join("src").join("target").join("mod.rs").as_std_path(), "fn f() {}").unwrap();
fs::create_dir_all(from.join("nested").join("target").as_std_path()).unwrap();
fs::write(from.join("nested").join("target").join("CACHEDIR.TAG").as_std_path(), "").unwrap();
fs::write(from.join("nested").join("target").join("junk").as_std_path(), "x").unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert!(to.join("src").join("target").join("mod.rs").as_std_path().exists());
assert!(!to.join("nested").join("target").join("junk").as_std_path().exists());
}
#[test]
fn nested_directories_are_recreated() {
let (_temporary, from, to) = tree();
fs::create_dir_all(from.join("a").join("b").join("c").as_std_path()).unwrap();
fs::write(from.join("a").join("b").join("c").join("deep.rs").as_std_path(), "fn f() {}").unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert_eq!(
fs::read_to_string(to.join("a").join("b").join("c").join("deep.rs").as_std_path()).unwrap(),
"fn f() {}"
);
}
#[test]
fn the_scratch_directory_is_not_copied_into_itself() {
let (_temporary, from, to) = tree();
let skip = from.join("scratch");
fs::create_dir_all(skip.as_std_path()).unwrap();
fs::write(skip.join("f").as_std_path(), "x").unwrap();
fs::create_dir_all(from.join("src").as_std_path()).unwrap();
copy_tree(&from, &to, &skip).unwrap();
assert!(!to.join("scratch").as_std_path().exists());
assert!(to.join("src").as_std_path().exists());
}
#[cfg(unix)]
#[test]
fn a_symlink_is_recreated_rather_than_followed() {
let (_temporary, from, to) = tree();
let outside = from.parent().unwrap().join("outside");
fs::create_dir_all(outside.as_std_path()).unwrap();
fs::write(outside.join("secret").as_std_path(), "x").unwrap();
std::os::unix::fs::symlink(outside.as_std_path(), from.join("link").as_std_path()).unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
let copied = to.join("link");
assert!(fs::symlink_metadata(copied.as_std_path()).unwrap().is_symlink());
assert_eq!(fs::read_link(copied.as_std_path()).unwrap(), outside.as_std_path());
}
#[cfg(windows)]
#[test]
fn a_relative_directory_symlink_is_recreated_as_a_directory_link() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8Path::from_path(temporary.path()).expect("utf8");
let from = root.join("from");
let to = root.join("to");
fs::create_dir_all(from.join("sub").as_std_path()).expect("sub");
fs::create_dir_all(from.join("shared").as_std_path()).expect("shared");
fs::write(from.join("shared/marker").as_std_path(), "present").expect("marker");
if let Err(cause) = std::os::windows::fs::symlink_dir(r"..\shared", from.join("sub/link").as_std_path()) {
if cause.kind() == ErrorKind::PermissionDenied {
return;
}
panic!("create source link: {cause}");
}
copy_tree(&from, &to, &to).expect("copy");
let copied = to.join("sub/link");
assert!(fs::symlink_metadata(copied.as_std_path()).expect("metadata").is_symlink());
assert!(copied.as_std_path().is_dir(), "the copied directory link is not traversable");
let through = fs::read_to_string(copied.join("marker").as_std_path()).expect("read through the copied link");
assert_eq!(through, "present");
}
#[cfg(unix)]
#[test]
fn a_symlink_cycle_does_not_hang_the_copy() {
let (_temporary, from, to) = tree();
fs::create_dir_all(from.join("a").as_std_path()).unwrap();
std::os::unix::fs::symlink(from.as_std_path(), from.join("a").join("loop").as_std_path()).unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert!(fs::symlink_metadata(to.join("a").join("loop").as_std_path()).unwrap().is_symlink());
}
#[test]
fn a_deep_tree_is_copied_whole() {
let (_temporary, from, to) = tree();
let mut deep = from.clone();
for _level in 0..80 {
deep = deep.join("d");
}
fs::create_dir_all(deep.as_std_path()).unwrap();
fs::write(deep.join("bottom.rs").as_std_path(), "fn f() {}").unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
let landed = to.join(deep.strip_prefix(&from).unwrap());
assert!(landed.join("bottom.rs").as_std_path().exists());
}
#[test]
fn an_empty_directory_is_preserved() {
let (_temporary, from, to) = tree();
fs::create_dir_all(from.join("empty").as_std_path()).unwrap();
copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap();
assert!(to.join("empty").as_std_path().is_dir());
}
#[test]
fn a_missing_source_tree_is_reported() {
let (_temporary, from, to) = tree();
let missing = from.join("absent");
let cause = copy_tree(&missing, &to, Utf8Path::new("/nowhere")).unwrap_err();
assert!(cause.to_string().contains("could not read the source tree"), "{cause}");
}
#[test]
fn a_destination_entry_that_cannot_be_replaced_is_reported() {
let (_temporary, from, to) = tree();
fs::write(from.join("file").as_std_path(), "source").unwrap();
fs::create_dir_all(to.join("file").as_std_path()).unwrap();
let cause = copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap_err();
assert!(cause.to_string().contains("could not copy"), "{cause}");
}
#[test]
fn only_the_first_of_several_concurrent_failures_is_kept() {
let (_temporary, from, to) = tree();
fs::create_dir_all(to.as_std_path()).unwrap();
for index in 0..32 {
let name = format!("blocked-{index}");
fs::create_dir_all(from.join(&name).as_std_path()).unwrap();
fs::write(from.join(&name).join("file").as_std_path(), "source").unwrap();
fs::create_dir_all(to.join(&name).join("file").as_std_path()).unwrap();
}
let cause = copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap_err();
assert!(cause.to_string().contains("could not copy"), "{cause}");
}
#[test]
fn a_second_recorded_failure_does_not_replace_the_first() {
let failure: Mutex<Option<Error>> = Mutex::new(None);
record(&failure, error!("first failure"));
record(&failure, error!("second failure"));
let held = failure.into_inner().unwrap();
let cause = held.expect("a failure was recorded");
assert!(cause.to_string().contains("first failure"), "{cause}");
}
#[test]
fn pruning_an_entry_without_a_file_name_is_not_a_match() {
assert!(!is_pruned(
Utf8Path::new("/workspace"),
Utf8Path::new(""),
Utf8Path::new("/elsewhere"),
));
}
#[test]
fn freshening_a_missing_file_is_harmless() {
let (_temporary, _from, to) = tree();
let file = to.join("copied");
fs::create_dir_all(to.as_std_path()).unwrap();
fs::write(file.as_std_path(), "x").unwrap();
freshen(&file);
fs::remove_file(file.as_std_path()).unwrap();
freshen(&file);
assert!(!file.as_std_path().exists());
}
#[cfg(unix)]
#[test]
fn a_non_utf8_source_entry_is_reported() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt as _;
let (_temporary, from, to) = tree();
let name = OsString::from_vec(b"bad-\xff".to_vec());
fs::write(from.as_std_path().join(name), "x").unwrap();
let cause = copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap_err();
assert!(cause.to_string().contains("not valid UTF-8"), "{cause}");
}
#[test]
fn a_destination_root_blocked_by_a_file_is_reported() {
let (_temporary, from, to) = tree();
fs::write(to.as_std_path(), "not a directory").unwrap();
let cause = copy_tree(&from, &to, Utf8Path::new("/nowhere")).unwrap_err();
assert!(cause.to_string().contains("could not create the scratch tree"), "{cause}");
}
#[test]
fn copying_an_entry_that_no_longer_exists_is_reported() {
let (_temporary, from, to) = tree();
let gone = from.join("was-here");
let cause = copy_entry(&gone, &to.join("was-here"), &Reflinks::isolated()).unwrap_err();
assert!(cause.to_string().contains("could not read"), "{cause}");
}
#[test]
fn copying_a_directory_blocked_by_a_file_is_reported() {
let (_temporary, from, to) = tree();
fs::create_dir_all(from.join("adir").as_std_path()).unwrap();
fs::create_dir_all(to.as_std_path()).unwrap();
fs::write(to.join("adir").as_std_path(), "blocking file").unwrap();
let cause = copy_entry(&from.join("adir"), &to.join("adir"), &Reflinks::isolated()).unwrap_err();
assert!(cause.to_string().contains("could not create"), "{cause}");
}
#[test]
fn a_files_parent_blocked_by_a_file_is_reported() {
let (_temporary, from, to) = tree();
fs::write(from.join("leaf").as_std_path(), "source").unwrap();
fs::create_dir_all(to.as_std_path()).unwrap();
fs::write(to.join("blocker").as_std_path(), "blocking file").unwrap();
let cause = copy_entry(&from.join("leaf"), &to.join("blocker").join("leaf"), &Reflinks::isolated()).unwrap_err();
assert!(cause.to_string().contains("could not create"), "{cause}");
}
#[test]
fn a_files_missing_parent_is_created_on_demand() {
let (_temporary, from, to) = tree();
fs::write(from.join("leaf").as_std_path(), "source").unwrap();
fs::create_dir_all(to.as_std_path()).unwrap();
copy_entry(&from.join("leaf"), &to.join("nested").join("leaf"), &Reflinks::isolated()).unwrap();
assert_eq!(fs::read_to_string(to.join("nested").join("leaf").as_std_path()).unwrap(), "source");
}
#[cfg(unix)]
#[test]
fn a_symlink_whose_target_can_no_longer_be_read_is_reported() {
let (_temporary, from, to) = tree();
let missing = from.join("not-a-link");
let cause = copy_symlink(&missing, &to.join("not-a-link")).unwrap_err();
assert!(cause.to_string().contains("could not read the link"), "{cause}");
}
#[cfg(unix)]
#[test]
fn recreating_a_symlink_over_an_existing_entry_is_reported() {
let (_temporary, from, to) = tree();
let target = from.join("target-file");
fs::write(target.as_std_path(), "x").unwrap();
std::os::unix::fs::symlink(target.as_std_path(), from.join("link").as_std_path()).unwrap();
fs::create_dir_all(to.as_std_path()).unwrap();
fs::write(to.join("link").as_std_path(), "already here").unwrap();
let cause = copy_symlink(&from.join("link"), &to.join("link")).unwrap_err();
assert!(cause.to_string().contains("could not recreate the link"), "{cause}");
}
#[test]
fn a_destination_that_cannot_clone_does_not_disable_cloning_for_another() {
let (_temporary, from, to) = tree();
let elsewhere = to.parent().expect("the fixture destination has a parent").join("elsewhere");
let unsupported = Reflinks::for_destination(&to);
let other = Reflinks::for_destination(&elsewhere);
unsupported.unsupported();
assert!(!unsupported.worth_trying(), "the failing destination must stop asking");
assert_eq!(
other.worth_trying(),
reflink_supported(),
"one destination's failure must not answer for another"
);
assert!(!Reflinks::for_destination(&to).worth_trying());
fs::write(from.join("file.rs").as_std_path(), "fn f() {}").unwrap();
copy_tree_with(&from, &to, Utf8Path::new("/nowhere"), CopyOptions::default(), &unsupported).unwrap();
copy_tree_with(&from, &elsewhere, Utf8Path::new("/nowhere"), CopyOptions::default(), &other).unwrap();
assert_eq!(fs::read_to_string(to.join("file.rs").as_std_path()).unwrap(), "fn f() {}");
assert_eq!(fs::read_to_string(elsewhere.join("file.rs").as_std_path()).unwrap(), "fn f() {}");
}
#[test]
fn an_isolated_capability_shares_nothing_with_the_registry_or_another_test() {
let (_temporary, _from, to) = tree();
let registered = Reflinks::for_destination(&to);
let mine = Reflinks::isolated();
let theirs = Reflinks::isolated();
mine.unsupported();
assert!(!mine.worth_trying(), "a test's own capability is its own to trip");
assert_eq!(theirs.worth_trying(), reflink_supported(), "another test must be unaffected");
assert_eq!(
registered.worth_trying(),
reflink_supported(),
"an isolated capability must not reach the shared registry"
);
registered.unsupported();
assert_eq!(theirs.worth_trying(), reflink_supported());
}
}