Skip to main content

git_policy_cli/subvolume/
subvolume_error.rs

1//! What:
2//!  The failure type of the subvolume migration and the test hook that simulates a kill.
3//! Why:
4//!  Every step names what it did to which path and why it stopped, so the report can tell the person the cause, the
5//!  state the journal holds, and the way forward. The interrupt hook stops a run at a named point exactly as a kill
6//!  would (all state is on disk or in the journal; nothing else survives a kill), so the resume controls can stop at
7//!  every journal step without killing the test process. Release code passes `Interrupt::never()`.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! class MigrateError extends Error {}
12//! const interrupt = { point: undefined as string | undefined, after: 0 }; // tests only
13//! ```
14
15use crate::fs_primitive_error::PrimitiveError;
16use std::fmt;
17use std::path::PathBuf;
18use std::sync::atomic::{AtomicU64, Ordering};
19
20/// What:
21///  Why a migration step stopped.
22///  `String` is owned text; `PathBuf` is an owned path.
23/// Why:
24///   Each variant has its own wording and its own remedy in the report.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// type MigrateError = { kind: 'io'; ... } | { kind: 'primitive'; ... } | { kind: 'unreadable-directory'; ... } | ...;
29/// ```
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum MigrateError {
32    /// An ordinary file operation failed.
33    Io {
34        /// What was being done, in words.
35        action: String,
36        /// The path involved.
37        path: PathBuf,
38        /// The operating system's words.
39        reason: String,
40    },
41    /// A kernel primitive failed (ioctl, clone, exchange).
42    Primitive(PrimitiveError),
43    /// A directory this user cannot list or search; the migration stops instead of leaving its subtree out.
44    UnreadableDirectory {
45        /// The directory.
46        path: PathBuf,
47        /// The operating system's words.
48        reason: String,
49    },
50    /// The journal file is unreadable or has an unknown shape.
51    Journal(String),
52    /// The run stopped at the named test point.
53    Interrupted(String),
54}
55
56impl MigrateError {
57    /// What:
58    ///  An I/O failure with the action and path, from a `std::io::Error`.
59    /// Why:
60    ///   One constructor keeps every message in the same shape.
61    ///
62    /// In TS you'd write (pseudocode):
63    /// ```ts
64    /// const io = (action: string, path: string, error: Error) => new MigrateError(...);
65    /// ```
66    pub fn io(action: &str, path: &std::path::Path, error: &std::io::Error) -> MigrateError {
67        return MigrateError::Io { action: action.to_string(), path: path.to_path_buf(), reason: error.to_string() };
68    }
69}
70
71/// Prints the cause in plain words.
72impl fmt::Display for MigrateError {
73    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            MigrateError::Io { action, path, reason } => return write!(formatter, "cannot {action} {}: {reason}", path.display()),
76            MigrateError::Primitive(error) => return write!(formatter, "{error}"),
77            MigrateError::UnreadableDirectory { path, reason } => {
78                return write!(
79                    formatter,
80                    "the directory {} cannot be listed or searched by this user ({reason}); copying around it would leave its content out of the new checkout",
81                    path.display()
82                );
83            }
84            MigrateError::Journal(message) => return write!(formatter, "{message}"),
85            MigrateError::Interrupted(point) => return write!(formatter, "stopped at the test point {point}"),
86        }
87    }
88}
89
90/// A migration error is an error value.
91impl std::error::Error for MigrateError {}
92
93/// A primitive failure is a migration failure.
94impl From<PrimitiveError> for MigrateError {
95    fn from(error: PrimitiveError) -> MigrateError {
96        return MigrateError::Primitive(error);
97    }
98}
99
100/// What:
101///  The simulated kill: stop the run after a named point has been reached a number of times.
102///  `AtomicU64` is a number that can change behind a shared borrow from several threads at once (TS has no distinction;
103///  everything mutates).
104/// Why:
105///   A control sets the point, runs, sees the `Interrupted` error, and runs again with `Interrupt::never()`. Worktrees converted
106///  side by side share one hook, so the hits are counted across all of them and exactly one hit is the chosen one.
107///
108/// In TS you'd write (pseudocode):
109/// ```ts
110/// class Interrupt { check(point: string): void { if (point === this.point && ++this.hits > this.after) throw ...; } }
111/// ```
112#[derive(Debug)]
113pub struct Interrupt {
114    /// The point name and how many earlier hits to let pass.
115    target: Option<(String, u64)>,
116    /// Hits seen so far.
117    hits: AtomicU64,
118}
119
120impl Interrupt {
121    /// A run that is never stopped: what the release code uses.
122    pub fn never() -> Interrupt {
123        return Interrupt { target: None, hits: AtomicU64::new(0) };
124    }
125
126    /// A run that stops at the `after`-th (counting from 0) hit of `point`.
127    pub fn at(point: &str, after: u64) -> Interrupt {
128        return Interrupt { target: Some((point.to_string(), after)), hits: AtomicU64::new(0) };
129    }
130
131    /// What:
132    ///  Pass the named point; fail with `Interrupted` when it is the chosen hit.
133    /// Why:
134    ///   The steps call it between their durable actions.
135    ///
136    /// In TS you'd write (pseudocode):
137    /// ```ts
138    /// interrupt.check('after-copy');
139    /// ```
140    pub fn check(&self, point: &str) -> Result<(), MigrateError> {
141        let Some((name, after)) = &self.target else { return Ok(()) };
142        if name != point {
143            return Ok(());
144        }
145        let seen: u64 = self.hits.fetch_add(1, Ordering::SeqCst);
146        if seen == *after {
147            return Err(MigrateError::Interrupted(point.to_string()));
148        }
149        return Ok(());
150    }
151}