use crate::action::ENGINE_FAILURE_EXIT_CODE;
use crate::auto_push_single_flight::PushGit;
use crate::child_environment::environment_with_overlay;
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::discard_audit::{AuditOutcome, record_audit};
use crate::global_arguments::GlobalOutcome;
use crate::invocation_config::{config_invalid_event, load_identity_config};
use crate::manual_push_cause::ManualPushCause;
use crate::manual_push_lifecycle::manual_push_scope;
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::landed_summary::short_name;
use crate::push_failure_notice::{ORPHAN_NOTICE_DIRECTORY, is_machine_read, notice_directory, take_orphaned_failures, take_push_failures_for};
use crate::worktree_removal_rescue::{settle_removals, snapshot_linked_worktrees};
use crate::policy_trigger::Trigger;
use crate::rule_linked_worktree::{LinkedWorktreeDecision, decide_linked_worktree};
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_controls::is_escaped;
use crate::wrapper_invocation::{StrippedInvocation, command_region, command_word};
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
#[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!("cli-git: {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 record_escape_use<F: RepositoryFacts>(stripped: &StrippedInvocation, checks: &mut ShippedChecks<F>) {
if !is_escaped(&stripped.controls, PolicyId::LinkedWorktreeOnly) {
return;
}
let command = match decide_linked_worktree(stripped.arguments.as_slice()) {
LinkedWorktreeDecision::Pass => return,
LinkedWorktreeDecision::NeedsEffectiveTarget(command) | LinkedWorktreeDecision::WhenDirty(command) => command,
};
if let Ok(location) = checks.facts.location() {
record_audit(&location.identity, command, AuditOutcome::Hatch, stripped.arguments.as_slice(), None);
}
}
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,
});
}
}
}
fn show_push_failures<F: RepositoryFacts>(stripped: &StrippedInvocation, checks: &mut ShippedChecks<F>) {
if is_machine_read(command_word(stripped), command_region(stripped)) {
return;
}
let Some(git_dir) = checks.facts.known_git_dir() else { return };
let common_dir: PathBuf = checks.facts.known_common_dir().unwrap_or_else(|| { return git_dir.clone(); });
for problem in settle_removals(common_dir.as_path()) {
let _written = writeln!(std::io::stderr(), "{problem}");
}
let own: bool = notice_directory(git_dir.as_path()).is_dir();
let orphaned: bool = common_dir.join(ORPHAN_NOTICE_DIRECTORY).is_dir();
if !own && !orphaned {
return;
}
let context = checks.facts.transaction_context();
let cwd: PathBuf = std::env::current_dir().unwrap_or_else(|_| { return git_dir.clone(); });
let objects: PathBuf = common_dir.join("objects");
let abbreviate = |commit: &str| -> String { return short_name(&context, cwd.as_path(), objects.as_path(), None, commit); };
let mut texts: Vec<String> = take_push_failures_for(git_dir.as_path(), None, &abbreviate);
texts.extend(take_orphaned_failures(common_dir.as_path(), &abbreviate));
for text in texts {
let _written = writeln!(std::io::stderr(), "{text}");
}
}
fn snapshot_before_worktree_removal<F: RepositoryFacts>(stripped: &StrippedInvocation, checks: &mut ShippedChecks<F>) {
if command_word(stripped) != b"worktree" {
return;
}
let removes: bool = command_region(stripped).first().is_some_and(|word| { return matches!(word.as_encoded_bytes(), b"remove" | b"prune"); });
if !removes {
return;
}
if let Some(common_dir) = checks.facts.known_common_dir() {
for problem in snapshot_linked_worktrees(common_dir.as_path()) {
let _written = writeln!(std::io::stderr(), "{problem}");
}
}
}
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())),
};
}
fn prepare_manual_push<F: RepositoryFacts>(
checks: &mut ShippedChecks<F>,
environment: &[(OsString, OsString)],
post_subcommand: &[OsString],
) -> Result<(), crate::manual_push_lifecycle::ManualPushFailure> {
let worktree: Option<PathBuf> = match checks.facts.location() {
Ok(location) => worktree_root(&location.identity).map(Path::to_path_buf),
Err(message) => return Err(crate::manual_push_lifecycle::ManualPushFailure::step(ManualPushCause::SelfExplained, message)),
};
let context = checks.facts.transaction_context();
let cwd = match worktree {
Some(root) => root,
None => std::env::current_dir().map_err(|error| {
return crate::manual_push_lifecycle::ManualPushFailure::step(ManualPushCause::SelfExplained, format!(
"the working directory could not be read: {error}"
));
})?,
};
let git = PushGit {
real_git: context.real_git,
cwd,
environment: environment_with_overlay(environment, context.overlay.as_slice()),
lfs: false,
};
let scope = manual_push_scope(&git, context.global_prefix.as_slice(), post_subcommand)?;
checks.candidates = LifecycleContent::Requested(CandidateRequest::Pushed(scope));
return Ok(());
}
#[cfg(test)]
#[path = "wrapped_command_tests.rs"]
mod tests;