1use crate::diagnostic_log::debug;
24use crate::private_storage::remove_tree;
26use crate::recovery_error::{RecoveryError, io_failure};
28use crate::recovery_files::{path_present, read_optional, remove_file_if_present};
30use crate::shadow_git::shadow_request;
32use crate::transaction_git::{GitContext, GitRequest, run_git, run_git_checked};
34use crate::transaction_journal::RefFormat;
36use std::collections::BTreeMap;
38use std::path::{Path, PathBuf};
40
41pub const MERGE_RR_FILENAME: &str = "MERGE_RR";
44
45pub const CONCLUSION_STATE_FILES: [&str; 9] = [
47 "MERGE_HEAD",
48 "MERGE_AUTOSTASH",
52 "MERGE_MSG",
53 "MERGE_MODE",
54 "SQUASH_MSG",
55 "AUTO_MERGE",
56 "CHERRY_PICK_HEAD",
57 "REVERT_HEAD",
58 MERGE_RR_FILENAME,
59];
60
61pub const SEQUENCER_DIRECTORY: &str = "sequencer";
63
64pub const STORE_HELD_PSEUDOREFS: [&str; 3] = ["AUTO_MERGE", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
66
67pub const REMOVED_CONCLUSION_FILES: [&str; 8] = [
69 "AUTO_MERGE",
70 "MERGE_AUTOSTASH",
71 "MERGE_HEAD",
72 "MERGE_MODE",
73 "MERGE_MSG",
74 "SQUASH_MSG",
75 "CHERRY_PICK_HEAD",
76 "REVERT_HEAD",
77];
78
79pub const CONCLUSION_COPY_DIRECTORY: &str = "conclusion";
81
82pub const STORE_RECORD_FILENAME: &str = "store-held.json";
84
85pub fn is_store_held(format: RefFormat, name: &str) -> bool {
96 return format == RefFormat::Reftable && STORE_HELD_PSEUDOREFS.contains(&name);
97}
98
99pub fn resolve_pseudoref(context: &GitContext, request: GitRequest) -> Option<String> {
110 let output: crate::transaction_git::GitOutput = run_git(context, &request).ok()?;
111 if !output.succeeded() {
112 return None;
113 }
114 let text: &str = std::str::from_utf8(output.stdout.as_slice()).ok()?;
115 return Some(String::from(crate::js_text::trim_javascript(text)));
116}
117
118fn read_tree(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>, RecoveryError> {
130 let mut files: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
132 if !path_present(root)? {
133 return Ok(files);
134 }
135 let mut pending: Vec<PathBuf> = vec![root.to_path_buf()];
136 while let Some(directory) = pending.pop() {
137 let listing: std::fs::ReadDir = match std::fs::read_dir(&directory) {
138 Ok(found) => found,
139 Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
140 };
141 for item in listing {
142 let entry: std::fs::DirEntry = match item {
143 Ok(found) => found,
144 Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
145 };
146 let kind: std::fs::FileType = match entry.file_type() {
147 Ok(found) => found,
148 Err(error) => return Err(io_failure("inspecting", entry.path().as_path(), &error)),
149 };
150 let path: PathBuf = entry.path();
151 if kind.is_dir() {
152 pending.push(path);
153 } else if kind.is_file() {
154 let bytes: Vec<u8> = match std::fs::read(&path) {
155 Ok(read) => read,
156 Err(error) => return Err(io_failure("reading", path.as_path(), &error)),
157 };
158 let relative: PathBuf = path
159 .strip_prefix(root)
160 .unwrap_or(path.as_path())
161 .to_path_buf();
162 files.insert(relative, bytes);
163 }
164 }
165 }
166 return Ok(files);
167}
168
169pub fn same_tree(left: &Path, right: &Path) -> Result<bool, RecoveryError> {
182 let first: BTreeMap<PathBuf, Vec<u8>> = read_tree(left)?;
183 let second: BTreeMap<PathBuf, Vec<u8>> = read_tree(right)?;
184 return Ok(!first.is_empty() && first == second);
185}
186
187fn read_store_record(copies: &Path) -> Result<serde_json::Value, RecoveryError> {
199 let path: PathBuf = copies.join(STORE_RECORD_FILENAME);
200 let bytes: Vec<u8> = match std::fs::read(&path) {
201 Ok(read) => read,
202 Err(error) => return Err(io_failure("reading", path.as_path(), &error)),
203 };
204 let Some(text) = crate::json_record::decode_fatal(bytes.as_slice()) else {
205 return Err(RecoveryError(format!("{} is not UTF-8.", path.display())));
206 };
207 match serde_json::from_str::<serde_json::Value>(text) {
208 Ok(value) => return Ok(value),
209 Err(_) => return Err(RecoveryError(format!("{} is not JSON.", path.display()))),
210 }
211}
212
213fn clean_store_held(
224 context: &GitContext,
225 cwd: &Path,
226 shadow: &Path,
227 name: &str,
228 copied: &str,
229) -> Result<(), RecoveryError> {
230 let in_shadow: bool = match run_git(
231 context,
232 &shadow_request(shadow, shadow, &["rev-parse", "--verify", "--quiet", name]),
233 ) {
234 Ok(output) => output.succeeded(),
235 Err(_) => false,
236 };
237 let in_real: Option<String> = resolve_pseudoref(
238 context,
239 GitRequest::new(cwd, &["rev-parse", "--verify", "--quiet", name]),
240 );
241 if !in_shadow
242 && in_real.as_deref() == Some(copied)
243 && let Err(failure) = run_git_checked(
244 context,
245 &GitRequest::new(cwd, &["update-ref", "-d", name, copied]),
246 )
247 {
248 return Err(RecoveryError(failure.0));
249 }
250 return Ok(());
251}
252
253pub fn reproduce_conclusion_cleanup(
268 context: &GitContext,
269 cwd: &Path,
270 git_dir: &Path,
271 shadow: &Path,
272 transaction_directory: &Path,
273 format: RefFormat,
274) -> Result<(), RecoveryError> {
275 let copies: PathBuf = transaction_directory.join(CONCLUSION_COPY_DIRECTORY);
276 if !path_present(copies.as_path())? {
277 debug(
278 "reproduceConclusionCleanup",
279 format!(
280 "no conclusion state was copied: {}",
281 transaction_directory.display()
282 )
283 .as_str(),
284 );
285 return Ok(());
286 }
287 let store: serde_json::Value = read_store_record(copies.as_path())?;
288 for name in REMOVED_CONCLUSION_FILES {
289 if is_store_held(format, name) {
290 if let Some(copied) = store.get(name).and_then(serde_json::Value::as_str) {
291 clean_store_held(context, cwd, shadow, name, copied)?;
292 }
293 continue;
294 }
295 let copy: Option<Vec<u8>> = read_optional(copies.join(name).as_path())?;
296 let in_shadow: Option<Vec<u8>> = read_optional(shadow.join(name).as_path())?;
297 let real: Option<Vec<u8>> = read_optional(git_dir.join(name).as_path())?;
298 if copy.is_some() && in_shadow.is_none() && copy == real {
299 remove_file_if_present(git_dir.join(name).as_path())?;
300 debug(
301 "reproduceConclusionCleanup",
302 format!("removed conclusion entry {name} as native Git did").as_str(),
303 );
304 }
305 }
306 if let Some(merge_rr) = read_optional(shadow.join(MERGE_RR_FILENAME).as_path())? {
307 let temporary: PathBuf = git_dir.join(format!("{MERGE_RR_FILENAME}.cli-git-{}", std::process::id()));
308 if let Err(error) = std::fs::write(&temporary, merge_rr.as_slice()) {
309 return Err(io_failure("writing", temporary.as_path(), &error));
310 }
311 let destination: PathBuf = git_dir.join(MERGE_RR_FILENAME);
312 if let Err(error) = std::fs::rename(&temporary, &destination) {
313 return Err(io_failure("replacing", destination.as_path(), &error));
314 }
315 }
316 let sequencer_copy: PathBuf = copies.join(SEQUENCER_DIRECTORY);
317 let removed_in_shadow: bool = path_present(sequencer_copy.as_path())?
318 && !path_present(shadow.join(SEQUENCER_DIRECTORY).as_path())?;
319 let real_sequencer: PathBuf = git_dir.join(SEQUENCER_DIRECTORY);
320 if removed_in_shadow && same_tree(sequencer_copy.as_path(), real_sequencer.as_path())? {
321 if let Err(error) = remove_tree(real_sequencer.as_path()) {
322 return Err(io_failure("removing", real_sequencer.as_path(), &error));
323 }
324 debug(
325 "reproduceConclusionCleanup",
326 "removed sequencer state as native Git did after the last pick",
327 );
328 }
329 return Ok(());
330}
331
332#[cfg(test)]
334#[path = "conclusion_cleanup_tests.rs"]
335mod tests;