Skip to main content

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;