use crate::wrapped_command_notices_snapshots_and_manual_push::{prepare_manual_push, record_escape_use, show_push_failures, snapshot_before_worktree_removal};
use crate::action::ENGINE_FAILURE_EXIT_CODE;
use crate::candidate_prediction::CandidateRequest;
use crate::command_options::OptionError;
use crate::command_push::{PushRegion, parse_push_region};
use crate::config_loading::{ConfigLoading, classify_config_loading};
use crate::command_commit::{CommitRegion, parse_commit_region};
use crate::commit_command::{CommitCommand, run_commit_command};
use crate::config_schema::{CliGitConfig, PolicyConfig};
use crate::diagnostics::EngineFailureCode;
use crate::global_arguments::GlobalOutcome;
use crate::invocation_config::{config_invalid_event, load_identity_config};
use crate::policy_checks::ShippedChecks;
use crate::policy_content::LifecycleContent;
use crate::policy_engine::{
StageEnd, StageRequest, StageResult, any_policy_applies, pass_exit_code, run_policy_stage,
};
use crate::event_hints::manual_push_failure_hint;
use crate::policy_events::{EngineFailureHint, PolicyEvent, render_policy_events};
use crate::policy_pass::{PassResult, policies_of_kind, run_policy_pass};
use crate::policy_registry::PolicyId;
use crate::policy_trigger::Trigger;
use crate::repository_facts::RepositoryFacts;
use crate::repository_location::RepositoryLocation;
use crate::startup_gate::{Gate, recover_for_command};
use crate::unported::{Unported, unported_from_unavailable, unported_notice};
use crate::worktree_identity::worktree_root;
use crate::wrapper_invocation::{StrippedInvocation, command_region, command_word};
use std::ffi::OsString;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WrappedOutcome {
Forward {
arguments: Vec<OsString>,
stderr: String,
},
Exit {
code: i32,
stderr: String,
},
}
fn refused(events: String, what: &Unported, command: &str) -> WrappedOutcome {
let mut stderr: String = events;
stderr.push_str(unported_notice(what, command).as_str());
return WrappedOutcome::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stderr,
};
}
fn stage_ending(events: &[PolicyEvent], end: StageEnd, command: &str) -> Option<WrappedOutcome> {
let rendered: String = render_policy_events(0, events);
if let StageEnd::Unavailable(unavailable) = end {
return Some(refused(
rendered,
&unported_from_unavailable(unavailable),
command,
));
}
let code: i32 = pass_exit_code(events, end);
if code == 0 {
return None;
}
return Some(WrappedOutcome::Exit {
code,
stderr: rendered,
});
}
fn push_is_dry_run(region: &[OsString]) -> bool {
let parsed: Result<PushRegion, OptionError> = parse_push_region(region, &[]);
match parsed {
Ok(found) => return found.dry_run,
Err(_) => return false,
}
}
fn every_policy() -> Vec<PolicyId> {
let mut policies: Vec<PolicyId> = policies_of_kind(false);
policies.extend(policies_of_kind(true));
return policies;
}
fn prepare_guarded_command<F: RepositoryFacts>(
stripped: &StrippedInvocation,
checks: &mut ShippedChecks<F>,
) -> Result<CliGitConfig, WrappedOutcome> {
let location: RepositoryLocation = match checks.facts.location() {
Ok(found) => found,
Err(message) => {
return Err(WrappedOutcome::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stderr: format!("monochromatic-git-policy-cli: {message}\n"),
});
}
};
match recover_for_command(&checks.facts.transaction_context(), &location.identity) {
Gate::Clear => {}
Gate::Stop(stderr) => {
return Err(WrappedOutcome::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stderr,
});
}
}
let config: CliGitConfig = match load_identity_config(&location.identity) {
Ok(loaded) => loaded.config,
Err(error) => {
return Err(WrappedOutcome::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stderr: config_invalid_event(&error),
});
}
};
checks.scanner_settings.options = config.policies.forbidden_strings.clone();
checks.markdown.options = config.policies.markdown_autofix.clone();
checks.discarded_limits = config.discarded;
if command_word(stripped) == b"add" && worktree_root(&location.identity).is_some() {
checks.candidates = LifecycleContent::Requested(CandidateRequest::Add(
command_region(stripped).to_vec(),
));
}
return Ok(config);
}
fn run_commit_if_real<F: RepositoryFacts>(
stripped: &StrippedInvocation,
environment: &[(OsString, OsString)],
checks: &mut ShippedChecks<F>,
config: &CliGitConfig,
) -> Option<WrappedOutcome> {
if command_word(stripped) != b"commit" {
return None;
}
let region: CommitRegion = match parse_commit_region(command_region(stripped), &[]) {
Ok(found) => found,
Err(_) => return None,
};
if region.dry_run {
return None;
}
let context = checks.facts.transaction_context();
let ending = run_commit_command(
&CommitCommand {
context: &context,
environment,
arguments: stripped.arguments.as_slice(),
subcommand_index: stripped.layout.prefix_len,
region: ®ion,
config,
controls: &stripped.controls,
},
checks,
);
return Some(WrappedOutcome::Exit {
code: ending.code,
stderr: String::new(),
});
}
fn recover_before_read_only<F: RepositoryFacts>(
checks: &ShippedChecks<F>,
) -> Option<WrappedOutcome> {
let Some(Ok(location)) = checks.facts.asked_location() else {
return None;
};
match recover_for_command(&checks.facts.transaction_context(), &location.identity) {
Gate::Clear => return None,
Gate::Stop(stderr) => {
return Some(WrappedOutcome::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stderr,
});
}
}
}
pub fn run_wrapped_command<F: RepositoryFacts>(
stripped: &StrippedInvocation,
environment: &[(OsString, OsString)],
checks: &mut ShippedChecks<F>,
) -> WrappedOutcome {
if stripped.layout.outcome != GlobalOutcome::Command {
return WrappedOutcome::Forward {
arguments: stripped.arguments.clone(),
stderr: String::new(),
};
}
let command: String = String::from_utf8_lossy(command_word(stripped)).into_owned();
let skips_config: bool =
classify_config_loading(stripped.arguments.as_slice()) == ConfigLoading::Skip;
let loaded: Option<CliGitConfig> = if skips_config {
None
} else {
match prepare_guarded_command(stripped, checks) {
Ok(found) => Some(found),
Err(ending) => return ending,
}
};
show_push_failures(stripped, checks);
snapshot_before_worktree_removal(stripped, checks);
if let Some(config) = &loaded
&& let Some(ending) = run_commit_if_real(stripped, environment, checks, config)
{
return ending;
}
let policies: PolicyConfig = match loaded {
Some(config) => config.policies,
None => PolicyConfig::defaults(),
};
record_escape_use(stripped, checks);
let request: StageRequest = StageRequest {
trigger: Trigger::PreForward,
config: policies,
controls: stripped.controls.clone(),
selected: Vec::<PolicyId>::new(),
};
let pass: PassResult = run_policy_pass(&request, checks);
if let Some(ending) = stage_ending(pass.events.as_slice(), pass.end, command.as_str()) {
return ending;
}
if skips_config
&& let Some(ending) = recover_before_read_only(checks)
{
return ending;
}
let mut events: Vec<PolicyEvent> = pass.events;
if command_word(stripped) == b"push" && !push_is_dry_run(command_region(stripped)) {
let gate_request: StageRequest = StageRequest {
trigger: Trigger::ManualPush,
config: request.config,
controls: request.controls,
selected: request.selected,
};
let gated: Vec<PolicyId> = every_policy();
if any_policy_applies(&gate_request, gated.as_slice()) {
if let Err(failure) = prepare_manual_push(checks, environment, command_region(stripped)) {
if let Some(refusal) = failure.refusal {
return WrappedOutcome::Exit {
code: refusal.code,
stderr: refusal.stderr,
};
}
events.push(PolicyEvent::EngineFailure {
code: EngineFailureCode::ContentUnavailable,
hint: EngineFailureHint::Chosen(manual_push_failure_hint(failure.cause)),
message: failure.message,
trigger: Some(Trigger::ManualPush),
policy: None,
path: None,
});
return WrappedOutcome::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stderr: render_policy_events(0, events.as_slice()),
};
}
let gate: StageResult = run_policy_stage(&gate_request, gated.as_slice(), checks);
events.extend(gate.events);
if let Some(ending) = stage_ending(events.as_slice(), gate.end, command.as_str()) {
return ending;
}
}
}
return WrappedOutcome::Forward {
arguments: pass.arguments,
stderr: format!("{}{}", checks.notices.concat(), render_policy_events(0, events.as_slice())),
};
}
#[cfg(test)]
#[path = "wrapped_command_tests.rs"]
mod tests;