1use super::recovery_error::RecoveryError;
19use super::recovery_files::{
23 create_owned_file_link, files_equal, install_recovered_index, read_recovery_file,
24 recovery_path_exists, release_owned_lock, remove_file_if_present,
25};
26use super::recovery_reflog::list_nonce_reflog_oids;
28use super::recovery_record_failure::{RecordKind, name_record_failure};
30use super::recovery_snapshot::{SnapshotRole, verify_snapshot};
32use super::transaction_git::{GitContext, GitRequest, run_git};
34use super::transaction_journal::{
36 FileIdentity, INDEX_INSTALLED_FILENAME, LandingRecord, PreparingRecord, REF_UPDATED_FILENAME,
37 post_index_filename, pre_landing_index_filename,
38};
39use super::transaction_journal_parse::parse_ref_updated;
41use std::path::{Path, PathBuf};
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum IndexCompletion {
56 Installed,
58 AlreadyInstalled,
60}
61
62pub fn nonce_subject_prefix(transaction_id: &str) -> String {
72 return format!("commit (cli-git {transaction_id}):");
73}
74
75pub fn commit_landed(
94 context: &GitContext,
95 cwd: &Path,
96 directory: &Path,
97 preparing: &PreparingRecord,
98 landing: &LandingRecord,
99) -> Result<bool, RecoveryError> {
100 let marker: PathBuf = directory.join(REF_UPDATED_FILENAME);
101 let new_oid: &str = landing.new_oid.as_deref().unwrap_or("undefined");
102 if recovery_path_exists(marker.as_path())? {
103 let landed: String = parse_ref_updated(read_recovery_file(marker.as_path())?.as_slice())
104 .map_err(|error| { return name_record_failure(error, marker.as_path(), RecordKind::RefUpdated); })?;
105 if landing.new_oid.as_deref() != Some(landed.as_str()) {
106 return Err(RecoveryError(format!(
107 "Landed marker names another commit than the landing record: {}",
108 directory.display()
109 )));
110 }
111 return Ok(true);
112 }
113 let oids: Vec<String> = list_nonce_reflog_oids(
114 context,
115 cwd,
116 preparing.target_ref.as_str(),
117 nonce_subject_prefix(preparing.transaction_id.as_str()).as_str(),
118 )?;
119 if oids.len() > 1 {
120 return Err(RecoveryError(format!(
121 "{} reflog names several commits for transaction {}; recovery retained at {}",
122 preparing.target_ref,
123 preparing.transaction_id,
124 directory.display()
125 )));
126 }
127 if let Some(found) = oids.first() {
128 if found != new_oid {
129 return Err(RecoveryError(format!(
130 "{} reflog nonce names {found}, not the recorded {new_oid}; recovery retained at {}",
131 preparing.target_ref,
132 directory.display()
133 )));
134 }
135 return Ok(true);
136 }
137 let reachable: bool = match run_git(
138 context,
139 &GitRequest::new(
140 cwd,
141 &[
142 "merge-base",
143 "--is-ancestor",
144 new_oid,
145 preparing.target_ref.as_str(),
146 ],
147 ),
148 ) {
149 Ok(output) => output.succeeded(),
150 Err(_) => false,
151 };
152 if reachable {
153 return Err(RecoveryError(format!(
154 "{} contains the transaction commit {new_oid} but its reflog lacks the nonce entry; \
155 recovery retained at {}",
156 preparing.target_ref,
157 directory.display()
158 )));
159 }
160 return Ok(false);
161}
162
163fn stabilize_artifact(
175 directory: &Path,
176 filename: &str,
177 identity: &FileIdentity,
178) -> Result<PathBuf, RecoveryError> {
179 let stable: PathBuf = directory.join(format!("{filename}.recovery"));
180 remove_file_if_present(stable.as_path())?;
181 create_owned_file_link(
182 directory.join(filename).as_path(),
183 stable.as_path(),
184 identity.device.as_str(),
185 identity.inode.as_str(),
186 )?;
187 return Ok(stable);
188}
189
190pub fn complete_index(
205 directory: &Path,
206 preparing: &PreparingRecord,
207 landing: &LandingRecord,
208) -> Result<IndexCompletion, RecoveryError> {
209 let pre_landing: PathBuf = stabilize_artifact(
210 directory,
211 pre_landing_index_filename(landing.attempt).as_str(),
212 &landing.pre_landing_index,
213 )?;
214 let post: PathBuf = stabilize_artifact(
215 directory,
216 post_index_filename(landing.attempt).as_str(),
217 &landing.post_index,
218 )?;
219 let real_index: &Path = Path::new(preparing.real_index_path.as_str());
220 let lock_path: PathBuf = PathBuf::from(format!("{}.lock", preparing.real_index_path));
221 if recovery_path_exists(directory.join(INDEX_INSTALLED_FILENAME).as_path())?
222 || files_equal(post.as_path(), real_index)?
223 {
224 release_owned_lock(&landing.lock, lock_path.as_path())?;
225 return Ok(IndexCompletion::AlreadyInstalled);
226 }
227 verify_snapshot(
229 directory,
230 preparing,
231 landing,
232 pre_landing.as_path(),
233 SnapshotRole::PreLanding,
234 landing.pre_landing_digest,
235 )?;
236 verify_snapshot(directory, preparing, landing, post.as_path(), SnapshotRole::Post, landing.post_digest)?;
237 if !files_equal(pre_landing.as_path(), real_index)? {
238 return Err(RecoveryError(format!(
239 "Real index no longer matches the pre-landing snapshot; recovery retained at {}",
240 directory.display()
241 )));
242 }
243 install_recovered_index(
244 lock_path.as_path(),
245 real_index,
246 post.as_path(),
247 &landing.lock,
248 )?;
249 return Ok(IndexCompletion::Installed);
250}
251
252#[cfg(test)]
254#[path = "recovery_completion_tests.rs"]
255mod tests;