use crate::add_timing::{begin_step, end_step};
use 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};
use crate::fs_btrfs_ioctl::snapshot_create_at;
use crate::subvolume_index_files::install_index_in;
use crate::subvolume_remove::remove_tree;
use crate::worktree_add_plan::AddPlan;
use crate::worktree_path_failure_message::subject_failure;
use crate::subvolume_index::{StatFields, StatLookup};
use crate::worktree_clean::{StatCursor, StatEntry, stored_stats, tree_fields_beneath};
use crate::worktree_snapshot_source::SnapshotSource;
use crate::worktree_trash::{Pruned, TrashManifest, prune_snapshot};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn is_gone(error: &std::io::Error) -> bool {
return error.kind() == std::io::ErrorKind::NotFound;
}
fn do_nothing() {}
fn ours_on_failure<T>(outcome: Result<T, String>) -> Result<T, PlaceError> {
return match outcome {
Ok(value) => Ok(value),
Err(reason) => Err(ours(reason)),
};
}
fn ours(reason: String) -> PlaceError {
return PlaceError { reason, stage_is_ours: true };
}
fn failing<T>(outcome: Result<T, String>, made: Option<ObjectId>) -> Result<T, SnapshotFailure> {
return match outcome {
Ok(value) => Ok(value),
Err(reason) => Err(SnapshotFailure { reason, stage: made }),
};
}
fn identity_of_name(path: &Path) -> Option<ObjectId> {
let parent: File = open_root(path.parent().unwrap_or(path)).ok()?;
return identity_at(&parent, path.file_name()?).ok();
}
fn is_directory_at(parent: &File, name: &OsStr) -> bool {
return match crate::fs_beneath::entry_metadata_at(parent, name) {
Ok(found) => found.is_dir(),
Err(_unreadable) => false,
};
}
fn open_for_settling(parent_path: &Path, stage_name: &OsStr, destination_name: &OsStr) -> std::io::Result<(File, File, File)> {
let parent: File = open_root(parent_path)?;
let displaced: File = open_directory_at(&parent, stage_name)?;
let placed: File = open_directory_at(&parent, destination_name)?;
return Ok((parent, displaced, placed));
}
fn file_holds(directory: &File, name: &OsStr, expected: &[u8]) -> bool {
let Ok(mut file) = open_file_at(directory, name) else { return false };
let mut found: Vec<u8> = Vec::new();
return std::io::Read::read_to_end(&mut file, &mut found).is_ok() && found == expected;
}
fn remove_stub(root: &File, source_root: &Path, path: &Path, pointer: bool) -> Result<(), String> {
let Ok(relative) = path.strip_prefix(source_root) else { return Ok(()) };
let shown: String = path.display().to_string();
let names: Vec<OsString> = subject_failure(plain_components(relative), &shown, "is not a plain path inside the source")?;
let Some((name, parents)) = names.split_last() else { return Ok(()) };
let parent: File = match open_directory_beneath(root, parents) {
Ok(found) => found,
Err(error) if is_gone(&error) => return Ok(()),
Err(error) => return Err(format!("{shown} is reached through a link or another mount in the snapshot ({error})")),
};
if pointer {
match open_directory_at(&parent, name.as_os_str()) {
Ok(stub) => match remove_at(&stub, OsStr::new(".git"), false) {
Ok(()) => {}
Err(error) if is_gone(&error) => {}
Err(error) => return Err(format!("{shown}/.git cannot be removed from the snapshot ({error})")),
},
Err(error) if is_gone(&error) => return Ok(()),
Err(error) => return Err(format!("{shown} cannot be entered in the snapshot ({error})")),
}
}
return match remove_at(&parent, name.as_os_str(), true) {
Ok(()) => Ok(()),
Err(error) if is_gone(&error) => Ok(()),
Err(error) => Err(format!("{shown} cannot be removed from the snapshot as an empty directory ({error})")),
};
}
fn remove_stubs(stage: &Path, source: &SnapshotSource, plan: &AddPlan) -> Result<(), String> {
let root: File = subject_failure(open_root(stage), &"the stage", "cannot be opened")?;
for nested in &source.nested {
remove_stub(&root, source.root.as_path(), nested.as_path(), false)?;
}
return remove_stub(&root, source.root.as_path(), plan.destination.as_path(), true);
}
fn replace_git_pointer(stage: &Path, pointer: &[u8]) -> Result<(), String> {
let path = stage.join(".git");
subject_failure(remove_tree(path.as_path()), &"the source's .git", "cannot be removed from the snapshot")?;
return subject_failure(std::fs::write(path.as_path(), pointer), &"the .git pointer", "cannot be written");
}
fn install_index(source: &SnapshotSource, stage: &Path, admin: &File, own_index: Option<&[u8]>) -> Result<(), String> {
let root: File = subject_failure(open_root(stage), &"the stage", "cannot be opened")?;
let bytes: &[u8] = own_index.unwrap_or(source.clean.index_bytes.as_slice());
let entries: Vec<StatEntry> = if own_index.is_none() {
let Some(stats) = stored_stats(bytes, source.clean.hash_length) else {
return Err(String::from("the source's index changed shape"));
};
stats
} else {
Vec::new()
};
let walk_snapshot_entered: bool = begin_step("walkSnapshot");
let walk_snapshot_outcome = tree_fields_beneath(&root, entries.as_slice(), source.clean.index_seconds);
end_step(walk_snapshot_entered);
let walked: Vec<StatFields> = match walk_snapshot_outcome {
Ok(fields) => fields,
Err(refusal) => return Err(format!("the source changed while it was copied ({})", refusal.reason)),
};
let mut known: std::collections::HashMap<&[u8], StatFields> = std::collections::HashMap::new();
for (entry, fields) in entries.iter().zip(walked) {
known.insert(entry.path.as_slice(), fields);
}
let mut cursor: StatCursor<'_> = StatCursor::new(&root);
let install_index_file_entered: bool = begin_step("installIndexFile");
let install_index_file_outcome = install_index_in(admin, bytes, &mut WalkedThenCursor { known: &known, cursor: &mut cursor }, source.clean.hash_length);
end_step(install_index_file_entered);
let report: String = install_index_file_outcome?;
crate::diagnostic_log::debug("takeSnapshot", report.as_str());
return Ok(());
}
struct WalkedThenCursor<'a, 'root> {
known: &'a std::collections::HashMap<&'a [u8], StatFields>,
cursor: &'a mut StatCursor<'root>,
}
impl StatLookup for WalkedThenCursor<'_, '_> {
fn stat_now(&mut self, path: &[u8]) -> Option<StatFields> {
return match self.known.get(path) {
Some(fields) => Some(*fields),
None => self.cursor.fields(path),
};
}
}
fn read_own_index(stage: &Path) -> Result<Vec<u8>, String> {
let root: File = subject_failure(open_root(stage), &"the stage", "cannot be opened")?;
let git_dir: File = subject_failure(open_directory_at(&root, OsStr::new(".git")), &"the snapshot", "has no .git directory")?;
let mut index: File = subject_failure(open_file_at(&git_dir, OsStr::new("index")), &"the snapshot", "has no index")?;
let mut bytes: Vec<u8> = Vec::new();
subject_failure(std::io::Read::read_to_end(&mut index, &mut bytes), &"the snapshot's index", "is unreadable")?;
return Ok(bytes);
}
pub fn take_snapshot(source: &SnapshotSource, plan: &AddPlan, stage: &Path, admin: &File, pointer: &[u8]) -> Result<Snapshotted, SnapshotFailure> {
let snapshot_ioctl_entered: bool = begin_step("snapshotIoctl");
let snapshot_ioctl_outcome = snapshot_create_at(source.root.as_path(), stage);
end_step(snapshot_ioctl_entered);
if let Err(error) = snapshot_ioctl_outcome {
return Err(SnapshotFailure { reason: format!("the snapshot was refused ({error})"), stage: None });
}
let made: Option<ObjectId> = identity_of_name(stage);
failing(subject_failure(std::fs::set_permissions(stage, std::fs::Permissions::from_mode(0o700)), &"the stage", "cannot be made private"), made)?;
let own_index: Option<Vec<u8>> = if source.dirty.is_some() {
let read_own_index_entered: bool = begin_step("readOwnIndex");
let read_own_index_outcome = read_own_index(stage);
end_step(read_own_index_entered);
Some(failing(read_own_index_outcome, made)?)
} else {
None
};
let prune_entered: bool = begin_step("prune");
let prune_outcome = prune_snapshot(stage, source);
end_step(prune_entered);
let pruned: Pruned = failing(prune_outcome, made)?;
let remove_stubs_entered: bool = begin_step("removeStubs");
let remove_stubs_outcome = remove_stubs(stage, source, plan);
end_step(remove_stubs_entered);
failing(remove_stubs_outcome, made)?;
let replace_pointer_entered: bool = begin_step("replacePointer");
let replace_pointer_outcome = replace_git_pointer(stage, pointer);
end_step(replace_pointer_entered);
failing(replace_pointer_outcome, made)?;
let install_index_entered: bool = begin_step("installIndex");
let install_index_outcome = install_index(source, stage, admin, own_index.as_deref());
end_step(install_index_entered);
failing(install_index_outcome, made)?;
let Some(stage_id) = made else {
return Err(SnapshotFailure { reason: String::from("the stage cannot be examined"), stage: None });
};
return Ok(Snapshotted { stage: stage_id, trash: pruned.trash });
}
#[derive(Debug, Eq, PartialEq)]
pub struct Snapshotted {
pub stage: ObjectId,
pub trash: Option<TrashManifest>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct SnapshotFailure {
pub reason: String,
pub stage: Option<ObjectId>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct PlaceError {
pub reason: String,
pub stage_is_ours: bool,
}
pub struct PlaceSeams<'a> {
pub after_check: &'a mut dyn FnMut(),
pub before_swap_back: &'a mut dyn FnMut(),
}
pub fn put_in_place(stage: &Path, destination: &Path, source_mode: u32, expected: ObjectId) -> Result<(), PlaceError> {
return put_in_place_with(stage, destination, source_mode, expected, &mut PlaceSeams { after_check: &mut do_nothing, before_swap_back: &mut do_nothing });
}
pub fn put_in_place_with(stage: &Path, destination: &Path, source_mode: u32, expected: ObjectId, seams: &mut PlaceSeams<'_>) -> Result<(), PlaceError> {
let (Some(parent_path), Some(stage_name), Some(destination_name)) = (stage.parent(), stage.file_name(), destination.file_name()) else {
return Err(ours(String::from("the stage and the destination have no parent")));
};
if destination.parent() != Some(parent_path) {
return Err(ours(String::from("the stage is not beside the destination")));
}
let parent: File = ours_on_failure(subject_failure(open_root(parent_path), &"the parent of the destination", "cannot be opened"))?;
let snapshot: ObjectId = ours_on_failure(subject_failure(identity_at(&parent, stage_name), &"the stage", "cannot be examined"))?;
if !is_directory_at(&parent, destination_name) || identity_at(&parent, destination_name).ok() != Some(expected) {
return Err(ours(format!("{} is not the directory Git made", destination.display())));
}
ours_on_failure(subject_failure(std::fs::set_permissions(stage, std::fs::Permissions::from_mode(source_mode)), &"the stage", "cannot take the source's mode"))?;
(seams.after_check)();
ours_on_failure(subject_failure(exchange_at(&parent, stage_name, destination_name), &"the destination", "cannot be swapped with the stage"))?;
if identity_at(&parent, stage_name).ok() == Some(expected) {
return Ok(());
}
(seams.before_swap_back)();
if let Err(error) = exchange_at(&parent, stage_name, destination_name) {
return Err(PlaceError { reason: format!("{} changed while it was swapped and the swap could not be undone ({error})", destination.display()), stage_is_ours: false });
}
let restored: bool = identity_at(&parent, stage_name).ok() == Some(snapshot);
return Err(PlaceError { reason: format!("{} changed while it was swapped; the swap was undone", destination.display()), stage_is_ours: restored });
}
pub fn settle_displaced(stage: &Path, destination: &Path, expected: ObjectId, pointer: &[u8]) -> Vec<String> {
let (Some(parent_path), Some(stage_name), Some(destination_name)) = (stage.parent(), stage.file_name(), destination.file_name()) else {
return vec![format!("{} has no parent", stage.display())];
};
let mut notes: Vec<String> = Vec::new();
let opened = open_for_settling(parent_path, stage_name, destination_name);
let Ok((parent, displaced, placed)) = opened else { return vec![format!("{} could not be opened to be settled", stage.display())] };
if crate::fs_beneath::identity_of(&displaced).ok() != Some(expected) {
return vec![format!("{} is not the directory Git made and stays", stage.display())];
}
let listing: Vec<OsString> = match std::fs::read_dir(crate::fs_beneath::handle_path(&displaced)) {
Ok(found) => {
let mut names: Vec<OsString> = Vec::new();
for entry in found.flatten() {
names.push(entry.file_name());
}
names
}
Err(error) => return vec![format!("{} cannot be listed ({error})", stage.display())],
};
for name in listing {
if name == ".git" {
let same: bool = file_holds(&displaced, name.as_os_str(), pointer);
if same {
if let Err(error) = remove_at(&displaced, name.as_os_str(), false) {
notes.push(format!("Git's pointer in {} cannot be removed ({error})", stage.display()));
}
} else {
notes.push(format!(".git in {} is not the pointer Git wrote and stays", stage.display()));
}
continue;
}
if let Err(error) = rename_at(&displaced, name.as_os_str(), &placed, name.as_os_str(), false) {
notes.push(format!("{} was added to {} while the worktree was made and stays in {} ({error})", name.to_string_lossy(), destination.display(), stage.display()));
}
}
if let Err(error) = remove_at(&parent, stage_name, true) {
notes.push(format!("{} is not removed ({error})", stage.display()));
}
return notes;
}
#[cfg(test)]
#[path = "worktree_snapshot_tests.rs"]
mod tests;