use super::fs_beneath::{create_file_at, entry_metadata_at, make_directory_at, open_directory_at, open_file_at, open_root, plain_components, read_link_at, set_times_at, symlink_at};
use super::fs_clone_exchange::clone_file;
use super::subvolume_index::StatFields;
use super::subvolume_index_files::{fields_of, install_index_in};
use super::worktree_clean::{StatEntry, metadata_differs_from_entry, stored_stats};
use super::worktree_snapshot_source::SnapshotSource;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
fn entry_names(path: &[u8]) -> Result<Vec<OsString>, String> {
let refused = || { return format!("the index path {} is not a plain path", String::from_utf8_lossy(path)); };
let names: Vec<OsString> = plain_components(Path::new(OsStr::from_bytes(path))).map_err(|_error| { return refused(); })?;
return if names.is_empty() || names.iter().any(|name: &OsString| { return name == ".git"; }) { Err(refused()) } else { Ok(names) };
}
fn times_of(from: &std::fs::Metadata) -> [libc::timespec; 2] {
return [
libc::timespec { tv_sec: from.atime() as libc::time_t, tv_nsec: from.atime_nsec() as libc::c_long },
libc::timespec { tv_sec: from.mtime() as libc::time_t, tv_nsec: from.mtime_nsec() as libc::c_long },
];
}
fn identity_of_metadata(from: &std::fs::Metadata) -> [i64; 8] {
return [from.dev() as i64, from.ino() as i64, from.size() as i64, from.mtime(), from.mtime_nsec(), from.ctime(), from.ctime_nsec(), i64::from(from.mode())];
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Created {
pub names: Vec<OsString>,
pub directory: bool,
}
type Directories = HashMap<Vec<OsString>, File>;
#[derive(Debug)]
pub struct Cloned {
created: Vec<Created>,
}
impl Cloned {
pub fn created(&self) -> &[Created] {
return self.created.as_slice();
}
}
pub fn leftover_report(created: &[Created], destination: &Path) -> String {
let shown: Vec<String> = created.iter().filter(|entry: &&Created| { return !entry.directory; }).take(3).map(|entry: &Created| { return entry.names.iter().map(|name: &OsString| { return name.to_string_lossy().into_owned(); }).collect::<Vec<String>>().join("/"); }).collect();
return format!("{} entries the clones created stay in {} (for example {}); the checkout that completes the worktree overwrites the tracked ones, and `git -C {} status` lists any other as untracked", created.len(), destination.display(), shown.join(", "), destination.display());
}
fn ensure_parents(directories: &mut Directories, names: &[OsString], created: &mut Vec<Created>) -> Result<File, String> {
let mut current: File = directories.get(&Vec::new()).ok_or_else(|| { return String::from("the destination is not held"); })?.try_clone().map_err(|error| { return format!("the destination handle cannot be kept ({error})"); })?;
for index in 0..names.len() - 1 {
let key: Vec<OsString> = names[..=index].to_vec();
if let Some(known) = directories.get(&key) {
current = known.try_clone().map_err(|error| { return format!("a directory handle cannot be kept ({error})"); })?;
continue;
}
let name: &OsString = &names[index];
let made: bool = make_directory_at(¤t, name.as_os_str(), 0o777).map_err(|error| { return format!("{} cannot be created ({error})", name.to_string_lossy()); })?;
let next: File = open_directory_at(¤t, name.as_os_str()).map_err(|error| { return format!("{} cannot be entered ({error})", name.to_string_lossy()); })?;
if super::fs_beneath::device_of(&next).ok() != super::fs_beneath::device_of(¤t).ok() {
return Err(format!("{} is on another device", name.to_string_lossy()));
}
if made {
created.push(Created { names: key.clone(), directory: true });
}
directories.insert(key, next.try_clone().map_err(|error| { return format!("a directory handle cannot be kept ({error})"); })?);
current = next;
}
return Ok(current);
}
struct Seams<'a> {
after_lookup: &'a mut dyn FnMut(),
after_clone: &'a mut dyn FnMut(),
before_index: &'a mut dyn FnMut(),
}
struct Work<'a> {
entry: &'a StatEntry,
names: &'a [OsString],
index_seconds: u32,
}
fn clone_entry(source_dir: &File, destination_dir: &File, work: &Work<'_>, results: (&mut Vec<Created>, &mut HashMap<Vec<u8>, StatFields>), seams: &mut Seams<'_>) -> Result<(), String> {
let (created, stats): (&mut Vec<Created>, &mut HashMap<Vec<u8>, StatFields>) = results;
let Work { entry, names: all_names, index_seconds }: &Work<'_> = work;
let name: &OsStr = all_names[all_names.len() - 1].as_os_str();
let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
let changed = |reason: String| { return format!("the source changed while it was cloned ({reason})"); };
let metadata: std::fs::Metadata = entry_metadata_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
if metadata.file_type().is_symlink() {
if let Some(reason) = metadata_differs_from_entry(entry, &metadata, *index_seconds) {
return Err(changed(reason));
}
let target: PathBuf = read_link_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
symlink_at(target.as_path(), destination_dir, name).map_err(|error| { return format!("{shown} cannot be linked ({error})"); })?;
created.push(Created { names: all_names.to_vec(), directory: false });
set_times_at(destination_dir, name, times_of(&metadata)).map_err(|error| { return format!("the times of {shown} cannot be set ({error})"); })?;
let after: std::fs::Metadata = entry_metadata_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
if identity_of_metadata(&after) != identity_of_metadata(&metadata) {
return Err(changed(format!("{shown} was replaced")));
}
return Ok(());
}
if !metadata.is_file() {
return Err(format!("{shown} is neither a file nor a link"));
}
(seams.after_lookup)();
let source: File = open_file_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
let held: std::fs::Metadata = source.metadata().map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
if let Some(reason) = metadata_differs_from_entry(entry, &held, *index_seconds) {
return Err(changed(reason));
}
let destination: File = create_file_at(destination_dir, name, 0o600).map_err(|error| { return format!("{shown} cannot be created ({error})"); })?;
created.push(Created { names: all_names.to_vec(), directory: false });
if held.len() > 0 {
clone_file(&source, &destination, Path::new(name)).map_err(|error| { return format!("the clone was refused ({error})"); })?;
}
(seams.after_clone)();
let after: std::fs::Metadata = source.metadata().map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
if identity_of_metadata(&after) != identity_of_metadata(&held) {
return Err(changed(format!("{shown} was written during the clone")));
}
destination.set_permissions(std::fs::Permissions::from_mode(held.mode() & 0o7777)).map_err(|error| { return format!("the mode of {shown} cannot be set ({error})"); })?;
let times: [libc::timespec; 2] = times_of(&held);
let result: libc::c_int = unsafe { libc::futimens(destination.as_raw_fd(), times.as_ptr()) };
if result != 0 {
return Err(format!("the times of {shown} cannot be set ({})", std::io::Error::last_os_error()));
}
let made: std::fs::Metadata = destination.metadata().map_err(|error| { return format!("{shown} cannot be examined ({error})"); })?;
stats.insert(entry.path.clone(), fields_of(&made));
return Ok(());
}
fn clone_all(source: &SnapshotSource, directories: &mut Directories, admin: &File, created: &mut Vec<Created>) -> Result<(), String> {
let mut seams: Seams<'_> = Seams { after_lookup: &mut || {}, after_clone: &mut || {}, before_index: &mut || {} };
return clone_with(source, directories, admin, created, &mut seams);
}
fn clone_with(source: &SnapshotSource, directories: &mut Directories, admin: &File, created: &mut Vec<Created>, seams: &mut Seams<'_>) -> Result<(), String> {
let entries: Vec<StatEntry> = stored_stats(source.clean.index_bytes.as_slice(), source.clean.hash_length).ok_or_else(|| { return String::from("the source's index changed shape"); })?;
let root: File = open_root(source.root.as_path()).map_err(|error| { return format!("the source's root cannot be opened ({error})"); })?;
let mut stats: HashMap<Vec<u8>, StatFields> = HashMap::new();
for entry in &entries {
let names: Vec<OsString> = entry_names(entry.path.as_slice())?;
let source_dir: File = super::fs_beneath::open_directory_beneath(&root, &names[..names.len() - 1]).map_err(|error| { return format!("the source directory of {} cannot be opened ({error})", String::from_utf8_lossy(entry.path.as_slice())); })?;
let destination_dir: File = ensure_parents(directories, names.as_slice(), created)?;
let work: Work<'_> = Work { entry, names: names.as_slice(), index_seconds: source.clean.index_seconds };
clone_entry(&source_dir, &destination_dir, &work, (&mut *created, &mut stats), seams)?;
}
(seams.before_index)();
let report: String = install_index_in(admin, source.clean.index_bytes.as_slice(), &mut |path: &[u8]| { return stats.get(path).copied(); }, source.clean.hash_length)?;
debug_report(report.as_str());
return Ok(());
}
fn debug_report(report: &str) {
super::diagnostic_log::debug("cloneTrackedFiles", report);
}
pub fn clone_tracked_files(source: &SnapshotSource, destination: &File, admin: &File) -> Result<Cloned, String> {
let mut directories: Directories = HashMap::new();
directories.insert(Vec::new(), destination.try_clone().map_err(|error| { return format!("the destination handle cannot be kept ({error})"); })?);
let mut created: Vec<Created> = Vec::new();
if let Err(reason) = clone_all(source, &mut directories, admin, &mut created) {
return Err(if created.is_empty() { reason } else { format!("{reason}; {} entries it created stay", created.len()) });
}
return Ok(Cloned { created });
}
#[cfg(test)]
#[path = "worktree_clone_tests.rs"]
mod tests;