use crate::commit_timing::CommitTiming;
use crate::service_commit::{ServiceCommit, service_commit};
use crate::service_handoff::{ServiceHandoff, hand_over};
use crate::service_messages::render;
use crate::commit_transaction::{CommitInvocation, TransactionStop};
use crate::config_schema::CliGitConfig;
use crate::command_commit::CommitRegion;
use crate::diagnostics::{EngineFailureCode, render_engine_failure};
use crate::git_version::check_git_version;
use crate::policy_checks::ShippedChecks;
use crate::policy_engine::StageRequest;
use crate::policy_events::{PolicyEvent, render_policy_events};
use crate::policy_trigger::Trigger;
use crate::post_commit::{POST_COMMIT_BLOCKED_EXIT_CODE, PostCommitRequest, run_post_commit_gate};
use crate::repository_facts::RepositoryFacts;
use crate::transaction_git::GitContext;
use crate::wrapper_controls::Controls;
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Instant;
pub fn effective_directory(start: &Path, global_prefix: &[OsString]) -> PathBuf {
let mut directory: PathBuf = start.to_path_buf();
let mut index: usize = 0;
while index < global_prefix.len() {
if global_prefix[index] == "-C"
&& let Some(value) = global_prefix.get(index + 1)
{
directory = directory.join(value);
index += 2;
continue;
}
index += 1;
}
return directory;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CommitEnding {
pub code: i32,
}
fn write_stderr(text: &str) {
let mut stream = std::io::stderr();
let _ = stream.write_all(text.as_bytes());
let _ = stream.flush();
}
fn report_stop(stop: TransactionStop) -> CommitEnding {
match stop {
TransactionStop::NativeExit(code) => return CommitEnding { code },
TransactionStop::Message(message) => {
write_stderr(format!("{message}\n").as_str());
return CommitEnding { code: 1 };
}
TransactionStop::Failed { code, message } => {
let event: PolicyEvent = PolicyEvent::EngineFailure {
code,
hint: crate::policy_events::EngineFailureHint::OfCode,
message,
trigger: Some(Trigger::PreForward),
policy: None,
path: None,
};
write_stderr(render_policy_events(0, std::slice::from_ref(&event)).as_str());
return CommitEnding { code: 2 };
}
}
}
pub struct CommitCommand<'a> {
pub context: &'a GitContext,
pub environment: &'a [(OsString, OsString)],
pub arguments: &'a [OsString],
pub subcommand_index: usize,
pub region: &'a CommitRegion,
pub config: &'a CliGitConfig,
pub controls: &'a Controls,
}
pub fn run_commit_command<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>) -> CommitEnding {
let mut timing: CommitTiming = CommitTiming::start();
let ending: CommitEnding = run_timed(command, checks, &mut timing);
timing.finish(ending.code);
return ending;
}
fn run_timed<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>, timing: &mut CommitTiming) -> CommitEnding {
if let Err(reason) = check_git_version(command.context) {
write_stderr(render_engine_failure(0, EngineFailureCode::ContentUnavailable, reason.as_str()).as_str());
return CommitEnding { code: 2 };
}
let start: PathBuf = std::env::current_dir().unwrap_or_else(|_| { return PathBuf::from("."); });
let cwd: PathBuf = effective_directory(start.as_path(), command.context.global_prefix.as_slice());
let invocation: CommitInvocation<'_> = CommitInvocation {
context: command.context,
environment: command.environment,
cwd: cwd.as_path(),
arguments: command.arguments,
subcommand_index: command.subcommand_index,
region: command.region,
config: command.config,
controls: command.controls,
};
let request: StageRequest = StageRequest {
trigger: Trigger::PreForward,
config: command.config.policies.clone(),
controls: command.controls.clone(),
selected: Vec::new(),
};
let served: ServiceCommit = match service_commit(&invocation, checks, &request, timing) {
Ok(found) => found,
Err(stop) => return report_stop(stop),
};
write_stderr(render_policy_events(0, served.result.events.as_slice()).as_str());
let (Some(landed), Some(handoff)) = (served.result.landed_oid, served.handoff) else {
timing.outcome = "ended";
return CommitEnding { code: served.result.exit_code };
};
timing.outcome = "landed";
return post_commit(command, &handoff, landed.as_str(), timing);
}
fn post_commit(command: &CommitCommand<'_>, handoff: &ServiceHandoff, landed: &str, timing: &mut CommitTiming) -> CommitEnding {
let request: PostCommitRequest<'_> = PostCommitRequest {
real_git: command.context.real_git.as_path(),
landed_oid: landed,
arguments: command.arguments,
global_prefix: command.context.global_prefix.as_slice(),
overlay: command.context.overlay.as_slice(),
environment: command.environment,
config: command.config.policies.clone(),
controls: command.controls.clone(),
};
let mut stderr = std::io::stderr();
let gating: Instant = Instant::now();
let gate = run_post_commit_gate(&request);
timing.gate += gating.elapsed();
write_stderr(render_policy_events(0, gate.events.as_slice()).as_str());
if gate.blocked {
return CommitEnding { code: POST_COMMIT_BLOCKED_EXIT_CODE };
}
let handing_over: Instant = Instant::now();
let handed = hand_over(handoff, landed, &mut stderr);
timing.handoff += handing_over.elapsed();
if let Err(failure) = handed {
write_stderr(format!("{}\n", render(&failure)).as_str());
}
return CommitEnding { code: 0 };
}
#[cfg(test)]
#[path = "commit_command_tests.rs"]
mod tests;