use crate::auto_push_single_flight::{
PushGit, SingleFlightError, run_query,
};
use crate::auto_push_dirty_paths::{diff_paths, listed, overlapping_paths, status_paths};
use crate::diagnostic_log::{debug, warn};
use std::path::{Path, PathBuf};
use crate::inner_git_words::record_streams;
use crate::iso_time::now_iso;
use crate::merge_isolation_journal::{MergeIntent, remove_merge_intent, write_merge_intent};
use crate::merge_isolation_recovery::{MergeRecoveryAction, RecoveryPlace, settle_failed_merge};
use crate::random_id::random_uuid;
use crate::transaction_git::GitContext;
use crate::transaction_owner::current_transaction_owner;
use crate::auto_push_reconcile_basics_and_limits::{TAG, argv};
pub(crate) fn status_differs(before: &[u8], after: &[u8]) -> bool {
return before != after;
}
pub(crate) fn fast_forward_recorded(git: &PushGit, branch_ref: &str, local: &str, merge_oid: &str) -> Result<Result<(), String>, SingleFlightError> {
let moved_arguments = argv(&["merge", "--ff-only", merge_oid]);
let git_dir: Option<PathBuf> = {
let named = run_query(git, &argv(&["rev-parse", "--absolute-git-dir"]))?;
if named.code == Some(0) { Some(PathBuf::from(String::from_utf8_lossy(&named.stdout).trim())) } else { None }
};
let mut recorded: Option<(PathBuf, MergeIntent)> = None;
if let Some(dir) = git_dir.as_deref() {
recorded = record_merge_intent(dir, git, branch_ref, local, merge_oid);
}
let moved = run_query(git, &moved_arguments)?;
if moved.code == Some(0) {
record_streams(&["merge", "--ff-only", merge_oid], &moved.stdout, &moved.stderr);
}
let Some((record, intent)) = recorded else {
return Ok(if moved.code == Some(0) { Ok(()) } else { Err(refusal_of(&moved.stderr)) });
};
if moved.code == Some(0) {
if let Err(error) = remove_merge_intent(record.as_path()) {
debug(TAG, &format!("the merge record could not be removed: {error}"));
}
return Ok(Ok(()));
}
let place: RecoveryPlace = RecoveryPlace {
git: GitContext { real_git: git.real_git.clone(), overlay: git.environment.clone(), global_prefix: Vec::new(), common_dir: git_dir.clone() },
cwd: git.cwd.clone(),
common_dir: git_dir.unwrap_or_default(),
};
return Ok(match settle_failed_merge(&place, record.as_path(), &intent) {
Ok(MergeRecoveryAction::Completed) => Ok(()),
Ok(_) => Err(refusal_of(&moved.stderr)),
Err(error) => Err(format!("{} ({})", refusal_of(&moved.stderr), error.0)),
});
}
pub(crate) fn refusal_of(stderr: &[u8]) -> String {
let detail = String::from_utf8_lossy(stderr);
return format!("Git refused the fast-forward: {}", detail.lines().next().unwrap_or_default().trim());
}
pub(crate) fn advance_local_branch(
git: &PushGit,
branch_ref: &str,
local: &str,
merge_oid: &str,
) -> Result<Result<bool, String>, SingleFlightError> {
let checked_out = run_query(git, &argv(&["symbolic-ref", "--quiet", "HEAD"]))?;
if checked_out.code != Some(0) {
return Ok(Err(String::from("HEAD is detached")));
}
if String::from_utf8_lossy(&checked_out.stdout).trim() != branch_ref {
return Ok(Err(String::from(
"this worktree has another branch checked out",
)));
}
let head = run_query(git, &argv(&["rev-parse", "--verify", "HEAD^{commit}"]))?;
if String::from_utf8_lossy(&head.stdout).trim() != local {
return Ok(Err(String::from(
"the branch moved after the commit landed",
)));
}
let status_arguments = argv(&[
"--no-optional-locks",
"status",
"--porcelain=v1",
"-z",
"--untracked-files=no",
]);
let status = run_query(git, &status_arguments)?;
if status.code != Some(0) {
return Ok(Err(String::from("the worktree status could not be read")));
}
let dirty = status_paths(&status.stdout);
let kept: bool = !dirty.is_empty();
if kept {
let incoming = run_query(
git,
&argv(&["diff", "--name-only", "-z", "--no-renames", local, merge_oid]),
)?;
if incoming.code != Some(0) {
return Ok(Err(String::from(
"the files the merge changes could not be read",
)));
}
let overlap = overlapping_paths(&dirty, &diff_paths(&incoming.stdout));
if !overlap.is_empty() {
return Ok(Err(format!(
"the worktree has uncommitted changes to files the merge changes: {}",
listed(&overlap)
)));
}
}
if let Err(reason) = fast_forward_recorded(git, branch_ref, local, merge_oid)? {
return Ok(Err(reason));
}
if kept {
let after = run_query(git, &status_arguments)?;
if status_differs(&status.stdout, &after.stdout) {
warn(
TAG,
"the worktree status differs after the fast-forward; compare it with the status before the commit",
);
}
}
return Ok(Ok(kept));
}
pub(crate) fn record_merge_intent(dir: &Path, git: &PushGit, branch_ref: &str, local: &str, merge_oid: &str) -> Option<(PathBuf, MergeIntent)> {
let owner = current_transaction_owner(random_uuid().ok()?.as_str(), now_iso().as_str()).ok()?;
let intent: MergeIntent = MergeIntent {
owner,
repository_root: git.cwd.to_string_lossy().into_owned(),
target_ref: Some(branch_ref.to_owned()),
old_oid: local.to_owned(),
new_oid: merge_oid.to_owned(),
};
let record: PathBuf = write_merge_intent(dir, &intent).ok()?;
return Some((record, intent));
}