git_policy_cli/worktree/checkout_workers.rs
1//! What:
2//! Whether the checkout of a new worktree may be given Git's parallel checkout, and the option that does it.
3//! Why:
4//! Git's `checkout.workers` defaults to one worker, and a checkout of twenty thousand files measured several times faster with
5//! `checkout.workers=0` (one worker per logical CPU). The owner decided on 2026-11-03 "Pass only if unset": a value the person
6//! set anywhere Git reads (a configuration file of any scope, `GIT_CONFIG_*`, `GIT_CONFIG_PARAMETERS`, a `-c` or
7//! `--config-env` they typed) is theirs and stands alone; only where nothing sets the key does the wrapper put
8//! `-c checkout.workers=0` before the subcommand of its own checkout run. The files are read through the one configuration
9//! read the wrapper already makes, so no extra Git start is added.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! if (!workersSetAnywhere(args, env, configSnapshot)) args.splice(subcommandIndex, 0, '-c', 'checkout.workers=0');
14//! ```
15
16use crate::config_snapshot::{ConfigSnapshot, read_config_snapshot};
17use crate::diagnostic_log::debug;
18use crate::global_arguments::{GlobalOutcome, global_layout};
19use crate::transaction_git::{GitContext, subcommand_of};
20use std::ffi::{OsStr, OsString};
21use std::path::Path;
22
23/// The configuration key, in the canonical form Git lists it.
24pub const KEY: &str = "checkout.workers";
25
26/// The option the wrapper adds: one worker per logical CPU.
27const PARALLEL: &str = "checkout.workers=0";
28
29/// What:
30/// How many leading words of the arguments are global options, which is where the subcommand starts.
31/// Why:
32/// The wrapper reads global options with one reader (`--git-dir <path>` and `--work-tree <path>` take a separate value, `-c`
33/// and `-C` too, and `--option=value` forms are one word), so the position agrees with where the wrapper's own hook options go.
34/// Arguments with no subcommand have no position, and the whole list is global options.
35///
36/// In TS you'd write (pseudocode):
37/// ```ts
38/// const prefixLength = layout.outcome === 'command' ? layout.prefixLen : args.length;
39/// ```
40fn global_prefix_length(arguments: &[OsString]) -> usize {
41 let layout = global_layout(arguments);
42 return if layout.outcome == GlobalOutcome::Command { layout.prefix_len } else { arguments.len() };
43}
44
45/// What:
46/// Whether a global option the person typed before the subcommand names the key: `-c checkout.workers=<n>`,
47/// `-ccheckout.workers=<n>` or `--config-env=checkout.workers=<variable>`.
48/// Why:
49/// A typed value is the person's, and the options come before the subcommand, so reading the words up to it finds them all.
50///
51/// In TS you'd write (pseudocode):
52/// ```ts
53/// args.slice(0, subcommandIndex).some((word) => word.toLowerCase().includes('checkout.workers'))
54/// ```
55pub fn typed_before_subcommand(arguments: &[OsString]) -> bool {
56 let before: &[OsString] = &arguments[..global_prefix_length(arguments)];
57 return before.iter().any(|word: &OsString| { return word.to_string_lossy().to_lowercase().contains(KEY); });
58}
59
60/// What:
61/// Whether the environment sets the key: a `GIT_CONFIG_KEY_<n>` variable naming it, or `GIT_CONFIG_PARAMETERS` mentioning it.
62/// Why:
63/// Git reads these after every file, so a value there is the person's whatever the files say; the check is on the text, so a
64/// spelling the reader would decline is still found.
65///
66/// In TS you'd write (pseudocode):
67/// ```ts
68/// Object.entries(env).some(([name, value]) => (name.startsWith('GIT_CONFIG_KEY_') || name === 'GIT_CONFIG_PARAMETERS') && value.toLowerCase().includes('checkout.workers'))
69/// ```
70pub fn environment_sets<'a>(variables: impl IntoIterator<Item = (&'a OsStr, &'a OsStr)>) -> bool {
71 return variables.into_iter().any(|(name, value): (&OsStr, &OsStr)| {
72 let variable: String = name.to_string_lossy().into_owned();
73 return (variable.starts_with("GIT_CONFIG_KEY_") || variable == "GIT_CONFIG_PARAMETERS") && value.to_string_lossy().to_lowercase().contains(KEY);
74 });
75}
76
77/// What:
78/// Whether any entry of the configuration read has the key, in any scope.
79/// Why:
80/// The listing holds every file Git reads, in precedence order; one entry of any value is the person's.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// snapshot.entries.some((entry) => entry.key === 'checkout.workers')
85/// ```
86pub fn configured(snapshot: &ConfigSnapshot) -> bool {
87 return snapshot.entries().iter().any(|entry| { return entry.key == KEY; });
88}
89
90/// What:
91/// Whether the wrapper may give the checkout parallel workers: nothing the person typed, exported or configured sets the key.
92/// Why:
93/// A configuration that cannot be read leaves the answer unknown, and unknown means the person's setting may exist, so
94/// nothing is added.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// const wanted = await parallelCheckoutWanted({ context, cwd, arguments });
99/// ```
100pub fn parallel_checkout_wanted(context: &GitContext, cwd: &Path, arguments: &[OsString]) -> bool {
101 if typed_before_subcommand(arguments) {
102 debug("parallelCheckoutWanted", "the person typed the key; nothing is added");
103 return false;
104 }
105 let process: Vec<(OsString, OsString)> = std::env::vars_os().collect();
106 let exported = process.iter().chain(context.overlay.iter()).map(|(name, value): &(OsString, OsString)| { return (name.as_os_str(), value.as_os_str()); });
107 if environment_sets(exported) {
108 debug("parallelCheckoutWanted", "the environment sets the key; nothing is added");
109 return false;
110 }
111 let Some(snapshot) = read_config_snapshot(context, cwd) else {
112 debug("parallelCheckoutWanted", "the configuration could not be read; nothing is added");
113 return false;
114 };
115 let wanted: bool = !configured(&snapshot);
116 debug("parallelCheckoutWanted", if wanted { "no configuration file sets the key; the option is added" } else { "a configuration file sets the key; nothing is added" });
117 return wanted;
118}
119
120/// What:
121/// Put `-c checkout.workers=0` in front of the subcommand of the arguments.
122/// Why:
123/// Git takes `-c` only as a global option, before the subcommand, after the options the person and the wrapper put there.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// args.splice(subcommandIndex, 0, '-c', 'checkout.workers=0');
128/// ```
129pub fn insert_parallel_checkout(arguments: &mut Vec<OsString>) {
130 let layout = global_layout(arguments.as_slice());
131 if layout.outcome != GlobalOutcome::Command {
132 return;
133 }
134 arguments.splice(layout.prefix_len..layout.prefix_len, [OsString::from("-c"), OsString::from(PARALLEL)]);
135}
136
137/// What:
138/// Whether the arguments are the wrapper's own checkout of a new worktree after a snapshot or clone attempt failed:
139/// `reset --hard` as the subcommand.
140/// Why:
141/// That run is the checkout the person's `git worktree add` would have made, so it takes the same option; every other step
142/// of the ladder is a bookkeeping or difference step and stays as it is.
143///
144/// In TS you'd write (pseudocode):
145/// ```ts
146/// subcommand(args) === 'reset' && args.includes('--hard')
147/// ```
148pub fn is_fallback_checkout(arguments: &[OsString]) -> bool {
149 return matches!(subcommand_of(arguments), Some((_index, name)) if name == "reset") && arguments.iter().any(|word: &OsString| { return word == "--hard"; });
150}
151
152/// Parallel-checkout controls stay out of the release executable.
153#[cfg(test)]
154#[path = "checkout_workers_tests.rs"]
155mod tests;