1use super::diagnostic_log::debug;
24use super::private_storage::remove_tree;
26use super::recovery_error::{RecoveryError, io_failure};
28use super::recovery_files::{path_present, read_optional, remove_file_if_present};
30use super::shadow_git::shadow_request;
32use super::transaction_git::{GitContext, GitRequest, run_git, run_git_checked};
34use super::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; 8] = [
47 "MERGE_HEAD",
48 "MERGE_MSG",
49 "MERGE_MODE",
50 "SQUASH_MSG",
51 "AUTO_MERGE",
52 "CHERRY_PICK_HEAD",
53 "REVERT_HEAD",
54 MERGE_RR_FILENAME,
55];
56
57pub const SEQUENCER_DIRECTORY: &str = "sequencer";
59
60pub const STORE_HELD_PSEUDOREFS: [&str; 3] = ["AUTO_MERGE", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
62
63pub const REMOVED_CONCLUSION_FILES: [&str; 7] = [
65 "AUTO_MERGE",
66 "MERGE_HEAD",
67 "MERGE_MODE",
68 "MERGE_MSG",
69 "SQUASH_MSG",
70 "CHERRY_PICK_HEAD",
71 "REVERT_HEAD",
72];
73
74pub const CONCLUSION_COPY_DIRECTORY: &str = "conclusion";
76
77pub const STORE_RECORD_FILENAME: &str = "store-held.json";
79
80pub fn is_store_held(format: RefFormat, name: &str) -> bool {
91 return format == RefFormat::Reftable && STORE_HELD_PSEUDOREFS.contains(&name);
92}
93
94pub fn resolve_pseudoref(context: &GitContext, request: GitRequest) -> Option<String> {
105 let output: super::transaction_git::GitOutput = run_git(context, &request).ok()?;
106 if !output.succeeded() {
107 return None;
108 }
109 let text: &str = std::str::from_utf8(output.stdout.as_slice()).ok()?;
110 return Some(String::from(super::js_text::trim_javascript(text)));
111}
112
113fn read_tree(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>, RecoveryError> {
125 let mut files: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
127 if !path_present(root)? {
128 return Ok(files);
129 }
130 let mut pending: Vec<PathBuf> = vec![root.to_path_buf()];
131 while let Some(directory) = pending.pop() {
132 let listing: std::fs::ReadDir = match std::fs::read_dir(&directory) {
133 Ok(found) => found,
134 Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
135 };
136 for item in listing {
137 let entry: std::fs::DirEntry = match item {
138 Ok(found) => found,
139 Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
140 };
141 let kind: std::fs::FileType = match entry.file_type() {
142 Ok(found) => found,
143 Err(error) => return Err(io_failure("inspecting", entry.path().as_path(), &error)),
144 };
145 let path: PathBuf = entry.path();
146 if kind.is_dir() {
147 pending.push(path);
148 } else if kind.is_file() {
149 let bytes: Vec<u8> = match std::fs::read(&path) {
150 Ok(read) => read,
151 Err(error) => return Err(io_failure("reading", path.as_path(), &error)),
152 };
153 let relative: PathBuf = path
154 .strip_prefix(root)
155 .unwrap_or(path.as_path())
156 .to_path_buf();
157 files.insert(relative, bytes);
158 }
159 }
160 }
161 return Ok(files);
162}
163
164pub fn same_tree(left: &Path, right: &Path) -> Result<bool, RecoveryError> {
177 let first: BTreeMap<PathBuf, Vec<u8>> = read_tree(left)?;
178 let second: BTreeMap<PathBuf, Vec<u8>> = read_tree(right)?;
179 return Ok(!first.is_empty() && first == second);
180}
181
182fn read_store_record(copies: &Path) -> Result<serde_json::Value, RecoveryError> {
194 let path: PathBuf = copies.join(STORE_RECORD_FILENAME);
195 let bytes: Vec<u8> = match std::fs::read(&path) {
196 Ok(read) => read,
197 Err(error) => return Err(io_failure("reading", path.as_path(), &error)),
198 };
199 let Some(text) = super::json_record::decode_fatal(bytes.as_slice()) else {
200 return Err(RecoveryError(format!("{} is not UTF-8.", path.display())));
201 };
202 match serde_json::from_str::<serde_json::Value>(text) {
203 Ok(value) => return Ok(value),
204 Err(_) => return Err(RecoveryError(format!("{} is not JSON.", path.display()))),
205 }
206}
207
208fn clean_store_held(
219 context: &GitContext,
220 cwd: &Path,
221 shadow: &Path,
222 name: &str,
223 copied: &str,
224) -> Result<(), RecoveryError> {
225 let in_shadow: bool = match run_git(
226 context,
227 &shadow_request(shadow, shadow, &["rev-parse", "--verify", "--quiet", name]),
228 ) {
229 Ok(output) => output.succeeded(),
230 Err(_) => false,
231 };
232 let in_real: Option<String> = resolve_pseudoref(
233 context,
234 GitRequest::new(cwd, &["rev-parse", "--verify", "--quiet", name]),
235 );
236 if !in_shadow
237 && in_real.as_deref() == Some(copied)
238 && let Err(failure) = run_git_checked(
239 context,
240 &GitRequest::new(cwd, &["update-ref", "-d", name, copied]),
241 )
242 {
243 return Err(RecoveryError(failure.0));
244 }
245 return Ok(());
246}
247
248pub fn reproduce_conclusion_cleanup(
263 context: &GitContext,
264 cwd: &Path,
265 git_dir: &Path,
266 shadow: &Path,
267 transaction_directory: &Path,
268 format: RefFormat,
269) -> Result<(), RecoveryError> {
270 let copies: PathBuf = transaction_directory.join(CONCLUSION_COPY_DIRECTORY);
271 if !path_present(copies.as_path())? {
272 debug(
273 "reproduceConclusionCleanup",
274 format!(
275 "no conclusion state was copied: {}",
276 transaction_directory.display()
277 )
278 .as_str(),
279 );
280 return Ok(());
281 }
282 let store: serde_json::Value = read_store_record(copies.as_path())?;
283 for name in REMOVED_CONCLUSION_FILES {
284 if is_store_held(format, name) {
285 if let Some(copied) = store.get(name).and_then(serde_json::Value::as_str) {
286 clean_store_held(context, cwd, shadow, name, copied)?;
287 }
288 continue;
289 }
290 let copy: Option<Vec<u8>> = read_optional(copies.join(name).as_path())?;
291 let in_shadow: Option<Vec<u8>> = read_optional(shadow.join(name).as_path())?;
292 let real: Option<Vec<u8>> = read_optional(git_dir.join(name).as_path())?;
293 if copy.is_some() && in_shadow.is_none() && copy == real {
294 remove_file_if_present(git_dir.join(name).as_path())?;
295 debug(
296 "reproduceConclusionCleanup",
297 format!("removed conclusion entry {name} as native Git did").as_str(),
298 );
299 }
300 }
301 if let Some(merge_rr) = read_optional(shadow.join(MERGE_RR_FILENAME).as_path())? {
302 let temporary: PathBuf = git_dir.join(format!("{MERGE_RR_FILENAME}.cli-git-{}", std::process::id()));
303 if let Err(error) = std::fs::write(&temporary, merge_rr.as_slice()) {
304 return Err(io_failure("writing", temporary.as_path(), &error));
305 }
306 let destination: PathBuf = git_dir.join(MERGE_RR_FILENAME);
307 if let Err(error) = std::fs::rename(&temporary, &destination) {
308 return Err(io_failure("replacing", destination.as_path(), &error));
309 }
310 }
311 let sequencer_copy: PathBuf = copies.join(SEQUENCER_DIRECTORY);
312 let removed_in_shadow: bool = path_present(sequencer_copy.as_path())?
313 && !path_present(shadow.join(SEQUENCER_DIRECTORY).as_path())?;
314 let real_sequencer: PathBuf = git_dir.join(SEQUENCER_DIRECTORY);
315 if removed_in_shadow && same_tree(sequencer_copy.as_path(), real_sequencer.as_path())? {
316 if let Err(error) = remove_tree(real_sequencer.as_path()) {
317 return Err(io_failure("removing", real_sequencer.as_path(), &error));
318 }
319 debug(
320 "reproduceConclusionCleanup",
321 "removed sequencer state as native Git did after the last pick",
322 );
323 }
324 return Ok(());
325}
326
327#[cfg(test)]
329#[path = "conclusion_cleanup_tests.rs"]
330mod tests;