git_policy_cli/
recovery_completion.rs1use 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::transaction_git::{GitContext, GitRequest, run_git};
30use super::transaction_journal::{
32 FileIdentity, INDEX_INSTALLED_FILENAME, LandingRecord, PreparingRecord, REF_UPDATED_FILENAME,
33 post_index_filename, pre_landing_index_filename,
34};
35use super::transaction_journal_parse::parse_ref_updated;
37use std::path::{Path, PathBuf};
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum IndexCompletion {
52 Installed,
54 AlreadyInstalled,
56}
57
58pub fn nonce_subject_prefix(transaction_id: &str) -> String {
68 return format!("commit (cli-git {transaction_id}):");
69}
70
71pub fn commit_landed(
90 context: &GitContext,
91 cwd: &Path,
92 directory: &Path,
93 preparing: &PreparingRecord,
94 landing: &LandingRecord,
95) -> Result<bool, RecoveryError> {
96 let marker: PathBuf = directory.join(REF_UPDATED_FILENAME);
97 let new_oid: &str = landing.new_oid.as_deref().unwrap_or("undefined");
98 if recovery_path_exists(marker.as_path())? {
99 let landed: String = parse_ref_updated(read_recovery_file(marker.as_path())?.as_slice())?;
100 if landing.new_oid.as_deref() != Some(landed.as_str()) {
101 return Err(RecoveryError(format!(
102 "Landed marker names another commit than the landing record: {}",
103 directory.display()
104 )));
105 }
106 return Ok(true);
107 }
108 let oids: Vec<String> = list_nonce_reflog_oids(
109 context,
110 cwd,
111 preparing.target_ref.as_str(),
112 nonce_subject_prefix(preparing.transaction_id.as_str()).as_str(),
113 )?;
114 if oids.len() > 1 {
115 return Err(RecoveryError(format!(
116 "{} reflog names several commits for transaction {}; recovery retained at {}",
117 preparing.target_ref,
118 preparing.transaction_id,
119 directory.display()
120 )));
121 }
122 if let Some(found) = oids.first() {
123 if found != new_oid {
124 return Err(RecoveryError(format!(
125 "{} reflog nonce names {found}, not the recorded {new_oid}; recovery retained at {}",
126 preparing.target_ref,
127 directory.display()
128 )));
129 }
130 return Ok(true);
131 }
132 let reachable: bool = match run_git(
133 context,
134 &GitRequest::new(
135 cwd,
136 &[
137 "merge-base",
138 "--is-ancestor",
139 new_oid,
140 preparing.target_ref.as_str(),
141 ],
142 ),
143 ) {
144 Ok(output) => output.succeeded(),
145 Err(_) => false,
146 };
147 if reachable {
148 return Err(RecoveryError(format!(
149 "{} contains the transaction commit {new_oid} but its reflog lacks the nonce entry; \
150 recovery retained at {}",
151 preparing.target_ref,
152 directory.display()
153 )));
154 }
155 return Ok(false);
156}
157
158fn stabilize_artifact(
170 directory: &Path,
171 filename: &str,
172 identity: &FileIdentity,
173) -> Result<PathBuf, RecoveryError> {
174 let stable: PathBuf = directory.join(format!("{filename}.recovery"));
175 remove_file_if_present(stable.as_path())?;
176 create_owned_file_link(
177 directory.join(filename).as_path(),
178 stable.as_path(),
179 identity.device.as_str(),
180 identity.inode.as_str(),
181 )?;
182 return Ok(stable);
183}
184
185pub fn complete_index(
200 directory: &Path,
201 preparing: &PreparingRecord,
202 landing: &LandingRecord,
203) -> Result<IndexCompletion, RecoveryError> {
204 let pre_landing: PathBuf = stabilize_artifact(
205 directory,
206 pre_landing_index_filename(landing.attempt).as_str(),
207 &landing.pre_landing_index,
208 )?;
209 let post: PathBuf = stabilize_artifact(
210 directory,
211 post_index_filename(landing.attempt).as_str(),
212 &landing.post_index,
213 )?;
214 let real_index: &Path = Path::new(preparing.real_index_path.as_str());
215 let lock_path: PathBuf = PathBuf::from(format!("{}.lock", preparing.real_index_path));
216 if recovery_path_exists(directory.join(INDEX_INSTALLED_FILENAME).as_path())?
217 || files_equal(post.as_path(), real_index)?
218 {
219 release_owned_lock(&landing.lock, lock_path.as_path())?;
220 return Ok(IndexCompletion::AlreadyInstalled);
221 }
222 if !files_equal(pre_landing.as_path(), real_index)? {
223 return Err(RecoveryError(format!(
224 "Real index no longer matches the pre-landing snapshot; recovery retained at {}",
225 directory.display()
226 )));
227 }
228 install_recovered_index(
229 lock_path.as_path(),
230 real_index,
231 post.as_path(),
232 &landing.lock,
233 )?;
234 return Ok(IndexCompletion::Installed);
235}
236
237#[cfg(test)]
239#[path = "recovery_completion_tests.rs"]
240mod tests;