weavatrix_worktree/operation/
commit.rs1use crate::{
2 error::{TransactionPhase, WorktreeError, WorktreeErrorCode},
3 filesystem::SlotEvidence,
4 journal::FinishOutcome,
5 report::{AbortReport, WorktreeApplyReport},
6};
7
8mod evidence;
9mod retained;
10mod rollback;
11
12use evidence::{classify, path_error, present, verify_all, verify_slot};
13use rollback::{finish_without_changes, required, rollback_changed};
14
15use super::{
16 PreparedWorktreeTransaction, journal::Record, model::StagedPath, operation_journal_error,
17 stage::cleanup,
18};
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub(super) enum MutationState {
22 Unchanged,
23 Changed,
24 LinkedInstall,
25 Ambiguous,
26}
27
28impl PreparedWorktreeTransaction {
29 pub fn commit(mut self) -> Result<WorktreeApplyReport, WorktreeError> {
31 if let Err(error) = verify_all(&self) {
32 finish_without_changes(&mut self)?;
33 return Err(error.in_transaction(self.transaction_id.clone()));
34 }
35 let mut changed = Vec::with_capacity(self.paths.len());
36 for index in 0..self.paths.len() {
37 if let Err((error, state)) = commit_one(&mut self, index) {
38 match state {
39 MutationState::Changed | MutationState::LinkedInstall => changed.push(index),
40 MutationState::Unchanged => {}
41 MutationState::Ambiguous => {
42 return Err(error
43 .in_transaction(self.transaction_id.clone())
44 .requiring_recovery());
45 }
46 }
47 rollback_changed(&mut self, &changed)?;
48 return Err(error.in_transaction(self.transaction_id.clone()));
49 }
50 changed.push(index);
51 }
52 self.journal
53 .append(&Record::Finished {
54 outcome: FinishOutcome::Committed,
55 })
56 .map_err(|error| {
57 operation_journal_error(
58 TransactionPhase::Commit,
59 "all operation paths changed but final journal sync failed",
60 error,
61 )
62 .requiring_recovery()
63 })?;
64 cleanup(&self.paths)?;
65 self.control.remove_operation_journal().map_err(|error| {
66 WorktreeError::with_source(
67 WorktreeErrorCode::RecoveryRequired,
68 TransactionPhase::Cleanup,
69 "operation commit succeeded but journal cleanup failed",
70 error,
71 )
72 .requiring_recovery()
73 })?;
74 Ok(WorktreeApplyReport::new(
75 self.transaction_id,
76 self.operation,
77 self.operations,
78 self.paths.len(),
79 ))
80 }
81
82 pub fn commit_retained(
88 self,
89 retention: crate::operation::UndoRetention,
90 ) -> Result<crate::operation::RetainedApplyReport, WorktreeError> {
91 retained::commit_retained(self, retention)
92 }
93
94 pub fn abort(mut self) -> Result<AbortReport, WorktreeError> {
96 self.journal
97 .append(&Record::Finished {
98 outcome: FinishOutcome::Aborted,
99 })
100 .map_err(|error| {
101 operation_journal_error(
102 TransactionPhase::Cleanup,
103 "failed to record explicit operation abort",
104 error,
105 )
106 .requiring_recovery()
107 })?;
108 let removed = cleanup(&self.paths)?;
109 self.control.remove_operation_journal().map_err(|error| {
110 WorktreeError::with_source(
111 WorktreeErrorCode::RecoveryRequired,
112 TransactionPhase::Cleanup,
113 "operation abort succeeded but journal cleanup failed",
114 error,
115 )
116 .requiring_recovery()
117 })?;
118 Ok(AbortReport::new(
119 self.transaction_id,
120 self.paths.len(),
121 removed,
122 ))
123 }
124}
125
126fn commit_one(
127 transaction: &mut PreparedWorktreeTransaction,
128 index: usize,
129) -> Result<(), (WorktreeError, MutationState)> {
130 let path = &transaction.paths[index];
131 transaction
132 .root
133 .revalidate_parent(&path.access)
134 .map_err(|error| {
135 (
136 path_error(
137 TransactionPhase::Commit,
138 path,
139 index,
140 "path parent changed before commit",
141 error,
142 ),
143 MutationState::Unchanged,
144 )
145 })?;
146 verify_slot(path, path.before, transaction, index, "source path changed")
147 .map_err(|error| (error, MutationState::Unchanged))?;
148 transaction
149 .journal
150 .append(&Record::CommitIntent {
151 index: path.stable_index,
152 })
153 .map_err(|error| {
154 (
155 operation_journal_error(
156 TransactionPhase::Commit,
157 "failed to synchronize operation commit intent",
158 error,
159 )
160 .requiring_recovery(),
161 MutationState::Unchanged,
162 )
163 })?;
164 if let Err(error) = mutate(path, transaction) {
165 let state = classify(path, transaction);
166 return Err((
167 if state == MutationState::Ambiguous {
168 error.requiring_recovery()
169 } else {
170 error
171 },
172 state,
173 ));
174 }
175 path.access.sync_parent().map_err(|error| {
176 (
177 path_error(
178 TransactionPhase::Commit,
179 path,
180 index,
181 "path changed but parent synchronization failed",
182 error,
183 )
184 .requiring_recovery(),
185 classify(path, transaction),
186 )
187 })?;
188 transaction
189 .journal
190 .append(&Record::Committed {
191 index: path.stable_index,
192 })
193 .map_err(|error| {
194 (
195 operation_journal_error(
196 TransactionPhase::Commit,
197 "path changed but completion record did not synchronize",
198 error,
199 )
200 .requiring_recovery(),
201 MutationState::Changed,
202 )
203 })?;
204 Ok(())
205}
206
207fn mutate(
208 path: &StagedPath,
209 transaction: &PreparedWorktreeTransaction,
210) -> Result<(), WorktreeError> {
211 match (path.before, path.after) {
212 (SlotEvidence::Absent, SlotEvidence::Present(_)) => path
213 .access
214 .install_absent_from(required(path.stage_name.as_deref(), path)?)
215 .map_err(|error| {
216 mutation_error(path, "failed to install an absent destination", error)
217 }),
218 (SlotEvidence::Present(_), SlotEvidence::Absent) => path
219 .access
220 .remove_exact(
221 present(path.before).expect("present match arm"),
222 transaction.options.limits.max_source_bytes_per_file,
223 )
224 .map_err(|error| mutation_error(path, "failed to remove an exact source", error)),
225 (SlotEvidence::Present(_), SlotEvidence::Present(_)) => path
226 .access
227 .replace_from(required(path.stage_name.as_deref(), path)?)
228 .map_err(|error| mutation_error(path, "failed to replace an exact path", error)),
229 (SlotEvidence::Absent, SlotEvidence::Absent) => Err(WorktreeError::new(
230 WorktreeErrorCode::InvalidPlan,
231 TransactionPhase::Commit,
232 "operation contains an empty path transition",
233 )
234 .at_path(path.path.clone())),
235 }
236}
237
238fn mutation_error(path: &StagedPath, message: &str, source: std::io::Error) -> WorktreeError {
239 path_error(
240 TransactionPhase::Commit,
241 path,
242 path.stable_index as usize,
243 message,
244 source,
245 )
246}