1use core::fmt;
2
3use crate::hash::Sha256Hash;
4
5mod operation;
6
7pub use operation::{OperationChange, OperationKind, WorktreeApplyReport, WorktreeDryRunReport};
8
9#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct FileChange {
12 path: String,
13 old_sha256: Sha256Hash,
14 new_sha256: Sha256Hash,
15 bytes_before: u64,
16 bytes_after: u64,
17 edits_applied: usize,
18}
19
20impl FileChange {
21 pub(crate) fn new(
22 path: impl Into<String>,
23 old_sha256: Sha256Hash,
24 new_sha256: Sha256Hash,
25 bytes_before: u64,
26 bytes_after: u64,
27 edits_applied: usize,
28 ) -> Self {
29 Self {
30 path: path.into(),
31 old_sha256,
32 new_sha256,
33 bytes_before,
34 bytes_after,
35 edits_applied,
36 }
37 }
38
39 #[must_use]
40 pub fn path(&self) -> &str {
41 &self.path
42 }
43
44 #[must_use]
45 pub const fn old_sha256(&self) -> Sha256Hash {
46 self.old_sha256
47 }
48
49 #[must_use]
50 pub const fn new_sha256(&self) -> Sha256Hash {
51 self.new_sha256
52 }
53
54 #[must_use]
55 pub const fn bytes_before(&self) -> u64 {
56 self.bytes_before
57 }
58
59 #[must_use]
60 pub const fn bytes_after(&self) -> u64 {
61 self.bytes_after
62 }
63
64 #[must_use]
65 pub const fn edits_applied(&self) -> usize {
66 self.edits_applied
67 }
68
69 #[must_use]
70 pub fn changed(&self) -> bool {
71 self.old_sha256 != self.new_sha256
72 }
73}
74
75#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct DryRunReport {
78 operation: String,
79 files: Vec<FileChange>,
80}
81
82impl DryRunReport {
83 pub(crate) fn new(operation: impl Into<String>, files: Vec<FileChange>) -> Self {
84 Self {
85 operation: operation.into(),
86 files,
87 }
88 }
89
90 #[must_use]
91 pub fn operation(&self) -> &str {
92 &self.operation
93 }
94
95 #[must_use]
96 pub fn files(&self) -> &[FileChange] {
97 &self.files
98 }
99
100 #[must_use]
101 pub fn total_edits(&self) -> usize {
102 self.files
103 .iter()
104 .fold(0, |total, file| total.saturating_add(file.edits_applied))
105 }
106
107 #[must_use]
108 pub fn total_bytes_before(&self) -> u64 {
109 total_bytes(&self.files, FileChange::bytes_before)
110 }
111
112 #[must_use]
113 pub fn total_bytes_after(&self) -> u64 {
114 total_bytes(&self.files, FileChange::bytes_after)
115 }
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct ApplyReport {
121 transaction_id: String,
122 operation: String,
123 files: Vec<FileChange>,
124}
125
126impl ApplyReport {
127 pub(crate) fn new(
128 transaction_id: impl Into<String>,
129 operation: impl Into<String>,
130 files: Vec<FileChange>,
131 ) -> Self {
132 Self {
133 transaction_id: transaction_id.into(),
134 operation: operation.into(),
135 files,
136 }
137 }
138
139 #[must_use]
140 pub fn transaction_id(&self) -> &str {
141 &self.transaction_id
142 }
143
144 #[must_use]
145 pub fn operation(&self) -> &str {
146 &self.operation
147 }
148
149 #[must_use]
150 pub fn files(&self) -> &[FileChange] {
151 &self.files
152 }
153
154 #[must_use]
155 pub fn total_edits(&self) -> usize {
156 self.files
157 .iter()
158 .fold(0, |total, file| total.saturating_add(file.edits_applied))
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct AbortReport {
165 transaction_id: String,
166 prepared_files: usize,
167 artifacts_removed: usize,
168}
169
170impl AbortReport {
171 pub(crate) fn new(
172 transaction_id: impl Into<String>,
173 prepared_files: usize,
174 artifacts_removed: usize,
175 ) -> Self {
176 Self {
177 transaction_id: transaction_id.into(),
178 prepared_files,
179 artifacts_removed,
180 }
181 }
182
183 #[must_use]
184 pub fn transaction_id(&self) -> &str {
185 &self.transaction_id
186 }
187
188 #[must_use]
189 pub const fn prepared_files(&self) -> usize {
190 self.prepared_files
191 }
192
193 #[must_use]
194 pub const fn artifacts_removed(&self) -> usize {
195 self.artifacts_removed
196 }
197}
198
199#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
201pub enum RecoveryAction {
202 NoPendingTransaction,
203 DiscardedStaging,
204 RolledBack,
205 FinishedCommitCleanup,
206}
207
208impl RecoveryAction {
209 #[must_use]
210 pub const fn as_str(self) -> &'static str {
211 match self {
212 Self::NoPendingTransaction => "NO_PENDING_TRANSACTION",
213 Self::DiscardedStaging => "DISCARDED_STAGING",
214 Self::RolledBack => "ROLLED_BACK",
215 Self::FinishedCommitCleanup => "FINISHED_COMMIT_CLEANUP",
216 }
217 }
218}
219
220impl fmt::Display for RecoveryAction {
221 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
222 formatter.write_str(self.as_str())
223 }
224}
225
226#[derive(Clone, Debug, Eq, PartialEq)]
228pub struct RecoveryReport {
229 transaction_id: Option<String>,
230 action: RecoveryAction,
231 files: Vec<FileChange>,
232 artifacts_removed: usize,
233}
234
235impl RecoveryReport {
236 pub(crate) fn new(
237 transaction_id: Option<String>,
238 action: RecoveryAction,
239 files: Vec<FileChange>,
240 artifacts_removed: usize,
241 ) -> Self {
242 Self {
243 transaction_id,
244 action,
245 files,
246 artifacts_removed,
247 }
248 }
249
250 #[must_use]
251 pub fn transaction_id(&self) -> Option<&str> {
252 self.transaction_id.as_deref()
253 }
254
255 #[must_use]
256 pub const fn action(&self) -> RecoveryAction {
257 self.action
258 }
259
260 #[must_use]
261 pub fn files(&self) -> &[FileChange] {
262 &self.files
263 }
264
265 #[must_use]
266 pub const fn artifacts_removed(&self) -> usize {
267 self.artifacts_removed
268 }
269}
270
271fn total_bytes(files: &[FileChange], projection: fn(&FileChange) -> u64) -> u64 {
272 files
273 .iter()
274 .fold(0, |total, file| total.saturating_add(projection(file)))
275}
276
277#[cfg(test)]
278mod tests {
279 use crate::hash::Sha256Hash;
280 use crate::report::{DryRunReport, FileChange, RecoveryAction};
281
282 #[test]
283 fn totals_and_wire_action_are_deterministic() {
284 let old = Sha256Hash::compute(b"old");
285 let new = Sha256Hash::compute(b"new");
286 let report = DryRunReport::new(
287 "rename",
288 vec![FileChange::new("src/lib.rs", old, new, 3, 3, 2)],
289 );
290 assert_eq!(report.total_edits(), 2);
291 assert_eq!(report.total_bytes_before(), 3);
292 assert_eq!(RecoveryAction::RolledBack.as_str(), "ROLLED_BACK");
293 assert!(report.files()[0].changed());
294 }
295}