use crate::inner_git_words::record_words;
use crate::merge_isolation_changes::{Change, WorktreeState, parse_raw_changes, worktree_path};
use crate::merge_isolation_journal::{MergeIntent, remove_merge_intent};
use crate::recovery_error::{RecoveryError, io_failure};
use crate::transaction_git::{GitContext, GitRequest, run_git_checked};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub struct RecoveryPlace {
pub git: GitContext,
pub cwd: PathBuf,
pub common_dir: PathBuf,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MergeRecoveryAction {
OwnerActive,
Discarded,
Completed,
AlreadyLanded,
BranchMoved,
}
pub fn merge_changes(place: &RecoveryPlace, root: &Path, intent: &MergeIntent) -> Result<Vec<Change>, RecoveryError> {
let request: GitRequest = GitRequest::new(
root,
&["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-abbrev", intent.old_oid.as_str(), intent.new_oid.as_str()],
);
let output = match run_git_checked(&place.git, &request) {
Ok(done) => done,
Err(error) => return Err(RecoveryError(error.to_string())),
};
return parse_raw_changes(output.stdout.as_slice()).map_err(RecoveryError);
}
pub fn index_holds_the_merge(
place: &RecoveryPlace,
root: &Path,
old_oid: &str,
changes: &[Change],
) -> Result<bool, RecoveryError> {
let request: GitRequest = GitRequest::new(
root,
&["diff-index", "--cached", "-z", "--raw", "--no-renames", "--no-abbrev", old_oid, "--"],
);
let output = match run_git_checked(&place.git, &request) {
Ok(done) => done,
Err(error) => return Err(RecoveryError(error.to_string())),
};
let staged: Vec<Change> = parse_raw_changes(output.stdout.as_slice()).map_err(RecoveryError)?;
for change in changes {
let mut found: Option<&Change> = None;
for candidate in &staged {
if candidate.path == change.path {
found = Some(candidate);
break;
}
}
let Some(entry) = found else {
continue;
};
let merged_entry: bool = entry.new_mode == change.new_mode && entry.new_oid == change.new_oid;
if merged_entry {
return Ok(true);
}
}
return Ok(false);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Standing {
Old,
New,
Foreign,
}
pub fn standing(change: &Change, state: &WorktreeState) -> Standing {
let old_matches: bool = match state {
WorktreeState::Missing => !change.existed(),
WorktreeState::Content(oid) => change.existed() && *oid == change.old_oid,
WorktreeState::Other => false,
};
if old_matches {
return Standing::Old;
}
let new_matches: bool = match state {
WorktreeState::Missing => !change.exists_after(),
WorktreeState::Content(oid) => change.exists_after() && *oid == change.new_oid,
WorktreeState::Other => false,
};
if new_matches {
return Standing::New;
}
return Standing::Foreign;
}
pub fn remove_record(record: &Path) -> Result<(), RecoveryError> {
return match remove_merge_intent(record) {
Ok(()) => Ok(()),
Err(error) => Err(io_failure("removing", record, &error)),
};
}
pub fn install_entries(
place: &RecoveryPlace,
root: &Path,
changes: &[Change],
standings: &[Standing],
) -> Result<(), RecoveryError> {
let mut index_input: Vec<u8> = Vec::new();
for change in changes {
if change.exists_after() {
index_input.extend_from_slice(format!("{} {}\t", change.new_mode, change.new_oid).as_bytes());
} else {
index_input.extend_from_slice(format!("0 {}\t", "0".repeat(change.old_oid.len())).as_bytes());
}
index_input.extend_from_slice(change.path.as_slice());
index_input.push(0);
}
let mut update: GitRequest = GitRequest::new(root, &["update-index", "-z", "--index-info"]);
update.input = Some(index_input);
if let Err(error) = run_git_checked(&place.git, &update) {
return Err(RecoveryError(error.to_string()));
}
let mut checkout_input: Vec<u8> = Vec::new();
for (change, found) in changes.iter().zip(standings.iter()) {
let location: PathBuf = worktree_path(root, change.path.as_slice());
if !change.exists_after() {
remove_file_and_empty_parents(root, location.as_path())?;
} else if *found != Standing::New {
checkout_input.extend_from_slice(change.path.as_slice());
checkout_input.push(0);
}
}
if !checkout_input.is_empty() {
let mut checkout: GitRequest = GitRequest::new(root, &["checkout-index", "--force", "-z", "--stdin"]);
checkout.input = Some(checkout_input);
let checked_out = match run_git_checked(&place.git, &checkout) {
Ok(done) => done,
Err(error) => return Err(RecoveryError(error.to_string())),
};
record_words(&["checkout-index", "--force", "-z", "--stdin"], &checked_out);
}
return Ok(());
}
pub fn remove_file_and_empty_parents(root: &Path, file: &Path) -> Result<(), RecoveryError> {
match std::fs::remove_file(file) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(io_failure("removing", file, &error)),
}
let mut directory: Option<&Path> = file.parent();
while let Some(current) = directory {
if current == root || !current.starts_with(root) || std::fs::remove_dir(current).is_err() {
break;
}
directory = current.parent();
}
return Ok(());
}