use crate::action::{Action, ENGINE_FAILURE_EXIT_CODE, PushForward, failure, with_leading_notice};
#[cfg(unix)]
use crate::auto_push_trigger::{TipWatch, hand_over_if_ahead, try_watch_tip};
use crate::push_trigger_log::{Decision, Origin, Outcome, record_decision};
use crate::announced_query::{WRAPPER_VERSION, announced_query, announcement, printable_after};
use crate::command_push::stackable_option_tokens;
use crate::external_hooks::external_hooks_warning;
use crate::lfs_pre_push::uses_lfs;
use crate::lfs_upload_plan::settings_from_snapshot;
use crate::config_snapshot::{ConfigSnapshot, read_config_snapshot};
use crate::push_stacking::{STACKED_PUSH_MARKER, run_stacked_push};
use crate::command_push::parse_push_region;
use crate::child_environment::{
FORWARD_TARGET_VARIABLE, child_environment_overlay, environment_value,
};
use crate::effective_target::default_allowed_worktree_dirs;
use crate::forwarded_lock_notice::{forwarded_lock_notice, is_covered};
use crate::forwarding::{exit_code, replace_process_with_real_git, run_real_git};
use crate::stale_ref_lock_judge::Judge;
use crate::index_writer_forward::{CoordinatedForward, CoordinatedRun, run_coordinated_forward};
use crate::index_writer_commands::is_index_writer;
use crate::forwarded_command::{ResolvedCommand, Route, creates_worktrees, resolve_forwarded_command};
use crate::invocation_config::{config_invalid_event, load_identity_config};
use crate::repository_location::RepositoryLocation;
use crate::transaction_git::GitContext;
use crate::worktree_identity::WorktreeIdentity;
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, RepositoryFacts, 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::worktree_add_plan::{AddPlan, copy_start_directory, parse_add_plan, parse_copy_destination, start_directory};
use crate::worktree_remove_plan::{RemovePlan, parse_remove_plan};
use crate::worktree_copy_forward::{CopyFacts, ForwardedRun, copy_applies_with, run_forwarded_git};
use crate::wrapper_invocation::{StrippedInvocation, strip_wrapper_controls};
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
pub const MANAGEMENT_COMMAND: &str = "cli-git";
fn forwarded_to_self(own_executable: &Path) -> Action {
return failure(
format!(
"another cli-git wrapper selected this cli-git executable ({}) as real Git. \
Forwarding again would never reach Git. Remove the extra cli-git 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
{
return plan_management_with_section(
&global_clean[layout.prefix_len + 1..],
&global_clean[..layout.prefix_len],
&global_controls,
environment,
inputs,
&host_filesystem_section,
);
}
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!(
"cli-git: 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!(
"cli-git: 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;
}
}
}
fn finish_forwarded(run: ForwardedRun) -> i32 {
match run {
ForwardedRun::Ran(code) => return code,
ForwardedRun::Failed { stderr, code } => {
write_stream(&mut std::io::stderr(), stderr.as_str());
return code;
}
}
}
fn run_coordinated_action(forward: &CoordinatedForward) -> i32 {
let watch: Option<TipWatch> = if adds_commits(&forward.command) { watch_forward(forward) } else { None };
let code: i32 = match run_coordinated_forward(forward) {
CoordinatedRun::Ran(code) => code,
CoordinatedRun::Failed { stderr, code } => {
write_stream(&mut std::io::stderr(), stderr.as_str());
code
}
CoordinatedRun::NotCoordinated if watch.is_some() => {
finish_forwarded(run_forwarded_git(forward.real_git.as_path(), forward.arguments.as_slice(), forward.overlay.as_slice(), None))
}
CoordinatedRun::NotCoordinated => {
let error: std::io::Error = replace_process_with_real_git(
forward.real_git.as_path(),
forward.arguments.as_slice(),
forward.overlay.as_slice(),
);
write_stream(
&mut std::io::stderr(),
format!("cli-git: cannot start {}: {error}\n", forward.real_git.display()).as_str(),
);
ENGINE_FAILURE_EXIT_CODE
}
};
if let Some(found) = watch.as_ref() {
if code == 0 {
hand_over_if_ahead(found, trigger_command(&forward.command).as_str(), &mut std::io::stderr());
} else {
record_decision(&Decision {
common_dir: Some(found.common_dir()),
origin: Some(found.origin()),
command: trigger_command(&forward.command).as_str(),
outcome: Outcome::Declined,
reason: "command-failed",
detail: format!("Git exited with status {code}; {} was at {} before the command", found.branch_ref(), found.tip_before()).as_str(),
branch: Some(found.branch_ref()),
});
}
}
return code;
}
#[cfg(not(unix))]
struct TipWatch;
#[cfg(not(unix))]
fn watch_forward(_forward: &CoordinatedForward) -> Option<TipWatch> {
return None;
}
#[cfg(not(unix))]
fn hand_over_if_ahead(_watch: &TipWatch, _command: &str, _stderr: &mut dyn Write) {}
fn adds_commits(command: &ResolvedCommand) -> bool {
return command.is_builtin() && matches!(command.word(), b"merge" | b"pull" | b"cherry-pick" | b"revert" | b"am");
}
fn trigger_command(command: &ResolvedCommand) -> String {
return String::from_utf8_lossy(command.word()).into_owned();
}
#[cfg(unix)]
fn watch_forward(forward: &CoordinatedForward) -> Option<TipWatch> {
let (root, common): (PathBuf, PathBuf) = match &forward.identity {
WorktreeIdentity::MainWorktree { worktree_root, common_dir, .. } | WorktreeIdentity::LinkedWorktree { worktree_root, common_dir, .. } => {
(worktree_root.clone(), common_dir.clone())
}
WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => return None,
};
return match try_watch_tip(root.as_path(), forward.real_git.as_path(), forward.overlay.as_slice(), forward.environment.as_slice()) {
Ok(watch) => Some(watch),
Err(declined) => {
let git_dir: PathBuf = match &forward.identity {
WorktreeIdentity::MainWorktree { git_dir, .. } | WorktreeIdentity::LinkedWorktree { git_dir, .. } => git_dir.clone(),
WorktreeIdentity::OutsideWorktree | WorktreeIdentity::BareRepository { .. } => common.clone(),
};
record_decision(&Decision {
common_dir: Some(common.as_path()),
origin: Some(Origin { worktree: root.as_path(), git_dir: git_dir.as_path() }),
command: trigger_command(&forward.command).as_str(),
outcome: Outcome::Declined,
reason: declined.reason,
detail: declined.detail.as_str(),
branch: None,
});
None
}
};
}
fn forward_action(
real_git: PathBuf,
arguments: Vec<OsString>,
overlay: Vec<(OsString, OsString)>,
stderr: String,
environment: &[(OsString, OsString)],
copy_bypassed: bool,
checks: &mut ShippedChecks<GitFacts>,
) -> Action {
let context: GitContext = GitContext {
real_git: real_git.clone(),
overlay: overlay.clone(),
global_prefix: Vec::new(),
common_dir: None,
};
let command: ResolvedCommand = resolve_forwarded_command(&context, arguments.as_slice());
let forwarded_arguments: Vec<OsString> = match command.route {
Route::Alias => command.arguments.clone(),
Route::Direct | Route::ShellAlias | Route::Unresolved => arguments,
};
let writes_index: bool = is_index_writer(&command);
if !writes_index && copy_bypassed {
return Action::Forward {
real_git,
arguments: forwarded_arguments,
overlay,
stderr,
};
}
let location: RepositoryLocation = match checks.facts.location() {
Ok(found) => found,
Err(message) => return failure(message.as_str()),
};
if location.identity == WorktreeIdentity::OutsideWorktree {
return Action::Forward {
real_git,
arguments: forwarded_arguments,
overlay,
stderr,
};
}
if configuration_needed(writes_index)
&& let Err(error) = load_identity_config(&location.identity)
{
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr: config_invalid_event(&error),
};
}
let creates: bool = creates_worktrees(&command);
let add_plan: Option<AddPlan> = if creates { plan_worktree_add(&command) } else { None };
let destination: Option<PathBuf> = if creates { plan_copy_destination(&command) } else { None };
let remove_plan: Option<RemovePlan> = if command.word() == b"worktree" { plan_worktree_remove(&command) } else { None };
let serves_ladder: bool = add_plan.is_some() || remove_plan.is_some();
let copy: Option<CopyFacts> =
if !copy_bypassed && copy_applies_with(&location.identity, creates, serves_ladder) {
Some(CopyFacts {
identity: location.identity.clone(),
creates,
destination,
add_plan,
remove_plan,
})
} else {
None
};
if !writes_index {
let Some(facts) = copy else {
return Action::Forward {
real_git,
arguments: forwarded_arguments,
overlay,
stderr,
};
};
return Action::ForwardSettled {
real_git,
arguments: forwarded_arguments,
overlay,
copy: Box::new(facts),
stderr,
};
}
return Action::ForwardCoordinated {
forward: Box::new(CoordinatedForward {
real_git,
arguments: forwarded_arguments,
overlay,
identity: location.identity,
command,
environment: environment.to_vec(),
copy,
}),
stderr,
};
}
fn plan_worktree_add(command: &ResolvedCommand) -> Option<AddPlan> {
let current: PathBuf = std::env::current_dir().ok()?;
let start: PathBuf = start_directory(&command.arguments[..command.subcommand_index], current.as_path())?;
return parse_add_plan(command.arguments.as_slice(), command.subcommand_index, start.as_path());
}
fn plan_copy_destination(command: &ResolvedCommand) -> Option<PathBuf> {
let current: PathBuf = std::env::current_dir().ok()?;
let start: PathBuf = copy_start_directory(&command.arguments[..command.subcommand_index], current.as_path())?;
return parse_copy_destination(command.arguments.as_slice(), command.subcommand_index, start.as_path());
}
fn plan_worktree_remove(command: &ResolvedCommand) -> Option<RemovePlan> {
let current: PathBuf = std::env::current_dir().ok()?;
let start: PathBuf = start_directory(&command.arguments[..command.subcommand_index], current.as_path())?;
return parse_remove_plan(command.arguments.as_slice(), command.subcommand_index, start.as_path());
}
fn needs_lfs_upload(
command: &ResolvedCommand,
real_git: &Path,
overlay: &[(OsString, OsString)],
) -> (bool, String, Option<Vec<(String, String)>>) {
if command.word() != b"push" {
return (false, String::new(), None);
}
let dry_run: bool = parse_push_region(command.region(), &[]).is_ok_and(|region| {
return region.dry_run;
});
let context: GitContext = GitContext {
real_git: real_git.to_path_buf(),
overlay: overlay.to_vec(),
global_prefix: command.arguments[..command.subcommand_index].to_vec(),
common_dir: None,
};
let snapshot: Option<ConfigSnapshot> = match std::env::current_dir() {
Ok(directory) => read_config_snapshot(&context, directory.as_path()),
Err(_) => None,
};
let warning: String = external_hooks_warning(snapshot.as_ref()).unwrap_or_default();
if dry_run {
return (false, warning, None);
}
return match snapshot {
Some(found) => {
let uses: bool = uses_lfs(&found);
let settings: Option<Vec<(String, String)>> = uses.then(|| {
return settings_from_snapshot(&found);
});
(uses, warning, settings)
}
None => (true, warning, None),
};
}
fn stack_if_push(action: Action, environment: &[(OsString, OsString)]) -> Action {
let (real_git, arguments, overlay, copy, stderr) = match action {
Action::Forward {
real_git,
arguments,
overlay,
stderr,
} => (real_git, arguments, overlay, None, stderr),
Action::ForwardSettled {
real_git,
arguments,
overlay,
copy,
stderr,
} => (real_git, arguments, overlay, Some(copy), stderr),
other => return other,
};
let context: GitContext = GitContext {
real_git: real_git.clone(),
overlay: overlay.clone(),
global_prefix: Vec::new(),
common_dir: None,
};
let command: ResolvedCommand = resolve_forwarded_command(&context, arguments.as_slice());
let (lfs, hook_warning, lfs_settings): (bool, String, Option<Vec<(String, String)>>) =
needs_lfs_upload(&command, real_git.as_path(), overlay.as_slice());
let stderr_with_warning: String = format!("{stderr}{hook_warning}");
let stacked_already: bool = environment_value(environment, STACKED_PUSH_MARKER).is_some();
let stack: bool =
!stacked_already && command.word() == b"push" && stackable_option_tokens(command.region()).is_some();
if stack || lfs {
return Action::ForwardPush {
forward: Box::new(PushForward {
real_git,
subcommand_index: command.subcommand_index,
arguments: command.arguments,
overlay,
copy,
environment: environment.to_vec(),
lfs,
lfs_settings,
stack,
}),
stderr: stderr_with_warning,
};
}
let lock_notice: String = if is_covered(command.word()) {
forwarded_lock_notice(command.word(), &command.arguments[..command.subcommand_index], environment, &Judge::here())
} else {
String::new()
};
let stderr_with_locks: String = format!("{stderr_with_warning}{lock_notice}");
let forwarded: Vec<OsString> = arguments;
return match copy {
Some(facts) => Action::ForwardSettled {
real_git,
arguments: forwarded,
overlay,
copy: facts,
stderr: stderr_with_locks,
},
None => Action::Forward {
real_git,
arguments: forwarded,
overlay,
stderr: stderr_with_locks,
},
};
}
fn configuration_needed(writes_index: bool) -> bool {
return writes_index;
}
fn write_stream(stream: &mut dyn Write, text: &str) {
let _ = stream.write_all(text.as_bytes());
}
#[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;