1use crate::add_timing::step as timed;
19use crate::fs_beneath::{ObjectId, exchange_at, identity_at, open_directory_at, open_directory_beneath, open_file_at, open_root, plain_components, remove_at, rename_at};
20use crate::fs_btrfs_ioctl::snapshot_create_at;
21use crate::subvolume_index_files::install_index_in;
22use crate::subvolume_remove::remove_tree;
23use crate::worktree_add_plan::AddPlan;
24use crate::subvolume_index::StatFields;
25use crate::worktree_clean::{CleanRefusal, StatCursor, StatEntry, stored_stats, tree_fields_beneath};
26use crate::worktree_snapshot_source::SnapshotSource;
27use crate::worktree_trash::{Pruned, TrashManifest, prune_snapshot};
28use std::ffi::{OsStr, OsString};
29use std::fs::File;
30use std::os::unix::fs::PermissionsExt;
31use std::path::Path;
32
33fn remove_stub(root: &File, source_root: &Path, path: &Path, pointer: bool) -> Result<(), String> {
47 let Ok(relative) = path.strip_prefix(source_root) else { return Ok(()) };
48 let shown: String = path.display().to_string();
49 let names: Vec<OsString> = plain_components(relative).map_err(|error| { return format!("{shown} is not a plain path inside the source ({error})"); })?;
50 let Some((name, parents)) = names.split_last() else { return Ok(()) };
51 let gone = |error: &std::io::Error| { return error.kind() == std::io::ErrorKind::NotFound; };
52 let parent: File = match open_directory_beneath(root, parents) {
53 Ok(found) => found,
54 Err(error) if gone(&error) => return Ok(()),
55 Err(error) => return Err(format!("{shown} is reached through a link or another mount in the snapshot ({error})")),
56 };
57 if pointer {
58 match open_directory_at(&parent, name.as_os_str()) {
59 Ok(stub) => match remove_at(&stub, OsStr::new(".git"), false) {
60 Ok(()) => {}
61 Err(error) if gone(&error) => {}
62 Err(error) => return Err(format!("{shown}/.git cannot be removed from the snapshot ({error})")),
63 },
64 Err(error) if gone(&error) => return Ok(()),
65 Err(error) => return Err(format!("{shown} cannot be entered in the snapshot ({error})")),
66 }
67 }
68 return match remove_at(&parent, name.as_os_str(), true) {
69 Ok(()) => Ok(()),
70 Err(error) if gone(&error) => Ok(()),
71 Err(error) => Err(format!("{shown} cannot be removed from the snapshot as an empty directory ({error})")),
72 };
73}
74
75fn remove_stubs(stage: &Path, source: &SnapshotSource, plan: &AddPlan) -> Result<(), String> {
88 let root: File = open_root(stage).map_err(|error| { return format!("the stage cannot be opened ({error})"); })?;
89 for nested in &source.nested {
90 remove_stub(&root, source.root.as_path(), nested.as_path(), false)?;
91 }
92 return remove_stub(&root, source.root.as_path(), plan.destination.as_path(), true);
93}
94
95fn replace_git_pointer(stage: &Path, pointer: &[u8]) -> Result<(), String> {
106 let path = stage.join(".git");
107 remove_tree(path.as_path()).map_err(|error| { return format!("the source's .git cannot be removed from the snapshot ({error})"); })?;
108 return std::fs::write(path.as_path(), pointer).map_err(|error| { return format!("the .git pointer cannot be written ({error})"); });
109}
110
111fn install_index(source: &SnapshotSource, stage: &Path, admin: &File, own_index: Option<&[u8]>) -> Result<(), String> {
123 let root: File = open_root(stage).map_err(|error| { return format!("the stage cannot be opened ({error})"); })?;
124 let bytes: &[u8] = own_index.unwrap_or(source.clean.index_bytes.as_slice());
127 let entries: Vec<StatEntry> = if own_index.is_none() { stored_stats(bytes, source.clean.hash_length).ok_or_else(|| { return String::from("the source's index changed shape"); })? } else { Vec::new() };
128 let walked: Vec<StatFields> = timed("walkSnapshot", || { return tree_fields_beneath(&root, entries.as_slice(), source.clean.index_seconds); })
131 .map_err(|refusal: CleanRefusal| { return format!("the source changed while it was copied ({})", refusal.reason); })?;
132 let known: std::collections::HashMap<&[u8], StatFields> = entries.iter().map(|entry: &StatEntry| { return entry.path.as_slice(); }).zip(walked).collect();
133 let mut cursor: StatCursor<'_> = StatCursor::new(&root);
135 let report: String = timed("installIndexFile", || { return install_index_in(admin, bytes, &mut |path: &[u8]| { return known.get(path).copied().or_else(|| { return cursor.fields(path); }); }, source.clean.hash_length); })?;
136 crate::diagnostic_log::debug("takeSnapshot", report.as_str());
137 return Ok(());
138}
139
140fn read_own_index(stage: &Path) -> Result<Vec<u8>, String> {
151 let root: File = open_root(stage).map_err(|error| { return format!("the stage cannot be opened ({error})"); })?;
152 let git_dir: File = open_directory_at(&root, OsStr::new(".git")).map_err(|error| { return format!("the snapshot has no .git directory ({error})"); })?;
153 let mut index: File = open_file_at(&git_dir, OsStr::new("index")).map_err(|error| { return format!("the snapshot has no index ({error})"); })?;
154 let mut bytes: Vec<u8> = Vec::new();
155 std::io::Read::read_to_end(&mut index, &mut bytes).map_err(|error| { return format!("the snapshot's index is unreadable ({error})"); })?;
156 return Ok(bytes);
157}
158
159pub fn take_snapshot(source: &SnapshotSource, plan: &AddPlan, stage: &Path, admin: &File, pointer: &[u8]) -> Result<Snapshotted, SnapshotFailure> {
169 timed("snapshotIoctl", || { return snapshot_create_at(source.root.as_path(), stage); }).map_err(|error| { return SnapshotFailure { reason: format!("the snapshot was refused ({error})"), stage: None }; })?;
170 let made: Option<ObjectId> = open_root(stage.parent().unwrap_or(stage)).ok().and_then(|parent: File| { return identity_at(&parent, stage.file_name()?).ok(); });
172 let failed = |reason: String| -> SnapshotFailure { return SnapshotFailure { reason, stage: made }; };
173 std::fs::set_permissions(stage, std::fs::Permissions::from_mode(0o700)).map_err(|error| { return failed(format!("the stage cannot be made private ({error})")); })?;
174 let own_index: Option<Vec<u8>> = if source.dirty.is_some() { Some(timed("readOwnIndex", || { return read_own_index(stage); }).map_err(failed)?) } else { None };
176 let pruned: Pruned = timed("prune", || { return prune_snapshot(stage, source); }).map_err(failed)?;
177 timed("removeStubs", || { return remove_stubs(stage, source, plan); }).map_err(failed)?;
178 timed("replacePointer", || { return replace_git_pointer(stage, pointer); }).map_err(failed)?;
179 timed("installIndex", || { return install_index(source, stage, admin, own_index.as_deref()); }).map_err(failed)?;
180 let stage_id: ObjectId = made.ok_or_else(|| { return failed(String::from("the stage cannot be examined")); })?;
181 return Ok(Snapshotted { stage: stage_id, trash: pruned.trash });
182}
183
184#[derive(Debug, Eq, PartialEq)]
186pub struct Snapshotted {
187 pub stage: ObjectId,
189 pub trash: Option<TrashManifest>,
192}
193
194#[derive(Debug, Eq, PartialEq)]
196pub struct SnapshotFailure {
197 pub reason: String,
199 pub stage: Option<ObjectId>,
201}
202
203#[derive(Debug, Eq, PartialEq)]
205pub struct PlaceError {
206 pub reason: String,
208 pub stage_is_ours: bool,
211}
212
213pub struct PlaceSeams<'a> {
215 pub after_check: &'a mut dyn FnMut(),
217 pub before_swap_back: &'a mut dyn FnMut(),
219}
220
221pub fn put_in_place(stage: &Path, destination: &Path, source_mode: u32, expected: ObjectId) -> Result<(), PlaceError> {
234 return put_in_place_with(stage, destination, source_mode, expected, &mut PlaceSeams { after_check: &mut || {}, before_swap_back: &mut || {} });
235}
236
237pub fn put_in_place_with(stage: &Path, destination: &Path, source_mode: u32, expected: ObjectId, seams: &mut PlaceSeams<'_>) -> Result<(), PlaceError> {
239 let ours = |reason: String| -> PlaceError { return PlaceError { reason, stage_is_ours: true }; };
240 let (Some(parent_path), Some(stage_name), Some(destination_name)) = (stage.parent(), stage.file_name(), destination.file_name()) else {
241 return Err(ours(String::from("the stage and the destination have no parent")));
242 };
243 if destination.parent() != Some(parent_path) {
244 return Err(ours(String::from("the stage is not beside the destination")));
245 }
246 let parent: File = open_root(parent_path).map_err(|error| { return ours(format!("the parent of the destination cannot be opened ({error})")); })?;
247 let snapshot: ObjectId = identity_at(&parent, stage_name).map_err(|error| { return ours(format!("the stage cannot be examined ({error})")); })?;
248 let is_directory = |name: &OsStr| -> bool { return crate::fs_beneath::entry_metadata_at(&parent, name).is_ok_and(|found: std::fs::Metadata| { return found.is_dir(); }); };
249 if !is_directory(destination_name) || identity_at(&parent, destination_name).ok() != Some(expected) {
250 return Err(ours(format!("{} is not the directory Git made", destination.display())));
251 }
252 std::fs::set_permissions(stage, std::fs::Permissions::from_mode(source_mode)).map_err(|error| { return ours(format!("the stage cannot take the source's mode ({error})")); })?;
253 (seams.after_check)();
254 exchange_at(&parent, stage_name, destination_name).map_err(|error| { return ours(format!("the destination cannot be swapped with the stage ({error})")); })?;
255 if identity_at(&parent, stage_name).ok() == Some(expected) {
257 return Ok(());
258 }
259 (seams.before_swap_back)();
260 if let Err(error) = exchange_at(&parent, stage_name, destination_name) {
261 return Err(PlaceError { reason: format!("{} changed while it was swapped and the swap could not be undone ({error})", destination.display()), stage_is_ours: false });
262 }
263 let restored: bool = identity_at(&parent, stage_name).ok() == Some(snapshot);
265 return Err(PlaceError { reason: format!("{} changed while it was swapped; the swap was undone", destination.display()), stage_is_ours: restored });
266}
267
268pub fn settle_displaced(stage: &Path, destination: &Path, expected: ObjectId, pointer: &[u8]) -> Vec<String> {
280 let (Some(parent_path), Some(stage_name), Some(destination_name)) = (stage.parent(), stage.file_name(), destination.file_name()) else {
281 return vec![format!("{} has no parent", stage.display())];
282 };
283 let mut notes: Vec<String> = Vec::new();
284 let opened = (|| -> std::io::Result<(File, File, File)> {
285 let parent: File = open_root(parent_path)?;
286 let displaced: File = open_directory_at(&parent, stage_name)?;
287 let placed: File = open_directory_at(&parent, destination_name)?;
288 return Ok((parent, displaced, placed));
289 })();
290 let Ok((parent, displaced, placed)) = opened else { return vec![format!("{} could not be opened to be settled", stage.display())] };
291 if crate::fs_beneath::identity_of(&displaced).ok() != Some(expected) {
292 return vec![format!("{} is not the directory Git made and stays", stage.display())];
293 }
294 let listing: Vec<OsString> = match std::fs::read_dir(crate::fs_beneath::handle_path(&displaced)) {
295 Ok(found) => found.flatten().map(|entry| { return entry.file_name(); }).collect(),
296 Err(error) => return vec![format!("{} cannot be listed ({error})", stage.display())],
297 };
298 for name in listing {
299 if name == ".git" {
300 let same: bool = open_file_at(&displaced, name.as_os_str()).ok().is_some_and(|mut file: File| {
301 let mut found: Vec<u8> = Vec::new();
302 return std::io::Read::read_to_end(&mut file, &mut found).is_ok() && found == pointer;
303 });
304 if same {
305 if let Err(error) = remove_at(&displaced, name.as_os_str(), false) {
306 notes.push(format!("Git's pointer in {} cannot be removed ({error})", stage.display()));
307 }
308 } else {
309 notes.push(format!(".git in {} is not the pointer Git wrote and stays", stage.display()));
310 }
311 continue;
312 }
313 if let Err(error) = rename_at(&displaced, name.as_os_str(), &placed, name.as_os_str(), false) {
314 notes.push(format!("{} was added to {} while the worktree was made and stays in {} ({error})", name.to_string_lossy(), destination.display(), stage.display()));
315 }
316 }
317 if let Err(error) = remove_at(&parent, stage_name, true) {
318 notes.push(format!("{} is not removed ({error})", stage.display()));
319 }
320 return notes;
321}
322
323#[cfg(test)]
325#[path = "worktree_snapshot_tests.rs"]
326mod tests;