Support durable host-to-isolated workspace migration
This commit is contained in:
parent
db9a561d0c
commit
0dda65fc2e
3 changed files with 1176 additions and 32 deletions
|
|
@ -11,7 +11,14 @@ impl IgnoredWorkspace {
|
|||
let path = self.child_path(parent, name)?;
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
let entry = self.resolve(&snapshot, &path)?;
|
||||
self.root(entry.backing).unlink(&path, directory)
|
||||
self.root(entry.backing).unlink(&path, directory)?;
|
||||
if entry.backing == Classification::Host {
|
||||
self.provenance
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove_tree(&path);
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
reply_result(result, reply);
|
||||
}
|
||||
|
|
@ -66,7 +73,9 @@ pub(super) struct ResolvedEntry {
|
|||
pub(super) struct OpenFile {
|
||||
pub(super) file: File,
|
||||
pub(super) backing: Classification,
|
||||
pub(super) path: PathBuf,
|
||||
pub(super) access_mode: i32,
|
||||
pub(super) migration_quiesced: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
use std::io::Write as _;
|
||||
|
||||
use super::nodes::{
|
||||
DirectoryStream, OpenFile, ResolvedEntry, bsd_lock_operation, errno, fuse_lock, record_lock,
|
||||
reply_result, reply_xattr, timestamp, unsupported_lock_flags, writable_xattr,
|
||||
};
|
||||
use super::{
|
||||
Arc, AsFd, BsdFileFlags, CopyFileRangeFlags, DirectoryEntry, Errno, FallocateFlags, FcntlArg,
|
||||
FileAttr, FileExt, FileHandle, FileType, Filesystem, FopenFlags, FuseRenameFlags, Generation,
|
||||
Gid, INodeNo, IgnoredWorkspace, InitFlags, KernelConfig, LockFlags, LockOwner, Mode, OFlags,
|
||||
OpenFlags, Ordering, OsStr, OsString, Path, RenameFlags, ReplyAttr, ReplyCreate, ReplyData,
|
||||
ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyLock, ReplyLseek, ReplyOpen, ReplyStatfs,
|
||||
ReplyWrite, ReplyXattr, Request, SeekFrom, SystemTime, TTL, TimeOrNow, Timestamps, Uid,
|
||||
WriteFlags, XattrFlags, bsd_flock, copy_file_range, fallocate, fchmod, fchown, fcntl,
|
||||
fgetxattr, flistxattr, fremovexattr, fsetxattr, fstat, fstatvfs, ftruncate, futimens, io,
|
||||
mknodat, open_flags, seek,
|
||||
Arc, AsFd, BsdFileFlags, Classification, CopyFileRangeFlags, DirectoryEntry, Errno,
|
||||
FallocateFlags, FcntlArg, FileAttr, FileExt, FileHandle, FileType, Filesystem, FopenFlags,
|
||||
FuseRenameFlags, Generation, Gid, INodeNo, IgnoredWorkspace, InitFlags, KernelConfig,
|
||||
LockFlags, LockOwner, Mode, OFlags, OpenFlags, Ordering, OsStr, OsString, Path, RenameFlags,
|
||||
ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyLock,
|
||||
ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, SeekFrom, SystemTime, TTL,
|
||||
TimeOrNow, Timestamps, Uid, WriteFlags, XattrFlags, bsd_flock, copy_file_range, fallocate,
|
||||
fchmod, fchown, fcntl, fgetxattr, flistxattr, fremovexattr, fsetxattr, fstat, fstatvfs,
|
||||
ftruncate, futimens, io, mknodat, open_flags, seek,
|
||||
};
|
||||
|
||||
impl Filesystem for IgnoredWorkspace {
|
||||
|
|
@ -31,6 +33,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
}
|
||||
|
||||
fn lookup(&self, _request: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
match self.child_path(parent, name) {
|
||||
Ok(path) => self.reply_entry(&path, reply),
|
||||
Err(error) => reply.error(errno(&error)),
|
||||
|
|
@ -44,6 +50,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
handle: Option<FileHandle>,
|
||||
reply: ReplyAttr,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = if let Some(handle) = handle {
|
||||
self.with_file(handle, |open| {
|
||||
let stat = fstat(open.file.as_fd())?;
|
||||
|
|
@ -85,7 +95,12 @@ impl Filesystem for IgnoredWorkspace {
|
|||
_flags: Option<BsdFileFlags>,
|
||||
reply: ReplyAttr,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let operation = |open: &OpenFile| -> io::Result<FileAttr> {
|
||||
open.migration_quiesced.store(false, Ordering::Release);
|
||||
if let Some(mode) = mode {
|
||||
fchmod(open.file.as_fd(), Mode::from_raw_mode(mode & 0o7777))?;
|
||||
}
|
||||
|
|
@ -108,6 +123,7 @@ impl Filesystem for IgnoredWorkspace {
|
|||
},
|
||||
)?;
|
||||
}
|
||||
self.record_changed(open.backing, &open.path);
|
||||
let path = self.path_for(inode)?;
|
||||
self.attributes(&ResolvedEntry {
|
||||
path,
|
||||
|
|
@ -178,6 +194,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
}
|
||||
|
||||
fn readlink(&self, _request: &Request, inode: INodeNo, reply: ReplyData) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = self.path_for(inode).and_then(|path| {
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
let entry = self.resolve(&snapshot, &path)?;
|
||||
|
|
@ -199,6 +219,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
rdev: u32,
|
||||
reply: ReplyEntry,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if matches!(mode & libc::S_IFMT, libc::S_IFCHR | libc::S_IFBLK) {
|
||||
reply.error(Errno::EPERM);
|
||||
return;
|
||||
|
|
@ -217,6 +241,7 @@ impl Filesystem for IgnoredWorkspace {
|
|||
Mode::from_raw_mode(mode & !umask),
|
||||
rustix::fs::makedev(rdev >> 8, rdev & 0xff),
|
||||
)?;
|
||||
self.record_created(backing, &path)?;
|
||||
Ok(path)
|
||||
})();
|
||||
match result {
|
||||
|
|
@ -234,12 +259,17 @@ impl Filesystem for IgnoredWorkspace {
|
|||
umask: u32,
|
||||
reply: ReplyEntry,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = (|| {
|
||||
let path = self.child_path(parent, name)?;
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
let backing = self.create_backing(&snapshot, &path, true)?;
|
||||
self.root(backing)
|
||||
.mkdir(&path, Mode::from_raw_mode((mode & !umask) & 0o7777))?;
|
||||
self.record_created(backing, &path)?;
|
||||
Ok(path)
|
||||
})();
|
||||
match result {
|
||||
|
|
@ -249,10 +279,18 @@ impl Filesystem for IgnoredWorkspace {
|
|||
}
|
||||
|
||||
fn unlink(&self, _request: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
self.remove(parent, name, false, reply);
|
||||
}
|
||||
|
||||
fn rmdir(&self, _request: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
self.remove(parent, name, true, reply);
|
||||
}
|
||||
|
||||
|
|
@ -264,11 +302,16 @@ impl Filesystem for IgnoredWorkspace {
|
|||
target: &Path,
|
||||
reply: ReplyEntry,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = (|| {
|
||||
let path = self.child_path(parent, name)?;
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
let backing = self.create_backing(&snapshot, &path, false)?;
|
||||
self.root(backing).symlink(target, &path)?;
|
||||
self.record_created(backing, &path)?;
|
||||
Ok(path)
|
||||
})();
|
||||
match result {
|
||||
|
|
@ -287,6 +330,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
flags: FuseRenameFlags,
|
||||
reply: ReplyEmpty,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if flags.contains(FuseRenameFlags::RENAME_WHITEOUT) {
|
||||
reply.error(Errno::EOPNOTSUPP);
|
||||
return;
|
||||
|
|
@ -302,14 +349,64 @@ impl Filesystem for IgnoredWorkspace {
|
|||
source_entry.stat.st_mode & libc::S_IFMT == libc::S_IFDIR,
|
||||
);
|
||||
if source_entry.backing != destination_backing {
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
let supported_flags =
|
||||
flags.is_empty() || flags == FuseRenameFlags::RENAME_NOREPLACE;
|
||||
let directory = source_entry.stat.st_mode & libc::S_IFMT == libc::S_IFDIR;
|
||||
if source_entry.backing != Classification::Host
|
||||
|| destination_backing != Classification::Isolated
|
||||
{
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
if !supported_flags {
|
||||
let _ = writeln!(
|
||||
io::stderr().lock(),
|
||||
"lazaret ignored-workspace refused host-to-isolated migration: unsupported rename flags"
|
||||
);
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
if !directory {
|
||||
let _ = writeln!(
|
||||
io::stderr().lock(),
|
||||
"lazaret ignored-workspace refused host-to-isolated migration: source is not a directory"
|
||||
);
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
if self.wait_for_unquiesced_descendants(&source) {
|
||||
let _ = writeln!(
|
||||
io::stderr().lock(),
|
||||
"lazaret ignored-workspace refused host-to-isolated migration: source has an open descendant"
|
||||
);
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
let mut provenance = self
|
||||
.provenance
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
crate::migration::migrate(
|
||||
&self.host,
|
||||
&self.isolated,
|
||||
&provenance,
|
||||
&source,
|
||||
&destination,
|
||||
)?;
|
||||
provenance.remove_tree(&source);
|
||||
return Ok(());
|
||||
}
|
||||
if destination == Path::new(".git/index") {
|
||||
self.require_index_publication_safe(source_entry.backing, &source, &snapshot)?;
|
||||
}
|
||||
let flags = RenameFlags::from_bits_retain(flags.bits());
|
||||
self.root(source_entry.backing)
|
||||
.rename(&source, &destination, flags)
|
||||
.rename(&source, &destination, flags)?;
|
||||
if source_entry.backing == Classification::Host {
|
||||
let mut provenance = self
|
||||
.provenance
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
provenance.remove_tree(&destination);
|
||||
provenance.rename_tree(&source, &destination);
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
reply_result(result, reply);
|
||||
}
|
||||
|
|
@ -322,6 +419,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
new_name: &OsStr,
|
||||
reply: ReplyEntry,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = (|| {
|
||||
let source = self.path_for(inode)?;
|
||||
let destination = self.child_path(new_parent, new_name)?;
|
||||
|
|
@ -342,6 +443,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
}
|
||||
|
||||
fn open(&self, _request: &Request, inode: INodeNo, flags: OpenFlags, reply: ReplyOpen) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
match self.open_file(inode, flags.0) {
|
||||
Ok(handle) => reply.opened(FileHandle(handle), FopenFlags::empty()),
|
||||
Err(error) => reply.error(errno(&error)),
|
||||
|
|
@ -359,6 +464,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
_lock_owner: Option<LockOwner>,
|
||||
reply: ReplyData,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = self.with_file(handle, |open| {
|
||||
let mut bytes = vec![0; size as usize];
|
||||
let read = open.file.read_at(&mut bytes, offset)?;
|
||||
|
|
@ -384,12 +493,15 @@ impl Filesystem for IgnoredWorkspace {
|
|||
reply: ReplyWrite,
|
||||
) {
|
||||
let result = self.with_file(handle, |open| {
|
||||
open.migration_quiesced.store(false, Ordering::Release);
|
||||
let offset = if flags.0 & libc::O_APPEND != 0 {
|
||||
open.file.metadata()?.len()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
open.file.write_at(data, offset)
|
||||
let written = open.file.write_at(data, offset)?;
|
||||
self.record_changed(open.backing, &open.path);
|
||||
Ok(written)
|
||||
});
|
||||
match result {
|
||||
Ok(written) => reply.written(u32::try_from(written).unwrap_or(u32::MAX)),
|
||||
|
|
@ -407,7 +519,9 @@ impl Filesystem for IgnoredWorkspace {
|
|||
) {
|
||||
let result = self.with_file(handle, |open| {
|
||||
self.release_posix_locks(open, owner)?;
|
||||
open.file.sync_data()
|
||||
open.file.sync_data()?;
|
||||
open.migration_quiesced.store(true, Ordering::Release);
|
||||
Ok(())
|
||||
});
|
||||
reply_result(result, reply);
|
||||
}
|
||||
|
|
@ -528,6 +642,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
}
|
||||
|
||||
fn opendir(&self, _request: &Request, inode: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = (|| {
|
||||
let path = self.path_for(inode)?;
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
|
|
@ -671,6 +789,10 @@ impl Filesystem for IgnoredWorkspace {
|
|||
position: u32,
|
||||
reply: ReplyEmpty,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if position != 0 || !writable_xattr(name) {
|
||||
reply.error(Errno::ENOTSUP);
|
||||
return;
|
||||
|
|
@ -683,6 +805,14 @@ impl Filesystem for IgnoredWorkspace {
|
|||
XattrFlags::from_bits_retain(u32::from_ne_bytes(flags.to_ne_bytes())),
|
||||
)?)
|
||||
});
|
||||
if result.is_ok()
|
||||
&& let Ok(path) = self.path_for(inode)
|
||||
{
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
if let Ok(entry) = self.resolve(&snapshot, &path) {
|
||||
self.record_changed(entry.backing, &path);
|
||||
}
|
||||
}
|
||||
reply_result(result, reply);
|
||||
}
|
||||
|
||||
|
|
@ -714,11 +844,23 @@ impl Filesystem for IgnoredWorkspace {
|
|||
}
|
||||
|
||||
fn removexattr(&self, _request: &Request, inode: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if !writable_xattr(name) {
|
||||
reply.error(Errno::ENOTSUP);
|
||||
return;
|
||||
}
|
||||
let result = self.with_temporary_file(inode, |file| Ok(fremovexattr(file.as_fd(), name)?));
|
||||
if result.is_ok()
|
||||
&& let Ok(path) = self.path_for(inode)
|
||||
{
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
if let Ok(entry) = self.resolve(&snapshot, &path) {
|
||||
self.record_changed(entry.backing, &path);
|
||||
}
|
||||
}
|
||||
reply_result(result, reply);
|
||||
}
|
||||
|
||||
|
|
@ -732,18 +874,28 @@ impl Filesystem for IgnoredWorkspace {
|
|||
flags: i32,
|
||||
reply: ReplyCreate,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = (|| {
|
||||
let path = self.child_path(parent, name)?;
|
||||
let snapshot = self.classifier.snapshot_for(&path);
|
||||
let backing = self.create_backing(&snapshot, &path, false)?;
|
||||
let existed = self.root(backing).stat(&path).is_ok();
|
||||
let file = self.root(backing).open_entry(
|
||||
&path,
|
||||
open_flags(flags) | OFlags::CREATE,
|
||||
Mode::from_raw_mode((mode & !umask) & 0o7777),
|
||||
)?;
|
||||
let stat = fstat(file.as_fd())?;
|
||||
if existed {
|
||||
self.record_changed(backing, &path);
|
||||
} else {
|
||||
self.record_created(backing, &path)?;
|
||||
}
|
||||
let attributes = self.attributes(&ResolvedEntry {
|
||||
path,
|
||||
path: path.clone(),
|
||||
backing,
|
||||
stat,
|
||||
})?;
|
||||
|
|
@ -756,7 +908,11 @@ impl Filesystem for IgnoredWorkspace {
|
|||
Arc::new(OpenFile {
|
||||
file,
|
||||
backing,
|
||||
path,
|
||||
access_mode: flags & libc::O_ACCMODE,
|
||||
migration_quiesced: std::sync::atomic::AtomicBool::new(
|
||||
flags & libc::O_ACCMODE == libc::O_RDONLY,
|
||||
),
|
||||
}),
|
||||
);
|
||||
Ok((attributes, handle))
|
||||
|
|
@ -783,13 +939,20 @@ impl Filesystem for IgnoredWorkspace {
|
|||
mode: i32,
|
||||
reply: ReplyEmpty,
|
||||
) {
|
||||
let _namespace = self
|
||||
.namespace
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let result = self.with_file(handle, |open| {
|
||||
Ok(fallocate(
|
||||
open.migration_quiesced.store(false, Ordering::Release);
|
||||
fallocate(
|
||||
open.file.as_fd(),
|
||||
FallocateFlags::from_bits_retain(u32::from_ne_bytes(mode.to_ne_bytes())),
|
||||
offset,
|
||||
length,
|
||||
)?)
|
||||
)?;
|
||||
self.record_changed(open.backing, &open.path);
|
||||
Ok(())
|
||||
});
|
||||
reply_result(result, reply);
|
||||
}
|
||||
|
|
@ -853,6 +1016,7 @@ impl Filesystem for IgnoredWorkspace {
|
|||
let output = files
|
||||
.get(&handle_out.0)
|
||||
.ok_or_else(|| io::Error::from_raw_os_error(libc::EBADF))?;
|
||||
output.migration_quiesced.store(false, Ordering::Release);
|
||||
if input.backing != output.backing {
|
||||
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ fn posix_lock_owners_use_distinct_backing_lock_domains() {
|
|||
.open(path)
|
||||
.unwrap(),
|
||||
backing: Classification::Host,
|
||||
path: std::path::PathBuf::from("ordinary"),
|
||||
access_mode: libc::O_RDWR,
|
||||
migration_quiesced: std::sync::atomic::AtomicBool::new(false),
|
||||
};
|
||||
let first_owner = LockOwner(101);
|
||||
let second_owner = LockOwner(202);
|
||||
|
|
@ -218,6 +220,50 @@ fn physical_router_preserves_routes_transitions_and_cross_route_atomicity() {
|
|||
session.umount_and_join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a usable host FUSE device"]
|
||||
fn physical_router_migrates_cargo_target_only_toward_isolated_state() {
|
||||
let fixture = Fixture::new();
|
||||
repository(&fixture.workspace);
|
||||
fs::write(fixture.workspace.join(".gitignore"), b"target/\n").unwrap();
|
||||
|
||||
let filesystem = IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
|
||||
let mut config = Config::default();
|
||||
config.mount_options.extend([
|
||||
MountOption::RW,
|
||||
MountOption::DefaultPermissions,
|
||||
MountOption::NoDev,
|
||||
MountOption::NoSuid,
|
||||
]);
|
||||
let session = fuser::spawn_mount2(filesystem, &fixture.mountpoint, &config).unwrap();
|
||||
|
||||
let temporary = fixture.mountpoint.join("targetAbCd12");
|
||||
fs::create_dir(&temporary).unwrap();
|
||||
fs::write(
|
||||
temporary.join("CACHEDIR.TAG"),
|
||||
b"Signature: 8a477f597d28d172789f06886806bc55\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::rename(&temporary, fixture.mountpoint.join("target")).unwrap();
|
||||
|
||||
assert!(!fixture.workspace.join("targetAbCd12").exists());
|
||||
assert!(!fixture.workspace.join("target").exists());
|
||||
assert_eq!(
|
||||
fs::read(fixture.isolated.join("target/CACHEDIR.TAG")).unwrap(),
|
||||
b"Signature: 8a477f597d28d172789f06886806bc55\n"
|
||||
);
|
||||
|
||||
let error = fs::rename(
|
||||
fixture.mountpoint.join("target"),
|
||||
fixture.mountpoint.join("exported-target"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
|
||||
assert!(!fixture.workspace.join("exported-target").exists());
|
||||
|
||||
session.umount_and_join().unwrap();
|
||||
}
|
||||
|
||||
fn repository(root: &std::path::Path) {
|
||||
fs::create_dir_all(root.join(".git/info")).unwrap();
|
||||
fs::create_dir_all(root.join(".git/hooks")).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue