use crate::service_commit_input_notes_and_embedded_commit::managed_client_config;
use crate::service_server::Bind;
use crate::service_provision::ProvisionHost;
use crate::service_commit_input_notes_and_embedded_commit::route_for_this_worktree;
use crate::service_commit_input_notes_and_embedded_commit::{Embedded, arguments_after_subcommand, embedded_commit, standard_input_for, switched_branch_name};
use crate::service_commit_requests_and_landing_report::{report_landing, request_for_embedded, request_for_git_built};
use crate::commit_timing::CommitTiming;
use crate::private_index_request::indexed_request;
use crate::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, failed};
use crate::diagnostic_log::warn;
use crate::policy_checks::ShippedChecks;
use crate::policy_engine::StageRequest;
use crate::random_id::random_uuid;
use crate::repository_facts::RepositoryFacts;
use crate::scratch_location::SCRATCH_REFRESH_EVERY;
use crate::service_client::{ClientConfig, FreshScratch, ScratchWatch, Submitted, submit_traced, watch_scratch};
use crate::service_commit_args::{opens_editor, parse_service_commit, with_fixed_message};
use crate::service_handoff::ServiceHandoff;
use crate::service_land::LANDING_ATTEMPTS;
use crate::service_messages::{ClientFailure, render};
use crate::service_native::{BuildInput, GitBuilt, build_with_git};
use crate::service_prepare::{NativeInputs, PrepareInputs, Prepared, Ready, prepare};
use crate::service_replay::{Captured, Retry, conflict_result, retry_of};
use crate::service_protocol::CommitRequest;
use crate::service_provision::provision;
use crate::service_pruned::{content_was_removed, prepared_content_missing, reach_service_phase};
use crate::transaction_phase::Phase;
use crate::transaction_git::run_git_checked;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::Instant;
pub struct ServiceCommit {
pub result: TransactionResult,
pub handoff: Option<ServiceHandoff>,
}
pub const REBUILD_ATTEMPTS: usize = 5;
fn branch_moved(ready: &Ready) -> TransactionStop {
let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
return TransactionStop::Message(render(&ClientFailure::BranchMoved { branch }));
}
fn display_text(file: &Path) -> String {
return file.display().to_string();
}
pub fn service_commit<F: RepositoryFacts>(
invocation: &CommitInvocation<'_>,
checks: &mut ShippedChecks<F>,
request: &StageRequest,
timing: &mut CommitTiming,
) -> Result<ServiceCommit, TransactionStop> {
let (address, provisioning): (Bind, Option<ProvisionHost>) = route_for_this_worktree(invocation, timing)?;
let after: &[OsString] = arguments_after_subcommand(invocation.arguments, invocation.subcommand_index);
let id: String = match random_uuid() {
Ok(made) => made,
Err(error) => return Err(failed(format!("an identity for this commit could not be made: {error:?}"))),
};
timing.request = Some(id.clone());
let standard_input: Option<Vec<u8>> = standard_input_for(invocation)?;
let mut provisioned: bool = false;
let editor_possible: bool = opens_editor(after);
let config: ClientConfig = managed_client_config(address, provisioning.as_ref());
let scratch_name: String = format!("monochromatic-git-policy-cli-service-{id}");
let mut embedded: Option<Embedded> = None;
let mut decided: bool = false;
let mut fixed_message: Option<PathBuf> = None;
let mut last_message: Option<String> = None;
let mut retry: Option<Captured> = None;
let mut first_reference: Option<String> = None;
let mut scratch_watch: Option<ScratchWatch> = None;
let mut unlanded: Vec<String> = Vec::new();
for attempt in 0..REBUILD_ATTEMPTS {
let mut replay: Option<Retry<'_>> = None;
if let Some(captured) = retry.as_ref() {
replay = Some(Retry { captured, attempt: u64::try_from(attempt).unwrap_or(u64::MAX) });
}
let inputs: PrepareInputs<'_> = PrepareInputs {
scratch_name: scratch_name.as_str(),
allow_empty: invocation.region.allow_empty,
standard_input: standard_input.as_deref(),
first: attempt == 0,
replay,
};
let preparing: Instant = Instant::now();
let prepared: Result<Prepared, TransactionStop> = prepare(invocation, checks, request, &inputs);
timing.prepare += preparing.elapsed();
timing.attempts += 1;
let ready: Box<Ready> = match prepared? {
Prepared::Conflict(conflict) => {
let saved: PathBuf = conflict.git_dir.join(format!("monochromatic-git-policy-cli-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
let mut kept: Option<&PathBuf> = None;
if let Some(text) = last_message.as_ref() {
match std::fs::write(&saved, text.as_str()) {
Ok(()) => kept = Some(&saved),
Err(error) => warn("serviceCommit", format!("saving the message failed: {error}").as_str()),
}
}
let result: TransactionResult = conflict_result(&conflict, kept.map(PathBuf::as_path));
return Ok(ServiceCommit { result, handoff: None });
}
Prepared::Ended(result) => {
if content_was_removed(result.events.as_slice()) {
return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
}
return Ok(ServiceCommit { result, handoff: None });
}
Prepared::Ready(ready) => ready,
};
let building: Instant = Instant::now();
if timing.reference.is_none() {
timing.reference = Some(ready.reference.clone());
}
if first_reference.is_none() {
first_reference = Some(ready.reference.clone());
}
let first_branch: String = match first_reference.as_ref() {
Some(known) => known.clone(),
None => ready.reference.clone(),
};
if first_branch != ready.reference {
let failure: ClientFailure = ClientFailure::BranchSwitched {
branch: switched_branch_name(first_branch.as_str()),
now: switched_branch_name(ready.reference.as_str()),
file: fixed_message.as_deref().map(display_text),
};
return Err(TransactionStop::Message(render(&failure)));
}
if scratch_watch.is_none() {
scratch_watch = watch_scratch(&config, ready.inputs.capture.layout.git_dir.as_path(), ready.scratch.0.as_path());
}
if !decided {
decided = true;
if !ready.needs_native
&& let Some(parsed) = parse_service_commit(after)
{
embedded = embedded_commit(invocation, parsed)?;
}
}
let native_inputs: &NativeInputs = &ready.inputs;
let mut tree: Option<String> = None;
if !ready.needs_native
&& let Some(made) = embedded.as_ref()
&& let Some(source) = made.source.as_ref()
{
if source.base != ready.base {
return Err(branch_moved(&ready));
}
let written = run_git_checked(
invocation.context,
&indexed_request(
invocation.cwd,
&["write-tree"],
Some(native_inputs.commit_index.as_path()),
Some(native_inputs.capture.layout.object_directory.as_path()),
),
)?;
let written_tree: String = String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string();
let against: &str = source.parent_tree.as_deref().unwrap_or(native_inputs.capture.layout.empty_tree_oid.as_str());
if source.parents.len() < 2 && !invocation.region.allow_empty && written_tree == against {
embedded = None;
} else {
tree = Some(written_tree);
}
}
let native: bool = ready.needs_native || embedded.is_none();
let embedded_in_use: Option<&Embedded> = if native { None } else { embedded.as_ref() };
let mut typed: Option<String> = None;
let (sent, first_parent): (CommitRequest, String) = match embedded_in_use {
None => {
let layout_prefix: usize = crate::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
let rebuilt_arguments: Option<Vec<OsString>> = match fixed_message.as_deref() {
Some(file) => with_fixed_message(native_inputs.arguments.as_slice(), layout_prefix, file),
None => None,
};
let built: GitBuilt = build_with_git(&BuildInput {
context: invocation.context,
cwd: invocation.cwd,
capture: &native_inputs.capture,
scratch: ready.scratch.0.as_path(),
commit_index: native_inputs.commit_index.as_path(),
arguments: rebuilt_arguments.as_deref().unwrap_or(native_inputs.arguments.as_slice()),
pathspecs: native_inputs.pathspecs.as_slice(),
mode: native_inputs.mode,
staged_all: native_inputs.staged_all,
selected_privately: native_inputs.selected_privately,
})?;
let parent: String = built.parents.first().cloned().unwrap_or_default();
typed = Some(built.message.clone());
let sent: CommitRequest = request_for_git_built(invocation, &ready, native_inputs, id.as_str(), built.commit);
(sent, parent)
}
Some(made) => {
let sent: CommitRequest = request_for_embedded(&ready, native_inputs, id.as_str(), made, &tree);
let parent: String = match made.source.as_ref() {
Some(replaced) => replaced.parents.first().cloned().unwrap_or_default(),
None => ready.base.clone(),
};
(sent, parent)
}
};
let mut remembered: Option<String> = typed.clone();
if remembered.is_none()
&& let Some(made) = embedded_in_use
{
remembered = Some(made.message.clone());
}
last_message = remembered;
timing.build += building.elapsed();
reach_service_phase(Phase::ServicePrepared)?;
if !provisioned
&& let Some(host) = provisioning.as_ref()
{
let provisioning_began: Instant = Instant::now();
let provided = provision(host);
timing.provision += provisioning_began.elapsed();
if let Err(failure) = provided {
return Err(TransactionStop::Message(render(&failure)));
}
provisioned = true;
}
let waiting_config: ClientConfig = ClientConfig {
keep_fresh: Some(FreshScratch { path: ready.scratch.0.clone(), every: SCRATCH_REFRESH_EVERY }),
..config.clone()
};
match submit_traced(&waiting_config, &sent, &mut std::io::stderr(), &mut timing.submit) {
Ok(Submitted::Landed { commit, .. }) => {
let after_landing: Instant = Instant::now();
report_landing(invocation, &ready, native_inputs, commit.as_str(), first_parent.as_str(), fixed_message.as_ref(), embedded_in_use, native, unlanded.as_slice())?;
let handoff: ServiceHandoff = ServiceHandoff {
config: config.clone(),
repository: ready.repository.clone(),
git_dir: native_inputs.capture.layout.git_dir.clone(),
reference: ready.reference.clone(),
parent: first_parent,
};
let result: TransactionResult = TransactionResult { events: ready.events, exit_code: 0, landed_oid: Some(commit), uses_lfs: ready.uses_lfs };
timing.after_landing += after_landing.elapsed();
return Ok(ServiceCommit { result, handoff: Some(handoff) });
}
Ok(Submitted::PrepareAgain { landed, .. }) => {
if native || sent.amend {
return Err(branch_moved(&ready));
}
if attempt + 1 == LANDING_ATTEMPTS {
let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: LANDING_ATTEMPTS })));
}
retry = Some(retry_of(invocation.context, invocation.cwd, &ready, landed)?);
}
Ok(Submitted::Moved) => {
let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
if native && !sent.amend && !editor_possible && !ready.interactive && !ready.fixed_parents {
if attempt + 1 == REBUILD_ATTEMPTS {
return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: REBUILD_ATTEMPTS })));
}
retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
unlanded.extend(sent.commit.clone());
continue;
}
if native && editor_possible && let Some(text) = typed.as_deref() {
let file: PathBuf = native_inputs.capture.layout.git_dir.join(format!("monochromatic-git-policy-cli-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
match std::fs::write(&file, text) {
Ok(()) => {
let prefix: usize = crate::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
let repeatable: bool = !sent.amend
&& !ready.interactive
&& !ready.fixed_parents
&& with_fixed_message(native_inputs.arguments.as_slice(), prefix, file.as_path()).is_some();
if repeatable && attempt + 1 < REBUILD_ATTEMPTS {
fixed_message = Some(file);
retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
unlanded.extend(sent.commit.clone());
continue;
}
let failure: ClientFailure = ClientFailure::BranchMovedMessageSaved { branch, file: file.display().to_string(), amend: sent.amend };
return Err(TransactionStop::Message(render(&failure)));
}
Err(error) => warn("serviceCommit", format!("saving the typed message failed: {error}").as_str()),
}
}
return Err(branch_moved(&ready));
}
Err(ClientFailure::Unavailable)
if prepared_content_missing(
native_inputs.capture.layout.common_dir.as_path(),
native_inputs.capture.layout.object_directory.as_path(),
native_inputs.capture.layout.object_format.as_str(),
&sent,
) =>
{
return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
}
Err(failure) => return Err(TransactionStop::Message(render(&failure))),
}
}
return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch: String::from("the branch"), times: REBUILD_ATTEMPTS })));
}
#[cfg(test)]
#[path = "service_commit_tests.rs"]
mod tests;