use crate::action::{Action, ENGINE_FAILURE_EXIT_CODE, failure, with_leading_notice};
use crate::candidate_prediction::CandidateRequest;
use crate::child_environment::{child_environment_overlay, environment_value};
use crate::config_error::ConfigError;
use crate::config_file::LoadedConfig;
use crate::diagnostics::EngineFailureCode;
use crate::direct_fix::run_direct_fix;
use crate::doctor::doctor_action_with_section;
use crate::invocation_config::{config_invalid_event, legacy_warning_events, load_identity_config};
use crate::git_system_config::with_real_git;
use crate::repo_read_layout::read_layout_after_success_for;
use crate::service_route::service_status;
use crate::management_arguments::{
MANAGEMENT_HELP, MANAGEMENT_USAGE, ManagementAction, ManagementRefusal, RetiredCommand,
parse_management_arguments,
};
use crate::management_selection::{selected_policies, selection_error};
use crate::policy_checks::{ShippedChecks, shipped_checks};
use crate::policy_content::LifecycleContent;
use crate::policy_engine::{StageEnd, StageRequest, pass_exit_code};
use crate::policy_events::{PolicyEvent, render_policy_events};
use crate::policy_pass::{PassResult, run_policy_pass};
use crate::policy_registry::PolicyId;
use crate::policy_trigger::Trigger;
use crate::real_git::{ResolutionInputs, ResolvedGit, resolve_real_git};
use crate::repository_facts::{GitFacts, RepositoryFacts, git_facts};
use crate::repository_location::RepositoryLocation;
use crate::scanner_selection::RULES_VARIABLE;
use crate::startup_gate::{Gate, recover_for_command};
use crate::transaction_git::GitContext;
use crate::subvolume_arguments::MigrateArguments;
use crate::worktree_convert_arguments::ConvertArguments;
#[cfg(target_os = "linux")]
use crate::subvolume_command::migrate_action;
use crate::unported::{unported_from_unavailable, unported_notice};
use crate::worktree_identity::worktree_root;
use crate::wrapper_controls::Controls;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
fn direct_name(fix: bool) -> &'static str {
if fix {
return "fix";
}
return "check";
}
fn direct_command(fix: bool) -> String {
return format!("cli-git {}", direct_name(fix));
}
fn retired_name(command: RetiredCommand) -> &'static str {
match command {
RetiredCommand::Trust => return "trust",
RetiredCommand::Untrust => return "untrust",
RetiredCommand::Status => return "status",
}
}
pub fn retired_explanation(command: RetiredCommand) -> String {
return format!(
"git cli-git {} is retired. cli-git now reads cli-git.config.jsonc as data and runs no \
repository-supplied code, so there is no code execution to approve, revoke or report. \
Nothing was changed; existing trust records are left in place and are no longer read.\n",
retired_name(command)
);
}
fn refusal(reason: ManagementRefusal) -> Action {
let stderr: String = match reason {
ManagementRefusal::Usage => String::from(MANAGEMENT_USAGE),
ManagementRefusal::PathspecsBeforeSeparator { fix } => format!(
"git cli-git {} pathspecs must follow --.\n",
direct_name(fix)
),
ManagementRefusal::ScopeRequired { fix } => format!(
"git cli-git {} requires exactly one scope: --all or non-empty pathspecs after --.\n",
direct_name(fix)
),
};
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr,
};
}
fn direct_config_failure(error: &ConfigError) -> Action {
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: config_invalid_event(error),
stderr: String::new(),
};
}
fn scope_pathspecs(all: bool, pathspecs: Vec<OsString>) -> Vec<OsString> {
if all {
return vec![OsString::from(":/")];
}
return pathspecs;
}
fn run_direct_command(
fix: bool,
selected: Vec<PolicyId>,
controls: &Controls,
loaded: &LoadedConfig,
checks: &mut ShippedChecks<GitFacts>,
root: Option<&Path>,
) -> Action {
let mut stdout: String = if fix {
String::new()
} else {
legacy_warning_events(loaded)
};
let first_sequence: u64 = stdout.matches('\n').count() as u64;
checks.scanner_settings.options = loaded.config.policies.forbidden_strings.clone();
checks.markdown.options = loaded.config.policies.markdown_autofix.clone();
checks.discarded_limits = loaded.config.discarded;
let trigger: Trigger = if fix {
Trigger::DirectFix
} else {
Trigger::DirectCheck
};
if let Err(error) = checks
.content
.prepare(&checks.candidates, &mut checks.facts)
{
stdout.push_str(
render_policy_events(
first_sequence,
&[PolicyEvent::EngineFailure {
code: EngineFailureCode::TransactionFailed,
hint: crate::policy_events::EngineFailureHint::OfCode,
message: error.message,
trigger: Some(trigger),
policy: None,
path: None,
}],
)
.as_str(),
);
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout,
stderr: String::new(),
};
}
let request: StageRequest = StageRequest {
trigger,
config: loaded.config.policies.clone(),
controls: controls.clone(),
selected,
};
let pass: PassResult = if fix {
run_direct_fix(&request, checks, root)
} else {
run_policy_pass(&request, checks)
};
stdout.push_str(render_policy_events(first_sequence, pass.events.as_slice()).as_str());
let stderr: String = if let StageEnd::Unavailable(unavailable) = pass.end {
unported_notice(
&unported_from_unavailable(unavailable),
direct_command(fix).as_str(),
)
} else {
String::new()
};
return Action::Exit {
code: pass_exit_code(pass.events.as_slice(), pass.end),
stdout,
stderr,
};
}
fn directory_options(global_arguments: &[OsString]) -> Option<Vec<OsString>> {
let mut kept: Vec<OsString> = Vec::new();
let mut options = global_arguments.iter();
while let Some(option) = options.next() {
let value: &OsString = options.next()?;
if option == "-C" {
kept.push(option.clone());
kept.push(value.clone());
} else if option != "-c" {
return None;
}
}
return Some(kept);
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Selection {
Found(PathBuf),
Outside,
Unresolved(String),
}
fn status_git_dir(global_arguments: &[OsString], environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Selection {
let named: Vec<(OsString, OsString)> = real_git.map_or_else(|| { return environment.to_vec(); }, |found| { return with_real_git(environment, found); });
if let Some(prefix) = directory_options(global_arguments)
&& let Some(layout) = read_layout_after_success_for(prefix.as_slice(), named.as_slice())
{
return Selection::Found(layout.git_dir);
}
let Some(real) = real_git else {
return Selection::Unresolved(String::from("no real Git was found to ask"));
};
let mut asking = std::process::Command::new(real);
asking
.args(global_arguments)
.args(["rev-parse", "--path-format=absolute", "--absolute-git-dir"])
.envs(environment.iter().map(|(name, value)| { return (name, value); }))
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null());
crate::started_as_git::mark_real_git_start(&mut asking, crate::started_as_git::StartMode::Child);
let asked = asking.output();
let output = match asked {
Ok(found) => found,
Err(error) => return Selection::Unresolved(format!("Git could not be started: {error}")),
};
let said: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
if output.status.success() && !said.is_empty() {
return Selection::Found(PathBuf::from(said));
}
let complaint: String = String::from_utf8_lossy(output.stderr.as_slice()).trim().to_owned();
if output.status.code() == Some(NOT_A_REPOSITORY_EXIT_CODE) && complaint.contains("not a git repository") {
return Selection::Outside;
}
return Selection::Unresolved(format!("Git said: {complaint}"));
}
const NOT_A_REPOSITORY_EXIT_CODE: i32 = 128;
fn plan_service(global_arguments: &[OsString], environment: &[(OsString, OsString)], inputs: &ResolutionInputs) -> Action {
let (real_git, notice): (Option<PathBuf>, Option<String>) = optional_real_git(inputs);
let action: Action = plan_service_with(global_arguments, environment, real_git.as_deref());
return with_leading_notice(action, notice.as_deref());
}
fn optional_real_git(inputs: &ResolutionInputs) -> (Option<PathBuf>, Option<String>) {
match resolve_real_git(inputs) {
Ok(found) => return (Some(found.path), found.notice),
Err(_) => return (None, None),
}
}
fn plan_service_with(global_arguments: &[OsString], environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Action {
let runtime: Option<PathBuf> = environment_value(environment, "XDG_RUNTIME_DIR").filter(|text| { return !text.is_empty(); }).map(PathBuf::from);
let git_dir: Option<PathBuf> = match status_git_dir(global_arguments, environment, real_git) {
Selection::Found(found) => Some(found),
Selection::Outside => None,
Selection::Unresolved(reason) => {
return Action::Exit {
code: 1,
stdout: String::new(),
stderr: format!("The commit service cannot be checked: {reason}, so it is not known which repository this invocation selects, and no service was counted.\n"),
};
}
};
let report = service_status(runtime.as_deref(), git_dir.as_deref());
return Action::Exit { code: report.code, stdout: report.stdout, stderr: report.stderr };
}
fn plan_migrate(parsed: Result<MigrateArguments, String>) -> Action {
let arguments: MigrateArguments = match parsed {
Ok(found) => found,
Err(reason) => {
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr: format!("git cli-git migrate-to-subvolume: {reason}\n"),
};
}
};
let cwd: PathBuf = match std::env::current_dir() {
Ok(found) => found,
Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
};
return run_migrate(&arguments, cwd.as_path());
}
fn plan_convert(parsed: Result<ConvertArguments, String>, global_arguments: &[OsString], environment: &[(OsString, OsString)], inputs: &ResolutionInputs) -> Action {
let arguments: ConvertArguments = match parsed {
Ok(found) => ConvertArguments { command_line_attributes: crate::worktree_convert_arguments::selects_attribute_source(global_arguments), ..found },
Err(reason) => {
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr: format!("git cli-git convert-worktree-to-subvolume: {reason}\n"),
};
}
};
let cwd: PathBuf = match std::env::current_dir() {
Ok(found) => found,
Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
};
let (real_git, notice): (Option<PathBuf>, Option<String>) = optional_real_git(inputs);
let action: Action = run_convert(&arguments, cwd.as_path(), environment, real_git.as_deref());
return with_leading_notice(action, notice.as_deref());
}
#[cfg(target_os = "linux")]
fn run_convert(arguments: &ConvertArguments, cwd: &Path, environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Action {
return crate::worktree_convert_command::convert_action(arguments, cwd, environment, real_git);
}
#[cfg(not(target_os = "linux"))]
fn run_convert(_arguments: &ConvertArguments, _cwd: &Path, _environment: &[(OsString, OsString)], _real_git: Option<&Path>) -> Action {
return failure("git cli-git convert-worktree-to-subvolume works on Linux with btrfs only");
}
#[cfg(target_os = "linux")]
pub fn host_filesystem_section(common_directory: &Path) -> String {
return crate::doctor_filesystem::host_section(common_directory);
}
#[cfg(not(target_os = "linux"))]
pub fn host_filesystem_section(_common_directory: &Path) -> String {
return String::new();
}
#[cfg(target_os = "linux")]
fn run_migrate(arguments: &MigrateArguments, cwd: &Path) -> Action {
return migrate_action(arguments, cwd);
}
#[cfg(not(target_os = "linux"))]
fn run_migrate(_arguments: &MigrateArguments, _cwd: &Path) -> Action {
return failure("git cli-git migrate-to-subvolume works on Linux with btrfs only");
}
fn plan_doctor(
git_global_arguments: &[OsString],
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
filesystem_section: &dyn Fn(&Path) -> String,
) -> Action {
let resolved: ResolvedGit = match resolve_real_git(inputs) {
Ok(found) => found,
Err(error) => return failure(error.to_string().as_str()),
};
let real_git: PathBuf = resolved.path;
let cwd: PathBuf = match std::env::current_dir() {
Ok(found) => found,
Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
};
let overlay: Vec<(OsString, OsString)> = child_environment_overlay(environment, real_git.as_path());
let context: GitContext = GitContext {
real_git,
overlay,
global_prefix: git_global_arguments.to_vec(),
common_dir: None,
};
let action: Action = doctor_action_with_section(&context, cwd.as_path(), filesystem_section);
return with_leading_notice(action, resolved.notice.as_deref());
}
pub fn plan_management(
arguments: &[OsString],
git_global_arguments: &[OsString],
controls: &Controls,
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
) -> Action {
return plan_management_with_section(arguments, git_global_arguments, controls, environment, inputs, &|_cwd: &Path| { return String::new(); });
}
pub fn plan_management_with_section(
arguments: &[OsString],
git_global_arguments: &[OsString],
controls: &Controls,
environment: &[(OsString, OsString)],
inputs: &ResolutionInputs,
filesystem_section: &dyn Fn(&Path) -> String,
) -> Action {
let parsed: ManagementAction = match parse_management_arguments(arguments) {
Ok(action) => action,
Err(reason) => return refusal(reason),
};
let (fix, policies, scope): (bool, Vec<String>, Vec<OsString>) = match parsed {
ManagementAction::Help => {
return Action::Exit {
code: 0,
stdout: String::from(MANAGEMENT_HELP),
stderr: String::new(),
};
}
ManagementAction::Retired { command, help } => {
if help {
return Action::Exit {
code: 0,
stdout: retired_explanation(command),
stderr: String::new(),
};
}
return Action::Exit {
code: 0,
stdout: String::new(),
stderr: retired_explanation(command),
};
}
ManagementAction::Doctor => return plan_doctor(git_global_arguments, environment, inputs, filesystem_section),
ManagementAction::ServiceStatus => return plan_service(git_global_arguments, environment, inputs),
ManagementAction::MigrateToSubvolume(migrate_parse) => return plan_migrate(migrate_parse),
ManagementAction::ConvertWorktree(convert_parse) => return plan_convert(convert_parse, git_global_arguments, environment, inputs),
ManagementAction::Direct {
fix,
all,
policies,
pathspecs,
} => (fix, policies, scope_pathspecs(all, pathspecs)),
};
let selected: Vec<PolicyId> = match selected_policies(policies.as_slice()) {
Ok(ids) => ids,
Err(error) => return direct_config_failure(&error),
};
let resolved: ResolvedGit = match resolve_real_git(inputs) {
Ok(found) => found,
Err(error) => return failure(error.to_string().as_str()),
};
let action: Action = plan_direct(
fix,
selected,
scope,
controls,
environment,
git_global_arguments,
resolved.path,
);
return with_leading_notice(action, resolved.notice.as_deref());
}
fn plan_direct(
fix: bool,
selected: Vec<PolicyId>,
scope: Vec<OsString>,
controls: &Controls,
environment: &[(OsString, OsString)],
git_global_arguments: &[OsString],
real_git: PathBuf,
) -> Action {
let overlay: Vec<(OsString, OsString)> =
child_environment_overlay(environment, real_git.as_path());
let mut checks: ShippedChecks<GitFacts> = shipped_checks(
git_facts(real_git.as_path(), git_global_arguments, overlay.as_slice()),
git_global_arguments.to_vec(),
LifecycleContent::Requested(CandidateRequest::Direct(scope)),
Vec::<PathBuf>::new(),
);
checks.scanner_settings.rules_variable = environment_value(environment, RULES_VARIABLE);
let location: RepositoryLocation = match checks.facts.location() {
Ok(found) => found,
Err(message) => return failure(message.as_str()),
};
match recover_for_command(&checks.facts.transaction_context(), &location.identity) {
Gate::Clear => {}
Gate::Stop(stderr) => {
return Action::Exit {
code: ENGINE_FAILURE_EXIT_CODE,
stdout: String::new(),
stderr,
};
}
}
let action: Action = match load_identity_config(&location.identity) {
Ok(loaded) => match selection_error(
selected.as_slice(),
&loaded.config.policies,
if fix { Trigger::DirectFix } else { Trigger::DirectCheck },
) {
Some(error) => direct_config_failure(&error),
None => run_direct_command(
fix,
selected,
controls,
&loaded,
&mut checks,
worktree_root(&location.identity),
),
},
Err(error) => direct_config_failure(&error),
};
return action;
}
#[cfg(test)]
#[path = "management_tests.rs"]
mod tests;