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