Skip to main content

git_policy_cli/subvolume/
subvolume_platform.rs

1//! What:
2//!  The operating-system operations the subvolume migration needs, behind one trait, and the Linux implementation
3//!  that calls the primitives (`fs_kind`, `fs_btrfs_ioctl`, `fs_clone_exchange`, `fs_mount_options`).
4//! Why:
5//!  The migration's journal, resume, rollback and refusal logic is the same on any filesystem; only five operations
6//!  need btrfs. With the operations behind a trait, the controls run the whole logic on a plain directory tree with a
7//!  stand-in platform where there is no btrfs (the container gate), and run it against the real platform on btrfs
8//!  (the host task). `doc/planning/cli-git-btrfs-worktree.md` is the design the primitives come from.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! interface Platform { filesystemKind(path): Kind; isSubvolumeRoot(path): boolean; createSubvolume(path): void; ... }
13//! ```
14
15use crate::fs_btrfs_ioctl::{subvolume_create_at, subvolume_destroy_at};
16use crate::fs_clone_exchange::{clone_file, exchange_paths};
17use crate::fs_kind::{FilesystemKind, filesystem_facts, is_subvolume_root};
18use crate::fs_mount_options::read_mount_facts;
19use crate::fs_primitive_error::PrimitiveError;
20use crate::git_system_config::with_real_git;
21use crate::worktree_clean_content::ConversionEnvironment;
22use std::ffi::OsString;
23use std::fs::File;
24use std::path::Path;
25
26/// What:
27///  The operations of the migration that depend on the kind of filesystem.
28///  `&self` borrows the platform read-only so one platform serves a whole run; `dyn Platform` is how a caller holds
29///  "any implementation" (TS interface).
30/// Why:
31///   See the module description.
32///
33/// In TS you'd write (pseudocode):
34/// ```ts
35/// interface Platform { ... }
36/// ```
37pub trait Platform {
38    /// The filesystem kind holding `path` and the bytes free there.
39    fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError>;
40    /// Whether `path` is the root of a subvolume.
41    fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError>;
42    /// Create an empty subvolume at `path`.
43    fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError>;
44    /// Destroy the subvolume at `path` with the kernel call (the caller falls back to emptying and `rmdir`).
45    fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError>;
46    /// Whether an unprivileged subvolume destroy is allowed on the mount holding `path`.
47    fn removal_allowed(&self, path: &Path) -> bool;
48    /// Whether `path` is itself a mount point (a mount point cannot be exchanged).
49    fn is_mount_point(&self, path: &Path) -> bool;
50    /// Clone a whole file's data blocks.
51    fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError>;
52    /// Swap two paths atomically.
53    fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError>;
54    /// Make `new` another name of the file `existing`; fails with `EXDEV` where the two sit in different subvolumes.
55    fn hard_link(&self, existing: &Path, new: &Path) -> std::io::Result<()> {
56        return std::fs::hard_link(existing, new);
57    }
58    /// Which configuration files and system attributes file Git reads here. The default names no Git and no variables, so the
59    ///  system files are unknown and a content decision that needs them refuses; a stand-in overrides it with what a control
60    ///  needs, and the real platform answers for the Git it forwards to.
61    fn conversion_environment(&self) -> ConversionEnvironment {
62        return ConversionEnvironment::from_variables(&[]);
63    }
64}
65
66/// What:
67///  The real platform: every operation is the primitive of the same name.
68/// Why:
69///   Production code and the host controls use it.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// const linuxPlatform: Platform = { filesystemKind, isSubvolumeRoot, ... };
74/// ```
75#[derive(Clone, Debug, Default)]
76pub struct LinuxPlatform {
77    /// The environment of the Git this platform's command forwards to, with the forward-target variable naming that Git, so the
78    ///  facts about Git's files are learned from the Git that reads them and never from the wrapper's own environment, which
79    ///  does not carry the variable; empty where no Git is known.
80    pub git_environment: Vec<(OsString, OsString)>,
81}
82
83impl LinuxPlatform {
84    /// What:
85    ///  The platform of a command that forwards to the real Git `real_git`, from the environment the command was started in.
86    /// Why:
87    ///   The wrapper puts the forward-target variable only into its children's environment, so the command names the Git it
88    ///  resolved itself; without one (`None`) the environment is left as it is and the system files stay unknown.
89    ///
90    /// In TS you'd write (pseudocode):
91    /// ```ts
92    /// const platform = linuxPlatform({ ...environment, [FORWARD_TARGET]: realGit });
93    /// ```
94    pub fn forwarding_to(environment: &[(OsString, OsString)], real_git: Option<&Path>) -> LinuxPlatform {
95        let named: Vec<(OsString, OsString)> = real_git.map_or_else(|| { return environment.to_vec(); }, |found: &Path| { return with_real_git(environment, found); });
96        return LinuxPlatform { git_environment: named };
97    }
98}
99
100/// The primitives, called as they are.
101impl Platform for LinuxPlatform {
102    fn conversion_environment(&self) -> ConversionEnvironment {
103        return ConversionEnvironment::from_variables(self.git_environment.as_slice());
104    }
105
106    fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError> {
107        return filesystem_facts(path);
108    }
109
110    fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError> {
111        return is_subvolume_root(path);
112    }
113
114    fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
115        return subvolume_create_at(path);
116    }
117
118    fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
119        return subvolume_destroy_at(path);
120    }
121
122    fn removal_allowed(&self, path: &Path) -> bool {
123        return read_mount_facts(path)
124            .ok()
125            .flatten()
126            .is_some_and(|facts| { return facts.has_option("user_subvol_rm_allowed"); });
127    }
128
129    fn is_mount_point(&self, path: &Path) -> bool {
130        let Ok(real) = std::fs::canonicalize(path) else { return false };
131        return read_mount_facts(real.as_path())
132            .ok()
133            .flatten()
134            .is_some_and(|facts| { return facts.mount_point == real; });
135    }
136
137    fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError> {
138        return clone_file(source, destination, path);
139    }
140
141    fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError> {
142        return exchange_paths(first, second);
143    }
144}
145
146/// Platform controls stay out of the release executable.
147#[cfg(test)]
148#[path = "subvolume_platform_tests.rs"]
149mod tests;