use crate::diagnostic_log::debug;
use crate::merge_isolation_journal::{
MergeIntent, list_merge_intents, parse_merge_intent,
};
use crate::merge_isolation_moved::settle_moved_branch;
pub use crate::merge_isolation_recovery_steps::{MergeRecoveryAction, RecoveryPlace, Standing};
use crate::merge_isolation_recovery_steps::{index_holds_the_merge, install_entries, merge_changes, remove_record, standing};
use crate::merge_isolation_changes::{
Change, WorktreeState, worktree_state,
};
use crate::recovery_error::{RecoveryError, io_failure};
use crate::transaction_owner::{OwnerLiveness, OwnerLivenessError, classify_transaction_owner_record};
use crate::transaction_git::{GitRequest, run_git, run_git_checked};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MergeRecoveryOutcome {
pub record: PathBuf,
pub action: MergeRecoveryAction,
}
pub fn merge_intents_to_recover(git_dir: &Path) -> Result<bool, RecoveryError> {
for record in list_merge_intents(git_dir)? {
let bytes: Vec<u8> = match std::fs::read(record.as_path()) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(io_failure("reading", record.as_path(), &error)),
};
let Some(intent) = parse_merge_intent(bytes.as_slice()) else {
return Ok(true);
};
if owner_liveness(&intent, record.as_path())? == OwnerLiveness::Dead {
return Ok(true);
}
}
return Ok(false);
}
pub fn recover_interrupted_merges(
place: &RecoveryPlace,
git_dir: &Path,
) -> Result<Vec<MergeRecoveryOutcome>, RecoveryError> {
let mut outcomes: Vec<MergeRecoveryOutcome> = Vec::new();
let mut planned: Vec<(PathBuf, MergeIntent, OwnerLiveness)> = Vec::new();
for record in list_merge_intents(git_dir)? {
let bytes: Vec<u8> = match std::fs::read(record.as_path()) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(io_failure("reading", record.as_path(), &error)),
};
let Some(intent) = parse_merge_intent(bytes.as_slice()) else {
return Err(RecoveryError(format!(
"The merge record {} is malformed. It was left in place; if no merge is running in this worktree, \
remove it, and check the branch and `git status` before merging again.",
record.display()
)));
};
let liveness: OwnerLiveness = owner_liveness(&intent, record.as_path())?;
planned.push((record, intent, liveness));
}
for (record, intent, liveness) in planned {
let action: MergeRecoveryAction = if liveness == OwnerLiveness::Alive {
debug("recoverInterruptedMerges", format!("the owner of {} still runs", record.display()).as_str());
MergeRecoveryAction::OwnerActive
} else {
settle_failed_merge(place, record.as_path(), &intent)?
};
outcomes.push(MergeRecoveryOutcome { record, action });
}
return Ok(outcomes);
}
fn owner_liveness(intent: &MergeIntent, record: &Path) -> Result<OwnerLiveness, RecoveryError> {
match classify_transaction_owner_record(&intent.owner, record) {
Ok(found) => return Ok(found),
Err(OwnerLivenessError::Probe(error)) if error.raw_os_error() == Some(libc::EPERM) => {
return Ok(OwnerLiveness::Alive);
}
Err(OwnerLivenessError::Probe(error)) => {
return Err(RecoveryError(format!(
"probing the owner {} of the merge record {} failed: {error}",
intent.owner.owner_pid,
record.display()
)));
}
Err(OwnerLivenessError::ExpiredForeign(text)) => return Err(RecoveryError(text)),
Err(OwnerLivenessError::Identity(error)) => {
return Err(RecoveryError(format!(
"reading the identity of the owner {} of the merge record {} failed: {error}",
intent.owner.owner_pid,
record.display()
)));
}
}
}
fn resolve_commit(place: &RecoveryPlace, root: &Path, reference: &str) -> Result<Option<String>, RecoveryError> {
let revision: String = format!("{reference}^{{commit}}");
let request: GitRequest = GitRequest::new(root, &["rev-parse", "--verify", "--quiet", revision.as_str()]);
let output = match run_git(&place.git, &request) {
Ok(done) => done,
Err(error) => return Err(RecoveryError(format!("git rev-parse could not start: {error}"))),
};
if !output.succeeded() {
return Ok(None);
}
return Ok(Some(String::from_utf8_lossy(&output.stdout).trim().to_owned()));
}
pub fn settle_failed_merge(
place: &RecoveryPlace,
record: &Path,
intent: &MergeIntent,
) -> Result<MergeRecoveryAction, RecoveryError> {
let root: &Path = Path::new(intent.repository_root.as_str());
let target: &str = intent.target_ref.as_deref().unwrap_or("HEAD");
let current: Option<String> = resolve_commit(place, root, target)?;
if current.as_deref() == Some(intent.new_oid.as_str()) {
remove_record(record)?;
return Ok(MergeRecoveryAction::AlreadyLanded);
}
match current.as_deref() {
Some(head) if head == intent.old_oid => {}
Some(head) => return settle_moved_branch(place, record, intent, head),
None => {
return Err(RecoveryError(format!(
"{target} names no commit although the interrupted merge in {} expected {}. Nothing was changed and the \
record {} was kept. Check `git status` and `git log {target}`; remove the record when {target} is as you \
want it.",
root.display(),
intent.old_oid,
record.display()
)));
}
}
let changes: Vec<Change> = merge_changes(place, root, intent)?;
let mut standings: Vec<Standing> = Vec::with_capacity(changes.len());
for change in &changes {
let state: WorktreeState = worktree_state(&place.git, root, change.path.as_slice()).map_err(RecoveryError)?;
standings.push(standing(change, &state));
}
let index_started: bool = index_holds_the_merge(place, root, intent.old_oid.as_str(), changes.as_slice())?;
let worktree_started: bool = standings.contains(&Standing::New);
if !index_started && !worktree_started {
remove_record(record)?;
return Ok(MergeRecoveryAction::Discarded);
}
let mut foreign: Vec<String> = Vec::new();
for (change, found) in changes.iter().zip(standings.iter()) {
if *found == Standing::Foreign {
foreign.push(String::from_utf8_lossy(change.path.as_slice()).into_owned());
}
}
if !foreign.is_empty() {
return Err(RecoveryError(format!(
"A merge in {} was interrupted while Git updated these files, and they now hold neither their old nor \
their merged content: {}. Nothing was changed and the record {} was kept. Save your edits to them \
elsewhere, restore them with `git restore -- <path>`, then run any git command again so recovery can \
finish the merge; or remove the record to abandon the merge and check `git status` yourself.",
root.display(),
foreign.join(", "),
record.display()
)));
}
complete(place, root, intent, &changes, &standings)?;
remove_record(record)?;
return Ok(MergeRecoveryAction::Completed);
}
fn complete(
place: &RecoveryPlace,
root: &Path,
intent: &MergeIntent,
changes: &[Change],
standings: &[Standing],
) -> Result<(), RecoveryError> {
install_entries(place, root, changes, standings)?;
let message: String = String::from("merge: Merge made by the 'ort' strategy. (completed by monochromatic-git-policy-cli recovery)");
let mut arguments: Vec<&str> = vec!["update-ref", "-m", message.as_str()];
if intent.target_ref.is_none() {
arguments.push("--no-deref");
}
arguments.push(intent.target_ref.as_deref().unwrap_or("HEAD"));
arguments.push(intent.new_oid.as_str());
arguments.push(intent.old_oid.as_str());
if let Err(error) = run_git_checked(&place.git, &GitRequest::new(root, &arguments)) {
return Err(RecoveryError(error.to_string()));
}
return Ok(());
}
#[cfg(test)]
#[path = "merge_isolation_recovery_tests.rs"]
mod tests;