1use crate::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};
23use crate::fs_clone_exchange::clone_file;
24use crate::subvolume_index::StatFields;
25use crate::subvolume_index_files::{fields_of, install_index_in};
26use crate::worktree_clean::{StatEntry, metadata_differs_from_entry, stored_stats};
27use crate::worktree_snapshot_source::SnapshotSource;
28use std::collections::HashMap;
29use std::ffi::{OsStr, OsString};
30use std::fs::File;
31use std::os::fd::AsRawFd;
32use std::os::unix::ffi::OsStrExt;
33use std::os::unix::fs::{MetadataExt, PermissionsExt};
34use std::path::{Path, PathBuf};
35
36fn entry_names(path: &[u8]) -> Result<Vec<OsString>, String> {
46 let refused = || { return format!("the index path {} is not a plain path", String::from_utf8_lossy(path)); };
47 let names: Vec<OsString> = plain_components(Path::new(OsStr::from_bytes(path))).map_err(|_error| { return refused(); })?;
48 return if names.is_empty() || names.iter().any(|name: &OsString| { return name == ".git"; }) { Err(refused()) } else { Ok(names) };
49}
50
51fn times_of(from: &std::fs::Metadata) -> [libc::timespec; 2] {
61 return [
62 libc::timespec { tv_sec: from.atime() as libc::time_t, tv_nsec: from.atime_nsec() as libc::c_long },
63 libc::timespec { tv_sec: from.mtime() as libc::time_t, tv_nsec: from.mtime_nsec() as libc::c_long },
64 ];
65}
66
67fn identity_of_metadata(from: &std::fs::Metadata) -> [i64; 8] {
77 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())];
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct Created {
92 pub names: Vec<OsString>,
94 pub directory: bool,
96}
97
98type Directories = HashMap<Vec<OsString>, File>;
101
102#[derive(Debug)]
113pub struct Cloned {
114 created: Vec<Created>,
116}
117
118impl Cloned {
119 pub fn created(&self) -> &[Created] {
121 return self.created.as_slice();
122 }
123}
124
125pub fn leftover_report(created: &[Created], destination: &Path) -> String {
136 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();
137 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());
138}
139
140fn ensure_parents(directories: &mut Directories, names: &[OsString], created: &mut Vec<Created>) -> Result<File, String> {
152 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})"); })?;
153 for index in 0..names.len() - 1 {
154 let key: Vec<OsString> = names[..=index].to_vec();
155 if let Some(known) = directories.get(&key) {
156 current = known.try_clone().map_err(|error| { return format!("a directory handle cannot be kept ({error})"); })?;
157 continue;
158 }
159 let name: &OsString = &names[index];
160 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()); })?;
161 let next: File = open_directory_at(¤t, name.as_os_str()).map_err(|error| { return format!("{} cannot be entered ({error})", name.to_string_lossy()); })?;
162 if crate::fs_beneath::device_of(&next).ok() != crate::fs_beneath::device_of(¤t).ok() {
163 return Err(format!("{} is on another device", name.to_string_lossy()));
164 }
165 if made {
166 created.push(Created { names: key.clone(), directory: true });
167 }
168 directories.insert(key, next.try_clone().map_err(|error| { return format!("a directory handle cannot be kept ({error})"); })?);
169 current = next;
170 }
171 return Ok(current);
172}
173
174struct Seams<'a> {
185 after_lookup: &'a mut dyn FnMut(),
187 after_clone: &'a mut dyn FnMut(),
189 before_index: &'a mut dyn FnMut(),
191}
192
193struct Work<'a> {
195 entry: &'a StatEntry,
197 names: &'a [OsString],
199 index_seconds: u32,
201}
202
203fn clone_entry(source_dir: &File, destination_dir: &File, work: &Work<'_>, results: (&mut Vec<Created>, &mut HashMap<Vec<u8>, StatFields>), seams: &mut Seams<'_>) -> Result<(), String> {
217 let (created, stats): (&mut Vec<Created>, &mut HashMap<Vec<u8>, StatFields>) = results;
218 let Work { entry, names: all_names, index_seconds }: &Work<'_> = work;
219 let name: &OsStr = all_names[all_names.len() - 1].as_os_str();
220 let shown: String = String::from_utf8_lossy(entry.path.as_slice()).into_owned();
221 let changed = |reason: String| { return format!("the source changed while it was cloned ({reason})"); };
222 let metadata: std::fs::Metadata = entry_metadata_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
223 if metadata.file_type().is_symlink() {
224 if let Some(reason) = metadata_differs_from_entry(entry, &metadata, *index_seconds) {
225 return Err(changed(reason));
226 }
227 let target: PathBuf = read_link_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
228 symlink_at(target.as_path(), destination_dir, name).map_err(|error| { return format!("{shown} cannot be linked ({error})"); })?;
229 created.push(Created { names: all_names.to_vec(), directory: false });
230 set_times_at(destination_dir, name, times_of(&metadata)).map_err(|error| { return format!("the times of {shown} cannot be set ({error})"); })?;
231 let after: std::fs::Metadata = entry_metadata_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
232 if identity_of_metadata(&after) != identity_of_metadata(&metadata) {
233 return Err(changed(format!("{shown} was replaced")));
234 }
235 return Ok(());
238 }
239 if !metadata.is_file() {
240 return Err(format!("{shown} is neither a file nor a link"));
241 }
242 (seams.after_lookup)();
243 let source: File = open_file_at(source_dir, name).map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
244 let held: std::fs::Metadata = source.metadata().map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
245 if let Some(reason) = metadata_differs_from_entry(entry, &held, *index_seconds) {
246 return Err(changed(reason));
247 }
248 let destination: File = create_file_at(destination_dir, name, 0o600).map_err(|error| { return format!("{shown} cannot be created ({error})"); })?;
249 created.push(Created { names: all_names.to_vec(), directory: false });
250 if held.len() > 0 {
251 clone_file(&source, &destination, Path::new(name)).map_err(|error| { return format!("the clone was refused ({error})"); })?;
252 }
253 (seams.after_clone)();
254 let after: std::fs::Metadata = source.metadata().map_err(|error| { return format!("{shown} is unreadable ({error})"); })?;
255 if identity_of_metadata(&after) != identity_of_metadata(&held) {
256 return Err(changed(format!("{shown} was written during the clone")));
257 }
258 destination.set_permissions(std::fs::Permissions::from_mode(held.mode() & 0o7777)).map_err(|error| { return format!("the mode of {shown} cannot be set ({error})"); })?;
259 let times: [libc::timespec; 2] = times_of(&held);
260 let result: libc::c_int = unsafe { libc::futimens(destination.as_raw_fd(), times.as_ptr()) };
262 if result != 0 {
263 return Err(format!("the times of {shown} cannot be set ({})", std::io::Error::last_os_error()));
264 }
265 let made: std::fs::Metadata = destination.metadata().map_err(|error| { return format!("{shown} cannot be examined ({error})"); })?;
267 stats.insert(entry.path.clone(), fields_of(&made));
268 return Ok(());
269}
270
271fn clone_all(source: &SnapshotSource, directories: &mut Directories, admin: &File, created: &mut Vec<Created>) -> Result<(), String> {
273 let mut seams: Seams<'_> = Seams { after_lookup: &mut || {}, after_clone: &mut || {}, before_index: &mut || {} };
274 return clone_with(source, directories, admin, created, &mut seams);
275}
276
277fn clone_with(source: &SnapshotSource, directories: &mut Directories, admin: &File, created: &mut Vec<Created>, seams: &mut Seams<'_>) -> Result<(), String> {
279 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"); })?;
280 let root: File = open_root(source.root.as_path()).map_err(|error| { return format!("the source's root cannot be opened ({error})"); })?;
281 let mut stats: HashMap<Vec<u8>, StatFields> = HashMap::new();
282 for entry in &entries {
283 let names: Vec<OsString> = entry_names(entry.path.as_slice())?;
284 let source_dir: File = crate::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())); })?;
285 let destination_dir: File = ensure_parents(directories, names.as_slice(), created)?;
286 let work: Work<'_> = Work { entry, names: names.as_slice(), index_seconds: source.clean.index_seconds };
287 clone_entry(&source_dir, &destination_dir, &work, (&mut *created, &mut stats), seams)?;
288 }
289 (seams.before_index)();
290 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)?;
291 debug_report(report.as_str());
292 return Ok(());
293}
294
295fn debug_report(report: &str) {
297 crate::diagnostic_log::debug("cloneTrackedFiles", report);
298}
299
300pub fn clone_tracked_files(source: &SnapshotSource, destination: &File, admin: &File) -> Result<Cloned, String> {
313 let mut directories: Directories = HashMap::new();
314 directories.insert(Vec::new(), destination.try_clone().map_err(|error| { return format!("the destination handle cannot be kept ({error})"); })?);
315 let mut created: Vec<Created> = Vec::new();
316 if let Err(reason) = clone_all(source, &mut directories, admin, &mut created) {
317 return Err(if created.is_empty() { reason } else { format!("{reason}; {} entries it created stay", created.len()) });
318 }
319 return Ok(Cloned { created });
320}
321
322#[cfg(test)]
324#[path = "worktree_clone_tests.rs"]
325mod tests;