use crate::entry_forward_action_planning::forward_action;
use crate::entry_push_stacking_and_lfs_upload::stack_if_push;
use crate::entry_run_forwarded_actions::{finish_forwarded, run_coordinated_action, write_stream};
use crate::action::{Action, ENGINE_FAILURE_EXIT_CODE, failure, with_leading_notice};
use crate::announced_query::{WRAPPER_VERSION, announced_query, announcement, printable_after};
use crate::push_stacking::{STACKED_PUSH_MARKER, run_stacked_push};
use crate::child_environment::{
FORWARD_TARGET_VARIABLE, child_environment_overlay, environment_value,
};
use crate::effective_target::default_allowed_worktree_dirs;
use crate::forwarding::{exit_code, replace_process_with_real_git, run_real_git};
use crate::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
use crate::management::{host_filesystem_section, plan_management_with_section};
use crate::policy_checks::{ShippedChecks, shipped_checks};
use crate::policy_content::LifecycleContent;
use crate::real_git::{ResolutionInputs, ResolvedGit, process_resolution_inputs, resolve_real_git};
use crate::started_as_git::refusal_when_started_as_git;
use crate::real_git_candidate::same_file;
use crate::repository_facts::{GitFacts, git_facts};
use crate::rule_require_root::{RequireRootDecision, decide_require_root};
use crate::scanner_selection::RULES_VARIABLE;
use crate::wrapped_command::{WrappedOutcome, run_wrapped_command};
use crate::wrapper_controls::{Controls, no_controls, strip_global_controls};
use crate::wrapper_names_old_and_new::COMMAND_WORD;
use crate::worktree_copy_forward::run_forwarded_git;
use crate::wrapper_invocation::{StrippedInvocation, strip_wrapper_controls};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub const MANAGEMENT_COMMAND: &str = COMMAND_WORD.current;
pub const EARLIER_COMMAND_WORD_NOTICE: &str = "monochromatic-git-policy-cli: `git cli-git` is the earlier spelling of this \
command; `git monochromatic-git-policy-cli` does the same, so use that from now on.\n";
fn forwarded_to_self(own_executable: &Path) -> Action {
return failure(
format!(
"another monochromatic-git-policy-cli wrapper selected this monochromatic-git-policy-cli executable ({}) as real Git. \
Forwarding again would never reach Git. Remove the extra monochromatic-git-policy-cli from PATH or \
place the real Git executable on PATH.",
own_executable.display()
)
.as_str(),
);
}
fn home_directory(environment: &[(OsString, OsString)]) -> Option<PathBuf> {
if let Some(home) = environment_value(environment, "HOME")
&& !home.is_empty()
{
return Some(PathBuf::from(home));
}
return None;
}
pub fn plan_invocation(
arguments: &[OsString],
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
) -> Action {
if let Some(target) = environment_value(environment, FORWARD_TARGET_VARIABLE)
&& same_file(Path::new(&target), inputs.own_executable.as_path())
{
return forwarded_to_self(inputs.own_executable.as_path());
}
let mut global_controls: Controls = no_controls();
let global_clean: Vec<OsString> = strip_global_controls(arguments, &mut global_controls);
let layout: GlobalLayout = global_layout(global_clean.as_slice());
if layout.outcome == GlobalOutcome::Command
&& (global_clean[layout.prefix_len] == MANAGEMENT_COMMAND || global_clean[layout.prefix_len] == COMMAND_WORD.earlier)
{
let earlier_spelling: bool = global_clean[layout.prefix_len] == COMMAND_WORD.earlier;
let answer: Action = plan_management_with_section(
&global_clean[layout.prefix_len + 1..],
&global_clean[..layout.prefix_len],
&global_controls,
environment,
inputs,
&host_filesystem_section,
);
if earlier_spelling {
return with_leading_notice(answer, Some(EARLIER_COMMAND_WORD_NOTICE));
}
return answer;
}
let resolved: ResolvedGit = match resolve_real_git(inputs) {
Ok(found) => found,
Err(error) => return failure(error.to_string().as_str()),
};
let notice: Option<String> = if environment_value(environment, STACKED_PUSH_MARKER).is_some() {
None
} else {
resolved.notice
};
let action: Action = plan_with_real_git(
arguments,
environment,
resolved.path,
global_clean,
&layout,
);
return with_leading_notice(action, notice.as_deref());
}
fn plan_with_real_git(
arguments: &[OsString],
environment: &[(OsString, OsString)],
real_git: PathBuf,
global_clean: Vec<OsString>,
layout: &GlobalLayout,
) -> Action {
let overlay: Vec<(OsString, OsString)> =
child_environment_overlay(environment, real_git.as_path());
if let Some(query) = announced_query(global_clean.as_slice(), layout) {
return Action::ForwardAnnounced {
announcement: announcement(query, WRAPPER_VERSION, real_git.as_path()),
real_git,
arguments: global_clean,
overlay,
stderr: String::new(),
};
}
let stripped: StrippedInvocation = strip_wrapper_controls(arguments);
let facts: GitFacts = git_facts(
real_git.as_path(),
&stripped.arguments[..stripped.layout.prefix_len],
overlay.as_slice(),
);
let mut checks: ShippedChecks<GitFacts> = shipped_checks(
facts,
stripped.arguments.clone(),
LifecycleContent::None,
default_allowed_worktree_dirs(
environment,
home_directory(environment).as_deref(),
),
);
checks.scanner_settings.rules_variable = environment_value(environment, RULES_VARIABLE);
match run_wrapped_command(&stripped, environment, &mut checks) {
WrappedOutcome::Forward {
arguments: forwarded,
stderr,
} => {
let copy_bypassed: bool = stripped.controls.skip_worktree_copy
|| stripped.layout.outcome != GlobalOutcome::Command
|| decide_require_root(forwarded.as_slice(), &[])
!= RequireRootDecision::NeedsRepositoryRoot;
return stack_if_push(
forward_action(
real_git,
forwarded,
overlay,
stderr,
environment,
copy_bypassed,
&mut checks,
),
environment,
);
}
WrappedOutcome::Exit { code, stderr } => {
return Action::Exit {
code,
stdout: String::new(),
stderr,
};
}
}
}
pub fn run_process(arguments: &[OsString], environment: &[(OsString, OsString)]) -> i32 {
crate::hooks_off::activate();
let action: Action = match process_resolution_inputs(environment) {
Ok(inputs) => match refusal_when_started_as_git(environment, inputs.own_executable.as_path()) {
Some(refusal) => failure(refusal.as_str()),
None => plan_invocation(arguments, environment, &inputs),
},
Err(error) => failure(
format!(
"cannot determine this executable or the current directory, so real Git \
cannot be selected safely: {error}"
)
.as_str(),
),
};
match action {
Action::Forward {
real_git,
arguments: forwarded,
overlay,
stderr,
} => {
write_stream(&mut std::io::stderr(), stderr.as_str());
let error: std::io::Error = replace_process_with_real_git(
real_git.as_path(),
forwarded.as_slice(),
overlay.as_slice(),
);
write_stream(
&mut std::io::stderr(),
format!(
"monochromatic-git-policy-cli: cannot start {}: {error}. Hint: if you moved or upgraded Git, check that the file exists and is executable with `ls -l {}`, \
then reinstall Git or the wrapper; no Git command ran.\n",
real_git.display(),
real_git.display()
)
.as_str(),
);
return ENGINE_FAILURE_EXIT_CODE;
}
Action::ForwardCoordinated { forward, stderr } => {
write_stream(&mut std::io::stderr(), stderr.as_str());
return run_coordinated_action(&forward);
}
Action::ForwardAnnounced {
real_git,
arguments: forwarded,
overlay,
announcement,
stderr,
} => {
write_stream(&mut std::io::stderr(), stderr.as_str());
return run_announced(
real_git.as_path(),
forwarded.as_slice(),
overlay.as_slice(),
announcement.as_str(),
);
}
Action::ForwardSettled {
real_git,
arguments: forwarded,
overlay,
copy,
stderr,
} => {
write_stream(&mut std::io::stderr(), stderr.as_str());
return finish_forwarded(run_forwarded_git(
real_git.as_path(),
forwarded.as_slice(),
overlay.as_slice(),
Some(&*copy),
));
}
Action::ForwardPush { forward, stderr } => {
write_stream(&mut std::io::stderr(), stderr.as_str());
return run_stacked_push(&forward);
}
Action::Exit {
code,
stdout,
stderr,
} => {
write_stream(&mut std::io::stdout(), stdout.as_str());
write_stream(&mut std::io::stderr(), stderr.as_str());
return code;
}
}
}
fn run_announced(
real_git: &Path,
arguments: &[OsString],
overlay: &[(OsString, OsString)],
text: &str,
) -> i32 {
match run_real_git(real_git, arguments, overlay) {
Ok(outcome) => {
write_stream(&mut std::io::stdout(), printable_after(outcome, text));
return exit_code(outcome);
}
Err(error) => {
write_stream(
&mut std::io::stderr(),
format!(
"monochromatic-git-policy-cli: cannot start {}: {error}. Hint: if you moved or upgraded Git, check that the file exists and is executable with `ls -l {}`, \
then reinstall Git or the wrapper; no Git command ran.\n",
real_git.display(),
real_git.display()
)
.as_str(),
);
return ENGINE_FAILURE_EXIT_CODE;
}
}
}
#[cfg(test)]
#[path = "entry_command_word_tests.rs"]
mod command_word_tests;
#[cfg(test)]
#[path = "entry_announced_tests.rs"]
mod announced_tests;
#[cfg(test)]
#[path = "entry_fallback_tests.rs"]
mod fallback_tests;
#[cfg(test)]
#[path = "entry_stop_tests.rs"]
mod stop_tests;
#[cfg(test)]
#[path = "entry_tests.rs"]
mod tests;