Skip to main content

git_policy_cli/
management.rs

1//! What:
2//!  Decide what one `git cli-git ...` invocation does.
3//! Why:
4//!  Help,
5//!  refusals and retired commands need no repository;
6//!  `check` and `fix`
7//!      validate their policy selection and configuration,
8//!  then run one policy pass over
9//!      the selected repository and report its events on standard output.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! // const action = planManagement(managementArgs, gitGlobalArgs, controls, environment, inputs);
14//! ```
15
16/// Import the sibling modules the decision combines.
17use super::action::{Action, ENGINE_FAILURE_EXIT_CODE, failure};
18/// A direct command asks for the worktree files its scope selects.
19use super::candidate_prediction::CandidateRequest;
20use super::child_environment::{child_environment_overlay, environment_value};
21use super::config_error::ConfigError;
22use super::config_file::LoadedConfig;
23/// A scope that cannot be projected is a `transaction-failed` engine failure.
24use super::diagnostics::EngineFailureCode;
25/// The direct fix:
26///  converge corrections in memory,
27///  then install them.
28use super::direct_fix::run_direct_fix;
29/// The traditional-hook listing of `git cli-git doctor`.
30use super::doctor::doctor_action_with_section;
31use super::invocation_config::{config_invalid_event, legacy_warning_events, load_identity_config};
32use super::git_system_config::with_real_git;
33use super::repo_read_layout::read_layout_after_success_for;
34use super::service_route::service_status;
35use super::management_arguments::{
36    MANAGEMENT_HELP, MANAGEMENT_USAGE, ManagementAction, ManagementRefusal, RetiredCommand,
37    parse_management_arguments,
38};
39/// The refusal of a `--policy` selection that is off.
40use super::management_selection::{selected_policies, selection_error};
41/// The linter the executable gives the Markdown policy.
42/// The shipped checks and their constructor.
43use super::policy_checks::{ShippedChecks, shipped_checks};
44/// What the direct command offers its content policies.
45use super::policy_content::LifecycleContent;
46use super::policy_engine::{StageEnd, StageRequest, pass_exit_code};
47/// The events of a direct command and their rendering.
48use super::policy_events::{PolicyEvent, render_policy_events};
49use super::policy_pass::{PassResult, run_policy_pass};
50use super::policy_registry::PolicyId;
51use super::policy_trigger::Trigger;
52use super::real_git::{ResolutionInputs, resolve_real_git};
53use super::repository_facts::{GitFacts, RepositoryFacts, git_facts};
54use super::repository_location::RepositoryLocation;
55/// The variable that names the forbidden-strings rules file.
56use super::scanner_selection::RULES_VARIABLE;
57/// Startup recovery as a command step.
58use super::startup_gate::{Gate, recover_for_command};
59use super::transaction_git::GitContext;
60use super::subvolume_arguments::MigrateArguments;
61#[cfg(target_os = "linux")]
62use super::subvolume_command::migrate_action;
63use super::unported::{unported_from_unavailable, unported_notice};
64/// The worktree's top level,
65///  where a fix installs corrected files.
66use super::worktree_identity::worktree_root;
67use super::wrapper_controls::Controls;
68/// What:
69///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
70///       must be UTF-8).
71/// Why:
72///   Arguments and environment values are never decoded.
73///
74/// In TS you'd write (pseudocode):
75/// ```ts
76/// // string[] argv, but byte-preserving.
77/// ```
78use std::ffi::OsString;
79/// `PathBuf` is an owned filesystem path of raw OS bytes.
80use std::path::{Path, PathBuf};
81
82/// What:
83///  The command word of a direct command.
84///       `&'static str` borrows text compiled into the executable for its whole run.
85/// Why:
86///   Messages name the command the caller typed.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// function directName(fix: boolean): string { return fix ? 'fix' : 'check'; }
91/// ```
92fn direct_name(fix: bool) -> &'static str {
93    if fix {
94        return "fix";
95    }
96    return "check";
97}
98
99/// What:
100///  The words of a direct command as the caller typed them after `git`.
101///       `String` is owned UTF-8 text.
102/// Why:
103///   A refusal names the command that was not run.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// function directCommand(fix: boolean): string { return `cli-git ${directName(fix)}`; }
108/// ```
109fn direct_command(fix: bool) -> String {
110    // `format!` builds owned text; `{}` interpolates the command word.
111    return format!("cli-git {}", direct_name(fix));
112}
113
114/// What:
115///  The command word of a retired command.
116/// Why:
117///   The explanation names the command the caller typed.
118///
119/// In TS you'd write (pseudocode):
120/// ```ts
121/// function retiredName(command: RetiredCommand): string;
122/// ```
123fn retired_name(command: RetiredCommand) -> &'static str {
124    // `match` must name every variant, so a new retired command cannot be forgotten.
125    match command {
126        RetiredCommand::Trust => return "trust",
127        RetiredCommand::Untrust => return "untrust",
128        RetiredCommand::Status => return "status",
129    }
130}
131
132/// What:
133///  Explain that a trust command is retired.
134///       `String` is the owned,
135///  line-terminated explanation.
136/// Why:
137///   Trust existed to approve executing a repository's configuration code.
138///  JSONC
139///       configuration is data,
140///  so there is nothing to approve,
141///  revoke or report.
142///       Old trust records are neither read nor deleted.
143///
144/// In TS you'd write (pseudocode):
145/// ```ts
146/// function retiredExplanation(command: RetiredCommand): string;
147/// ```
148pub fn retired_explanation(command: RetiredCommand) -> String {
149    // `format!` builds the owned text; `{}` interpolates the command word.
150    return format!(
151        "git cli-git {} is retired. cli-git now reads cli-git.config.jsonc as data and runs no \
152         repository-supplied code, so there is no code execution to approve, revoke or report. \
153         Nothing was changed; existing trust records are left in place and are no longer read.\n",
154        retired_name(command)
155    );
156}
157
158/// What:
159///  Turn a refusal into its usage action:
160///  text on standard error,
161///  exit status 2.
162/// Why:
163///   A malformed management invocation must never fall through to Git.
164///
165/// In TS you'd write (pseudocode):
166/// ```ts
167/// function refusal(reason: ManagementRefusal): Action;
168/// ```
169fn refusal(reason: ManagementRefusal) -> Action {
170    // `match` picks the message for the refusal and binds its `fix` flag where present.
171    let stderr: String = match reason {
172        ManagementRefusal::Usage => String::from(MANAGEMENT_USAGE),
173        ManagementRefusal::PathspecsBeforeSeparator { fix } => format!(
174            "git cli-git {} pathspecs must follow --.\n",
175            direct_name(fix)
176        ),
177        ManagementRefusal::ScopeRequired { fix } => format!(
178            "git cli-git {} requires exactly one scope: --all or non-empty pathspecs after --.\n",
179            direct_name(fix)
180        ),
181    };
182    return Action::Exit {
183        code: ENGINE_FAILURE_EXIT_CODE,
184        // `String::new()` is empty owned text.
185        stdout: String::new(),
186        stderr,
187    };
188}
189
190/// What:
191///  Stop a direct command with a `config-invalid` event on standard output.
192/// Why:
193///   Direct commands report policy events on standard output,
194///  and an invalid
195///       configuration or an unknown selected policy exits 2 before any policy runs.
196///
197/// In TS you'd write (pseudocode):
198/// ```ts
199/// function directConfigFailure(error: ConfigError): Action;
200/// ```
201fn direct_config_failure(error: &ConfigError) -> Action {
202    return Action::Exit {
203        code: ENGINE_FAILURE_EXIT_CODE,
204        stdout: config_invalid_event(error),
205        stderr: String::new(),
206    };
207}
208
209/// What:
210///  The pathspecs of a direct command's scope.
211///  `all` is `--all`;
212///  `Vec<OsString>`
213///       is the owned list of pathspecs written after `--`.
214/// Why:
215///   `--all` selects the whole worktree from its top level,
216///  which Git spells `:/`,
217///       as the installed wrapper does.
218///
219/// In TS you'd write (pseudocode):
220/// ```ts
221/// const directPathspecs = all ? [':/'] : pathspecs;
222/// ```
223fn scope_pathspecs(all: bool, pathspecs: Vec<OsString>) -> Vec<OsString> {
224    if all {
225        // `vec![...]` builds the one-item list.
226        return vec![OsString::from(":/")];
227    }
228    return pathspecs;
229}
230
231/// What:
232///  Run the policy pass of one direct command and build its action.
233///       `&LoadedConfig` borrows the accepted configuration;
234///  `&mut ShippedChecks<GitFacts>`
235///       lends the shipped policies over real Git for writing.
236/// Why:
237///   Direct commands report every event on standard output.
238///  `check` first reports a
239///       legacy file left beside the JSONC file,
240///  and policy events continue its numbering.
241///       A policy that cannot be evaluated here stops the command with exit status 2 and
242///       a notice on standard error,
243///  after the events gathered so far.
244///
245/// In TS you'd write (pseudocode):
246/// ```ts
247/// async function runDirectCommand({ fix, selected, controls, loaded, checks }): Promise<Action>;
248/// ```
249fn run_direct_command(
250    fix: bool,
251    selected: Vec<PolicyId>,
252    controls: &Controls,
253    loaded: &LoadedConfig,
254    checks: &mut ShippedChecks<GitFacts>,
255    root: Option<&Path>,
256) -> Action {
257    // Only `check` reports a legacy file left beside the JSONC file; `fix` stays silent.
258    let mut stdout: String = if fix {
259        String::new()
260    } else {
261        legacy_warning_events(loaded)
262    };
263    // Each event is one line, so the lines written so far are the next event number.
264    // `.matches('\n').count()` counts them; `as u64` widens the count to the number type.
265    let first_sequence: u64 = stdout.matches('\n').count() as u64;
266    // `.clone()` copies the scanner and Markdown options the configuration chose.
267    checks.scanner_settings.options = loaded.config.policies.forbidden_strings.clone();
268    checks.markdown.options = loaded.config.policies.markdown_autofix.clone();
269    checks.discarded_limits = loaded.config.discarded;
270    let trigger: Trigger = if fix {
271        Trigger::DirectFix
272    } else {
273        Trigger::DirectCheck
274    };
275    // The selected files are staged on a private index before any policy runs, as the
276    // installed wrapper does, so a scope Git refuses is reported whatever is selected.
277    if let Err(error) = checks
278        .content
279        .prepare(&checks.candidates, &mut checks.facts)
280    {
281        stdout.push_str(
282            render_policy_events(
283                first_sequence,
284                &[PolicyEvent::EngineFailure {
285                    code: EngineFailureCode::TransactionFailed,
286                    message: error.message,
287                    // `Some(x)` is the "present" case of `Option`.
288                    trigger: Some(trigger),
289                    // `None` is the "absent" case: no policy has run.
290                    policy: None,
291                    path: None,
292                }],
293            )
294            .as_str(),
295        );
296        return Action::Exit {
297            code: ENGINE_FAILURE_EXIT_CODE,
298            stdout,
299            stderr: String::new(),
300        };
301    }
302    let request: StageRequest = StageRequest {
303        trigger,
304        // `.clone()` copies the settings and controls into the request.
305        config: loaded.config.policies.clone(),
306        controls: controls.clone(),
307        selected,
308    };
309    // A fix converges corrections and installs them; a check runs one pass.
310    let pass: PassResult = if fix {
311        run_direct_fix(&request, checks, root)
312    } else {
313        run_policy_pass(&request, checks)
314    };
315    // `.as_slice()` lends the owned events as a borrowed view.
316    stdout.push_str(render_policy_events(first_sequence, pass.events.as_slice()).as_str());
317    // `if let StageEnd::Unavailable(x) = ...` runs only for that ending and binds what it carries.
318    let stderr: String = if let StageEnd::Unavailable(unavailable) = pass.end {
319        unported_notice(
320            &unported_from_unavailable(unavailable),
321            direct_command(fix).as_str(),
322        )
323    } else {
324        String::new()
325    };
326    return Action::Exit {
327        code: pass_exit_code(pass.events.as_slice(), pass.end),
328        stdout,
329        stderr,
330    };
331}
332
333/// What:
334///  The global options of an invocation that choose the directory it runs in (`-C <path>`,
335/// in order),
336/// or nothing when
337///  another option could choose the repository too.
338/// Why:
339///   `git -C /path/B cli-git service status` asks about B,
340/// wherever it is run from;
341/// `-c <name>=<value>` changes no directory,
342///  while `--git-dir` and its kin name a repository the in-process reader does not resolve.
343///
344/// In TS you'd write (pseudocode):
345/// ```ts
346/// function directoryOptions(globalArguments): string[] | undefined;
347/// ```
348fn directory_options(global_arguments: &[OsString]) -> Option<Vec<OsString>> {
349    let mut kept: Vec<OsString> = Vec::new();
350    let mut options = global_arguments.iter();
351    while let Some(option) = options.next() {
352        let value: &OsString = options.next()?;
353        if option == "-C" {
354            kept.push(option.clone());
355            kept.push(value.clone());
356        } else if option != "-c" {
357            return None;
358        }
359    }
360    return Some(kept);
361}
362
363/// Which repository a `git cli-git service status` invocation selects.
364#[derive(Clone, Debug, Eq, PartialEq)]
365enum Selection {
366    /// The Git directory of the worktree it selects.
367    Found(PathBuf),
368    /// Git says the invocation is outside a repository.
369    Outside,
370    /// Neither the reader nor Git could say,
371    /// with the reason.
372    Unresolved(String),
373}
374
375/// What:
376///  The repository a `git <global options> cli-git service status` selects:
377/// the Git directory of its worktree,
378/// outside a
379///  repository,
380/// or unresolved.
381/// Why:
382///   The command needs no valid configuration,
383/// so the structure of the files decides and the configuration values do not.
384///  The reader learns the system configuration file from the real Git `real_git` names,
385/// and resolves `-C` options only.
386///  Whatever it declines (`--git-dir`,
387/// `--work-tree`,
388/// `GIT_DIR`,
389/// an unusual layout) is put to the real Git once,
390/// which resolves the
391///  selection as every other command does;
392/// "outside a repository" is Git's own answer,
393/// and anything Git cannot answer is
394///  unresolved,
395/// never an invitation to count unrelated services.
396///
397/// In TS you'd write (pseudocode):
398/// ```ts
399/// function statusGitDir(globalArguments, environment, realGit): string | undefined;
400/// ```
401fn status_git_dir(global_arguments: &[OsString], environment: &[(OsString, OsString)], real_git: Option<&Path>) -> Selection {
402    let named: Vec<(OsString, OsString)> = real_git.map_or_else(|| { return environment.to_vec(); }, |found| { return with_real_git(environment, found); });
403    if let Some(prefix) = directory_options(global_arguments)
404        && let Some(layout) = read_layout_after_success_for(prefix.as_slice(), named.as_slice())
405    {
406        return Selection::Found(layout.git_dir);
407    }
408    let Some(real) = real_git else {
409        return Selection::Unresolved(String::from("no real Git was found to ask"));
410    };
411    let asked = std::process::Command::new(real)
412        .args(global_arguments)
413        .args(["rev-parse", "--path-format=absolute", "--absolute-git-dir"])
414        .envs(environment.iter().map(|(name, value)| { return (name, value); }))
415        .env("LC_ALL", "C")
416        .stdin(std::process::Stdio::null())
417        .output();
418    let output = match asked {
419        Ok(found) => found,
420        Err(error) => return Selection::Unresolved(format!("Git could not be started: {error}")),
421    };
422    let said: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
423    if output.status.success() && !said.is_empty() {
424        return Selection::Found(PathBuf::from(said));
425    }
426    let complaint: String = String::from_utf8_lossy(output.stderr.as_slice()).trim().to_owned();
427    if output.status.code() == Some(NOT_A_REPOSITORY_EXIT_CODE) && complaint.contains("not a git repository") {
428        return Selection::Outside;
429    }
430    return Selection::Unresolved(format!("Git said: {complaint}"));
431}
432
433/// The exit status Git gives for a fatal error such as running outside a repository.
434const NOT_A_REPOSITORY_EXIT_CODE: i32 = 128;
435
436/// What:
437///  Decide the action of `git cli-git service status`.
438/// Why:
439///   The command is read-only:
440/// it says whether the service of the worktree it runs in answers.
441///
442/// In TS you'd write (pseudocode):
443/// ```ts
444/// function planService(globalArguments, environment, inputs): Action;
445/// ```
446fn plan_service(global_arguments: &[OsString], environment: &[(OsString, OsString)], inputs: &ResolutionInputs) -> Action {
447    let runtime: Option<PathBuf> = environment_value(environment, "XDG_RUNTIME_DIR").filter(|text| { return !text.is_empty(); }).map(PathBuf::from);
448    let real_git: Option<PathBuf> = resolve_real_git(inputs).ok();
449    // The worktree the command runs in has its own service; outside a repository the answer counts any.
450    let git_dir: Option<PathBuf> = match status_git_dir(global_arguments, environment, real_git.as_deref()) {
451        Selection::Found(found) => Some(found),
452        Selection::Outside => None,
453        Selection::Unresolved(reason) => {
454            return Action::Exit {
455                code: 1,
456                stdout: String::new(),
457                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"),
458            };
459        }
460    };
461    let report = service_status(runtime.as_deref(), git_dir.as_deref());
462    return Action::Exit { code: report.code, stdout: report.stdout, stderr: report.stderr };
463}
464
465/// What:
466///  Decide the action of `git cli-git migrate-to-subvolume`: a refused invocation prints its reason, anything else runs
467///  from the working directory.
468/// Why:
469///   The command needs neither configuration nor Git: it reads `.git` itself and calls the filesystem.
470///
471/// In TS you'd write (pseudocode):
472/// ```ts
473/// function planMigrate(parsed: MigrateArguments | string): Action;
474/// ```
475fn plan_migrate(parsed: Result<MigrateArguments, String>) -> Action {
476    let arguments: MigrateArguments = match parsed {
477        Ok(found) => found,
478        Err(reason) => {
479            return Action::Exit {
480                code: ENGINE_FAILURE_EXIT_CODE,
481                stdout: String::new(),
482                stderr: format!("git cli-git migrate-to-subvolume: {reason}\n"),
483            };
484        }
485    };
486    let cwd: PathBuf = match std::env::current_dir() {
487        Ok(found) => found,
488        Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
489    };
490    return run_migrate(&arguments, cwd.as_path());
491}
492
493/// The doctor's filesystem section read from the running system.
494#[cfg(target_os = "linux")]
495pub fn host_filesystem_section(common_directory: &Path) -> String {
496    return super::doctor_filesystem::host_section(common_directory);
497}
498
499/// Other platforms have no btrfs or overlayfs section.
500#[cfg(not(target_os = "linux"))]
501pub fn host_filesystem_section(_common_directory: &Path) -> String {
502    return String::new();
503}
504
505/// Run the migration command where the platform has btrfs subvolumes.
506#[cfg(target_os = "linux")]
507fn run_migrate(arguments: &MigrateArguments, cwd: &Path) -> Action {
508    return migrate_action(arguments, cwd);
509}
510
511/// Other platforms have no btrfs subvolumes, so the command says so.
512#[cfg(not(target_os = "linux"))]
513fn run_migrate(_arguments: &MigrateArguments, _cwd: &Path) -> Action {
514    return failure("git cli-git migrate-to-subvolume works on Linux with btrfs only");
515}
516
517fn plan_doctor(
518    git_global_arguments: &[OsString],
519    environment: &[(OsString, OsString)],
520    inputs: &ResolutionInputs,
521    filesystem_section: &dyn Fn(&Path) -> String,
522) -> Action {
523    let real_git: PathBuf = match resolve_real_git(inputs) {
524        Ok(found) => found,
525        Err(error) => return failure(error.to_string().as_str()),
526    };
527    let cwd: PathBuf = match std::env::current_dir() {
528        Ok(found) => found,
529        Err(error) => return failure(format!("cannot determine the current directory: {error}").as_str()),
530    };
531    let overlay: Vec<(OsString, OsString)> = child_environment_overlay(environment, real_git.as_path());
532    let context: GitContext = GitContext {
533        real_git,
534        overlay,
535        global_prefix: git_global_arguments.to_vec(),
536        // The doctor sends no input, so no scratch directory is needed.
537        common_dir: None,
538    };
539    return doctor_action_with_section(&context, cwd.as_path(), filesystem_section);
540}
541
542/// What:
543///  Decide the action for the arguments after `git cli-git`.
544///       `&[OsString]` borrows argument lists;
545///  `&Controls` borrows what the wrapper
546///       controls written before `cli-git` asked for;
547///  `&ResolutionInputs` borrows process facts.
548/// Why:
549///   Help,
550///  refusals and retired commands answer without resolving Git or reading
551///       any repository.
552///  A direct command validates its selection,
553///  asks Git once where it
554///       runs,
555///  refuses beside commit transactions it cannot recover,
556///  loads that
557///       worktree's configuration and runs one policy pass.
558///
559/// In TS you'd write (pseudocode):
560/// ```ts
561/// function planManagement(args, gitGlobalArgs, controls, environment, inputs): Action;
562/// ```
563pub fn plan_management(
564    arguments: &[OsString],
565    git_global_arguments: &[OsString],
566    controls: &Controls,
567    environment: &[(OsString, OsString)],
568    inputs: &ResolutionInputs,
569) -> Action {
570    return plan_management_with_section(arguments, git_global_arguments, controls, environment, inputs, &|_cwd: &Path| { return String::new(); });
571}
572
573/// What:
574///  The same decision with the doctor's filesystem section supplied by the caller.
575///  `&dyn Fn(&Path) -> String` is a borrowed function from the repository's common Git directory to the section text.
576/// Why:
577///   The executable passes the section read from the running system (`host_filesystem_section`); the controls of the
578///  doctor and of the management command pass an empty one, so their exact output does not depend on the host.
579///
580/// In TS you'd write (pseudocode):
581/// ```ts
582/// function planManagementWithSection(args, gitGlobalArgs, controls, environment, inputs, filesystemSection): Action;
583/// ```
584pub fn plan_management_with_section(
585    arguments: &[OsString],
586    git_global_arguments: &[OsString],
587    controls: &Controls,
588    environment: &[(OsString, OsString)],
589    inputs: &ResolutionInputs,
590    filesystem_section: &dyn Fn(&Path) -> String,
591) -> Action {
592    // What: `match` on the parse `Result`: keep the action or return the refusal.
593    // Why:  Nothing else may run for an invocation the grammar rejects.
594    //
595    // In TS you'd write (pseudocode):
596    // ```ts
597    // const parsed = parseManagementArgs(args); if (parsed === MANAGEMENT_REFUSED) return usage();
598    // ```
599    let parsed: ManagementAction = match parse_management_arguments(arguments) {
600        Ok(action) => action,
601        Err(reason) => return refusal(reason),
602    };
603    // `match` picks by variant and binds the fields each one carries.
604    let (fix, policies, scope): (bool, Vec<String>, Vec<OsString>) = match parsed {
605        ManagementAction::Help => {
606            return Action::Exit {
607                code: 0,
608                stdout: String::from(MANAGEMENT_HELP),
609                stderr: String::new(),
610            };
611        }
612        ManagementAction::Retired { command, help } => {
613            // Help goes to standard output; an attempted action is explained on standard error.
614            if help {
615                return Action::Exit {
616                    code: 0,
617                    stdout: retired_explanation(command),
618                    stderr: String::new(),
619                };
620            }
621            return Action::Exit {
622                code: 0,
623                stdout: String::new(),
624                stderr: retired_explanation(command),
625            };
626        }
627        ManagementAction::Doctor => return plan_doctor(git_global_arguments, environment, inputs, filesystem_section),
628        ManagementAction::ServiceStatus => return plan_service(git_global_arguments, environment, inputs),
629        ManagementAction::MigrateToSubvolume(migrate_parse) => return plan_migrate(migrate_parse),
630        ManagementAction::Direct {
631            fix,
632            all,
633            policies,
634            pathspecs,
635        } => (fix, policies, scope_pathspecs(all, pathspecs)),
636    };
637    let selected: Vec<PolicyId> = match selected_policies(policies.as_slice()) {
638        Ok(ids) => ids,
639        // `&error` lends the error to the renderer.
640        Err(error) => return direct_config_failure(&error),
641    };
642    let real_git: PathBuf = match resolve_real_git(inputs) {
643        Ok(found) => found,
644        Err(error) => return failure(error.to_string().as_str()),
645    };
646    let overlay: Vec<(OsString, OsString)> =
647        child_environment_overlay(environment, real_git.as_path());
648    // `mut` lets the pass cache facts it asks Git for.
649    let mut checks: ShippedChecks<GitFacts> = shipped_checks(
650        git_facts(real_git.as_path(), git_global_arguments, overlay.as_slice()),
651        // `.to_vec()` copies the borrowed global options for the rule cores.
652        git_global_arguments.to_vec(),
653        LifecycleContent::Requested(CandidateRequest::Direct(scope)),
654        // `Vec::new()` is an empty owned list: no direct-command policy reads the tool caches.
655        Vec::<PathBuf>::new(),
656    );
657    // The forbidden-strings rules variable is read from this invocation's environment.
658    checks.scanner_settings.rules_variable = environment_value(environment, RULES_VARIABLE);
659    let location: RepositoryLocation = match checks.facts.location() {
660        Ok(found) => found,
661        Err(message) => return failure(message.as_str()),
662    };
663    // A direct command forwards nothing, so only an isolated merge whose process died matters here: it is settled first.
664    match recover_for_command(&checks.facts.transaction_context(), &location.identity) {
665        Gate::Clear => {}
666        Gate::Stop(stderr) => {
667            return Action::Exit {
668                code: ENGINE_FAILURE_EXIT_CODE,
669                stdout: String::new(),
670                stderr,
671            };
672        }
673    }
674    let action: Action = match load_identity_config(&location.identity) {
675        Ok(loaded) => match selection_error(
676            selected.as_slice(),
677            &loaded.config.policies,
678            if fix { Trigger::DirectFix } else { Trigger::DirectCheck },
679        ) {
680            // A selected policy that is off, or does not run for this command, would check nothing and look clean:
681            // stop before any work.
682            Some(error) => direct_config_failure(&error),
683            None => run_direct_command(
684                fix,
685                selected,
686                controls,
687                &loaded,
688                &mut checks,
689                worktree_root(&location.identity),
690            ),
691        },
692        Err(error) => direct_config_failure(&error),
693    };
694    return action;
695}
696
697/// Decision controls stay out of the release executable.
698#[cfg(test)]
699#[path = "management_tests.rs"]
700mod tests;