git_policy_cli/hooks_off.rs
1//! What:
2//! Start the real Git with traditional hooks switched off.
3//! Why:
4//! Traditional hooks (executable files in `<git-common-dir>/hooks` or a `core.hooksPath` directory) are unsupported,
5//! and `git lfs` writes its four shims there again whenever it cleans a file, so a hook file must never run
6//! because of a start the wrapper made.
7//! Every real-Git start of the wrapper therefore carries `-c core.hooksPath=<a directory that never exists>` ahead of
8//! its arguments: Git then finds no hook file, while hooks defined in the configuration
9//! (`hook.<name>.event` with `hook.<name>.command`) still run.
10//! A `-c core.hooksPath=...` the caller writes comes later on the command line, so it wins.
11//! The option is part of the argument vector, so it adds no system call and no process start.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! spawn(realGit, ['-c', `core.hooksPath=${NO_HOOKS_DIRECTORY}`, ...args]);
16//! ```
17
18/// The global-option parser, to find the subcommand behind the caller's global options.
19use super::global_arguments::{GlobalOutcome, global_layout};
20/// Owned operating-system text for the arguments.
21use std::ffi::OsString;
22/// The switch that turns the option on for the process.
23use std::sync::atomic::{AtomicBool, Ordering};
24/// The child that gets the option.
25use std::process::Command;
26
27/// The `core.hooksPath` value of every start: below `/dev/null`, which is a device, so nothing can create it.
28pub const NO_HOOKS_DIRECTORY: &str = "/dev/null/cli-git-no-hooks";
29
30/// What:
31/// Whether `arguments` run `git config`.
32/// Why:
33/// A configuration query reports what the repository configures, and the wrapper itself reads `core.hooksPath` that
34/// way; the injected value must not answer for it.
35/// A query runs no hook, so leaving it out costs no protection.
36///
37/// In TS you'd write (pseudocode):
38/// ```ts
39/// layout.outcome === 'command' && args[layout.prefixLen] === 'config'
40/// ```
41fn is_configuration_query(arguments: &[OsString]) -> bool {
42 let layout = global_layout(arguments);
43 return layout.outcome == GlobalOutcome::Command && arguments[layout.prefix_len] == "config";
44}
45
46/// Whether this process starts real Git with hooks off; the executable switches it on before any start.
47static ACTIVE: AtomicBool = AtomicBool::new(false);
48
49/// What:
50/// Switch the hooks-off option on for every real-Git start this process makes from now on.
51/// Why:
52/// The executable's entry calls it first, so no production start misses it.
53/// The library's unit controls, which fake Git with scripts that record the exact arguments they receive, leave it
54/// off, and the binary-level controls prove the option on the built executable.
55///
56/// In TS you'd write (pseudocode):
57/// ```ts
58/// hooksOff.active = true;
59/// ```
60pub fn activate() {
61 ACTIVE.store(true, Ordering::SeqCst);
62}
63
64/// What:
65/// Add the hooks-off option to a command, unless it is a configuration query.
66/// Why:
67/// The option goes before the arguments the command already carries or is about to carry, so a `-c core.hooksPath`
68/// of the caller comes later and wins.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// if (!isConfigurationQuery(args)) command.args.unshift('-c', `core.hooksPath=${NO_HOOKS_DIRECTORY}`);
73/// ```
74pub fn add_hooks_off_option(command: &mut Command, arguments: &[OsString]) {
75 if is_configuration_query(arguments) {
76 return;
77 }
78 command.arg("-c").arg(format!("core.hooksPath={NO_HOOKS_DIRECTORY}"));
79}
80
81/// What:
82/// Put the hooks-off option in front of a real-Git command's arguments once the process has switched it on.
83/// Why:
84/// One function serves both builders of real-Git commands, so no start can miss it.
85///
86/// In TS you'd write (pseudocode):
87/// ```ts
88/// if (hooksOff.active) addHooksOffOption(command, args);
89/// ```
90pub fn disable_traditional_hooks(command: &mut Command, arguments: &[OsString]) {
91 if ACTIVE.load(Ordering::SeqCst) {
92 add_hooks_off_option(command, arguments);
93 }
94}
95
96/// The option's position, its exemption and its effect on real Git are controlled in the test module.
97#[cfg(test)]
98#[path = "hooks_off_tests.rs"]
99mod tests;