Skip to main content

git_policy_cli/worktree/
worktree_convert_remove.rs

1//! What:
2//!  Remove the old tree of a conversion, but only the directory the journal recorded, pinned by a descriptor from the moment
3//!  its identity is checked until its last entry is gone.
4//! Why:
5//!  A device and inode read from a path say nothing about what the path names a moment later: another process can rename the
6//!  recorded directory away and put an unrelated one at the name before a recursive delete resolves it again. The directory
7//!  is opened below its parent without following a link, its identity is read from that descriptor, its entries are removed
8//!  through the descriptor's own path, and the last step removes the name only while it still names the same object and only
9//!  if that object is empty (a directory removal cannot take a tree).
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const outcome = await removeRecordedOldTree({ platform, parent, name, oldRoot, installed }); // 'removed' | 'absent' | { kept: reason } | { differs: reason }
14//! ```
15
16use crate::fs_beneath::{ObjectId, handle_path, identity_at, identity_of, open_directory_at, remove_at};
17use crate::subvolume_error::MigrateError;
18use crate::subvolume_platform::Platform;
19use crate::subvolume_remove::remove_tree;
20use crate::worktree_convert_compare::first_difference;
21use crate::worktree_convert_journal::is_recorded_object;
22use std::ffi::OsStr;
23use std::os::unix::fs::PermissionsExt;
24use std::path::{Path, PathBuf};
25
26/// What a request to remove the old tree did.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum Removal {
29    /// The recorded directory was removed.
30    Removed,
31    /// Nothing was at the name.
32    Absent,
33    /// The name holds something else, or the directory moved; nothing was deleted, for this reason.
34    Kept(String),
35    /// The old tree holds something the installed tree does not hold in the same form, or something this command does not
36    ///  delete; nothing was deleted or copied, for this reason.
37    Differs(String),
38}
39
40/// What:
41///  Remove the directory `name` of `parent` if it is the old root, deleting beneath a descriptor on it.
42/// Why:
43///   See the module description.
44///
45/// In TS you'd write (pseudocode):
46/// ```ts
47/// removeRecordedOldTree(platform, parent, name, oldRoot, installed);
48/// ```
49pub fn remove_recorded_old_tree(platform: &dyn Platform, parent: &std::fs::File, name: &OsStr, old: ObjectId, installed: &Path) -> Result<Removal, MigrateError> {
50    return remove_recorded_old_tree_seamed(platform, parent, name, old, installed, &mut || {});
51}
52
53/// `remove_recorded_old_tree` with a call right after the directory was pinned and judged to be the old tree, where a control
54///  moves it away and puts another directory at the name.
55pub fn remove_recorded_old_tree_seamed(platform: &dyn Platform, parent: &std::fs::File, name: &OsStr, old: ObjectId, installed: &Path, after_pinned: &mut dyn FnMut()) -> Result<Removal, MigrateError> {
56    let shown: PathBuf = PathBuf::from(name);
57    let pinned: std::fs::File = match open_directory_at(parent, name) {
58        Ok(opened) => opened,
59        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Removal::Absent),
60        Err(error) => return Ok(Removal::Kept(format!("{} cannot be opened as a directory below its parent ({error}), so it is not the old tree this conversion recorded", shown.display()))),
61    };
62    let found = identity_of(&pinned).map_err(|error: std::io::Error| { return MigrateError::io("examine", shown.as_path(), &error); })?;
63    if !is_recorded_object(&old, &found) {
64        return Ok(Removal::Kept(format!("{} is not the old tree this conversion recorded", shown.display())));
65    }
66    let through: PathBuf = handle_path(&pinned);
67    if platform.is_subvolume_root(through.as_path())? {
68        return Ok(Removal::Kept(format!("{} is a subvolume, so it is not the old tree this conversion recorded", shown.display())));
69    }
70    // Nothing is deleted unless the installed tree holds all of the old one; a difference is reported and settled by a person.
71    match first_difference(through.as_path(), installed) {
72        Ok(None) => {}
73        Ok(Some(reason)) => return Ok(Removal::Differs(reason)),
74        Err(error) => return Ok(Removal::Differs(format!("the old tree cannot be compared with the installed worktree ({error})"))),
75    }
76    let metadata: std::fs::Metadata = pinned.metadata().map_err(|error: std::io::Error| { return MigrateError::io("examine", shown.as_path(), &error); })?;
77    if std::os::unix::fs::MetadataExt::mode(&metadata) & 0o700 != 0o700 {
78        pinned.set_permissions(std::fs::Permissions::from_mode(std::os::unix::fs::MetadataExt::mode(&metadata) | 0o700)).map_err(|error: std::io::Error| { return MigrateError::io("make writable", shown.as_path(), &error); })?;
79    }
80    after_pinned();
81    let listing = std::fs::read_dir(&through).map_err(|error: std::io::Error| { return MigrateError::io("list the old tree", shown.as_path(), &error); })?;
82    for item in listing {
83        let entry: std::fs::DirEntry = item.map_err(|error: std::io::Error| { return MigrateError::io("list the old tree", shown.as_path(), &error); })?;
84        remove_tree(entry.path().as_path()).map_err(|error: std::io::Error| { return MigrateError::io("remove the old tree", entry.path().as_path(), &error); })?;
85    }
86    match identity_at(parent, name) {
87        Ok(still) if is_recorded_object(&old, &still) => {}
88        Ok(_) => return Ok(Removal::Kept(format!("{} no longer names the old tree, which is empty and was left where it is", shown.display()))),
89        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Removal::Removed),
90        Err(error) => return Err(MigrateError::io("examine", shown.as_path(), &error)),
91    }
92    remove_at(parent, name, true).map_err(|error: std::io::Error| { return MigrateError::io("remove the old tree", shown.as_path(), &error); })?;
93    return Ok(Removal::Removed);
94}