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;