use crate::child_environment::environment_with_overlay;
use crate::diagnostic_log::debug;
use crate::inner_git_words::record_streams;
use crate::diagnostics::{EngineFailureCode, render_engine_failure_with_hint};
use crate::merge_second_pass::{ContextRunner, GitReply, GitRunner, Merged, config_options, conflicts_of_merge_tree};
use crate::journald_native::CLIENT_IDENTIFIER;
use crate::merge_second_pass_tree::{AttributeProbe, announce, attribute_tree_variables, resolve_tree};
use crate::path_bytes::path_message_text;
use crate::merge_isolation_journal::{MergeIntent, remove_merge_intent, write_merge_intent};
use crate::merge_isolation_recovery::{
MergeRecoveryAction, settle_failed_merge,
};
use crate::merge_isolation_report::{conflict_finding, not_applied_finding};
use crate::merge_options::{MergeShape, Signing};
use crate::merge_isolation_recovery::RecoveryPlace;
use crate::transaction_phase::{Phase, reach_transaction_phase};
use crate::transaction_git::{GitContext, GitOutput};
use crate::transaction_owner::current_transaction_owner;
use crate::iso_time::now_iso;
use crate::random_id::random_uuid;
use crate::index_holder_wait::wait_for_index_release_here;
use crate::merge_isolation_git_reads::Session;
use crate::merge_isolation_run_types::{Bail, IsolatedMerge, IsolatedMergeInput, MergeConfig, Plan, TAG, refused};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub fn with_second_pass_report(done: IsolatedMerge, merged: &[Merged]) -> IsolatedMerge {
match done {
IsolatedMerge::Finished { code: 0, stdout, stderr } if !merged.is_empty() => {
let mut report: String = String::new();
for line in announce("monochromatic-git-policy-cli: ", (CLIENT_IDENTIFIER, "merge"), merged) {
report.push_str(line.as_str());
report.push('\n');
}
return IsolatedMerge::Finished { code: 0, stdout, stderr: report + stderr.as_str() };
}
other => return other,
}
}
pub fn merge_tree(
session: &Session,
shape: &MergeShape,
ends: (&str, &str, &str),
places: (&Path, &Path, &[(OsString, OsString)]),
staged_work: bool,
) -> Result<(String, Vec<Merged>), Bail> {
let (head, target, reference): (&str, &str, &str) = ends;
let (common_dir, root, environment): (&Path, &Path, &[(OsString, OsString)]) = places;
let mut arguments: Vec<&str> = vec!["merge-tree", "--write-tree", "-z", "--no-messages"];
if shape.allow_unrelated_histories {
arguments.push("--allow-unrelated-histories");
}
arguments.push(head);
arguments.push(target);
let probing: ContextRunner<'_> = ContextRunner { context: &session.context, cwd: Path::new("."), without_prefix: false, object_directory: None, environment: Vec::new() };
let variables: Vec<(OsString, OsString)> = environment_with_overlay(environment, session.context.overlay.as_slice());
let probe: AttributeProbe<'_> = AttributeProbe { directory: root, global_prefix: session.context.global_prefix.as_slice(), variables: variables.as_slice(), runner: &probing };
let runner: ContextRunner<'_> = ContextRunner {
context: &session.context,
cwd: Path::new("."),
without_prefix: false,
object_directory: None,
environment: attribute_tree_variables(head, &probe),
};
let merged: GitReply = runner.run(arguments.as_slice(), None, None)?;
let (tree, conflicts) = conflicts_of_merge_tree(merged.stdout.as_slice());
match merged.code {
Some(0) if !tree.is_empty() => return Ok((tree, Vec::new())),
Some(0) => return Err(Bail::from(String::from("merge-tree printed no tree"))),
Some(1) if !staged_work => return Err(Bail::Forward(None)),
Some(1) => {
let top_context: GitContext = GitContext { global_prefix: config_options(&session.context.global_prefix), ..session.context.clone() };
let top_runner: ContextRunner<'_> = ContextRunner { context: &top_context, cwd: root, ..runner };
if let Some(resolved) = resolve_tree(&top_runner, common_dir, tree.as_str(), conflicts.as_slice()) {
return Ok((resolved.tree, resolved.merged));
}
let mut paths: Vec<String> = Vec::new();
for conflict in &conflicts {
paths.push(path_message_text(conflict.path.as_slice()));
}
return Err(refused(2, conflict_finding(reference, paths.as_slice())));
}
_ => return Err(Bail::from(format!("git merge-tree failed: {}", String::from_utf8_lossy(&merged.stderr).trim()))),
}
}
pub fn commit_merge(
session: &Session,
shape: &MergeShape,
config: &MergeConfig,
ends: (&str, &str, &str),
message: Vec<u8>,
) -> Result<String, String> {
let (head, target, tree): (&str, &str, &str) = ends;
let signing: Option<String> = match &shape.signing {
Signing::Never => None,
Signing::Sign(None) => Some(String::from("-S")),
Signing::Sign(Some(key)) => Some(format!("-S{}", String::from_utf8_lossy(key))),
Signing::Default => if config.sign_commits { Some(String::from("-S")) } else { None },
};
let mut arguments: Vec<&str> = vec!["commit-tree", tree, "-p", head, "-p", target];
if let Some(flag) = signing.as_deref() {
arguments.push(flag);
}
let created: GitOutput = session.run_with_input(arguments.as_slice(), message)?;
if !created.succeeded() {
return Err(created.error_text());
}
return Ok(String::from_utf8_lossy(&created.stdout).trim().to_owned());
}
pub fn apply(input: &IsolatedMergeInput<'_>, session: &Session, plan: &Plan<'_>) -> Result<IsolatedMerge, Bail> {
let (git_dir, root): (&Path, &Path) = (plan.git_dir, plan.root);
let (old_oid, new_oid): (&str, &str) = (plan.old_oid, plan.new_oid);
let shape: &MergeShape = plan.shape;
let reference: String = shape.reference.to_string_lossy().into_owned();
let target_ref: Option<String> = {
let named: GitOutput = session.run(&["symbolic-ref", "--quiet", "HEAD"])?;
if named.succeeded() { Some(String::from_utf8_lossy(&named.stdout).trim().to_owned()) } else { None }
};
wait_for_index_release_here(git_dir.join("index").as_path(), "merge");
let transaction_id: String = match random_uuid() {
Ok(id) => id,
Err(error) => return Err(Bail::from(error.0)),
};
let owner = match current_transaction_owner(transaction_id.as_str(), now_iso().as_str()) {
Ok(found) => found,
Err(error) => return Err(Bail::from(error.to_string())),
};
let intent: MergeIntent = MergeIntent {
owner,
repository_root: root.to_string_lossy().into_owned(),
target_ref,
old_oid: String::from(old_oid),
new_oid: String::from(new_oid),
};
let record: PathBuf = match write_merge_intent(git_dir, &intent) {
Ok(written) => written,
Err(error) => return Err(Bail::from(format!("the merge record could not be written: {error}"))),
};
reach_phase(Phase::MergeIntent)?;
let mut overlay: Vec<(OsString, OsString)> = input.overlay.to_vec();
overlay.push((OsString::from("GIT_REFLOG_ACTION"), OsString::from(format!("merge {reference}"))));
let mover: Session = Session {
context: GitContext { overlay, ..session.context.clone() },
};
let moved: GitOutput = match mover.run(&["merge", "--ff-only", "--no-edit", new_oid]) {
Ok(output) => output,
Err(reason) => GitOutput { code: None, stdout: Vec::new(), stderr: reason.into_bytes() },
};
let place: RecoveryPlace = RecoveryPlace {
git: GitContext { global_prefix: Vec::new(), ..session.context.clone() },
cwd: root.to_path_buf(),
common_dir: git_dir.to_path_buf(),
};
if !moved.succeeded() {
let detail: String = moved.error_text();
let lock_taken: bool = detail.contains("index.lock");
return Err(match settle_failed_merge(&place, record.as_path(), &intent) {
Ok(MergeRecoveryAction::BranchMoved) => Bail::Again(format!("{reference} was merged on a branch that moved meanwhile")),
Ok(MergeRecoveryAction::Discarded) if lock_taken => Bail::Again(String::from("another program took the index lock first")),
Ok(MergeRecoveryAction::Completed) => Bail::Done(IsolatedMerge::Finished {
code: 0,
stdout: String::new(),
stderr: format!(
"monochromatic-git-policy-cli: git merge {reference}: Git reported an error ({detail}) after it had started, \
and the wrapper completed the merge.\n"
),
}),
Ok(_) => refused(
moved.code.unwrap_or(2),
format!("{}{}", String::from_utf8_lossy(&moved.stderr), not_applied_finding(reference.as_str(), detail.as_str())),
),
Err(error) => refused(2, render_engine_failure_with_hint(0, EngineFailureCode::ContentUnavailable, error.0.as_str(), None)),
});
}
reach_phase(Phase::MergeApplied)?;
if let Err(error) = remove_merge_intent(record.as_path()) {
debug(TAG, format!("the merge record could not be removed: {error}").as_str());
}
record_streams(&["merge", "--ff-only", "--no-edit", new_oid], &moved.stdout, &[]);
let mut stdout: String = String::new();
if !shape.quiet {
stdout.push_str("Merge made by the 'ort' strategy.\n");
if shape.stat.unwrap_or(plan.config.stat)
&& let Ok(stat) = session.run(&["diff", "--stat", "--summary", old_oid, new_oid])
{
stdout.push_str(String::from_utf8_lossy(&stat.stdout).as_ref());
}
}
return Ok(IsolatedMerge::Finished {
code: 0,
stdout,
stderr: String::from_utf8_lossy(&moved.stderr).into_owned(),
});
}
pub fn reach_phase(phase: Phase) -> Result<(), Bail> {
return match reach_transaction_phase(phase, None) {
Ok(()) => Ok(()),
Err(error) => Err(refused(2, format!("monochromatic-git-policy-cli: {error}\n"))),
};
}