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