Skip to main content

git_policy_cli/push/
child_class.rs

1//! What:
2//!  The two kinds of Git step a commit starts for itself, and the quiet window and the notice grace of each.
3//!  A step is ended only when it has shown no sign of work for its window (`child_watch.rs`); elapsed time alone never ends
4//!  one (owner, 2026-11-03: "Stop only on no progress"; pick 4 of `doc/planning/cli-git-shipped-limits-audit.md`, the
5//!  coordinator's under the owner's delegation and open to the owner's veto).
6//! Why:
7//!  One window for every step had to be long enough for the slowest, so a stalled `rev-parse` held a commit for as long
8//!  as a stalled `pack-objects`, and the first notice came after one second for steps that only needed a moment.
9//!  The classes follow what a step touches, measured on the repository this crate lives in
10//!  (`doc/planning/cli-git-commit-timeouts.md`):
11//!  a query reads refs, commits and trees and ends in milliseconds whatever the worktree holds, while a step that
12//!  writes objects, or that compares the index with the worktree, costs time in proportion to the bytes it reads
13//!  (`reset`, `update-index --refresh` and `ls-files --modified` took 2.9 s on a stat-dirty 20 000-file index against
14//!  0.03 s on a clean one, and a cold disk multiplies that).
15//!  The user's own `git commit` with its hooks is not a class: its runtime belongs to the user and it is never bounded.
16//!
17//! In TS you'd write (pseudocode):
18//! ```ts
19//! const limit = QUERY_SUBCOMMANDS.includes(subcommandOf(args)) ? QUERY_LIMIT_MS : BULK_LIMIT_MS;
20//! ```
21
22/// Positive-seconds parsing shared with the lock wait limits.
23use crate::bounded_push::positive_seconds;
24/// The name of the subcommand a request runs.
25use crate::transaction_git::subcommand_of;
26/// Operating-system text of the arguments and of the environment value.
27use std::ffi::{OsStr, OsString};
28/// Time spans.
29use std::time::Duration;
30
31/// Environment variable that sets, in whole seconds, how long any Git step the wrapper starts for itself may show no sign of
32///  work before it is ended, whatever its class.
33pub const CHILD_WAIT_VARIABLE: &str = "CLI_GIT_CHILD_WAIT_SECONDS";
34
35/// How long a query step may show no sign of work before it is ended.
36///  Citation: none, and **unmeasured as a quiet window**. The figure was the wall-clock bound of a query step, derived from
37///  elapsed times (`doc/planning/cli-git-commit-timeouts.md`), which measure a different thing from a quiet gap. No log held a
38///  quiet gap before `quiet_ledger.rs`; the timing records now carry the longest gap of each step, and the audit records what
39///  the existing logs bound (`doc/planning/cli-git-shipped-limits-audit.md`, "Time limits on Git child steps of a commit").
40///  The number stays until a measured interval replaces it.
41pub const PLUMBING_QUIET_WINDOW: Duration = Duration::from_secs(60);
42
43/// How long a bulk step may show no sign of work before it is ended.
44///  Citation: none, and **unmeasured as a quiet window**, for the reason given at `PLUMBING_QUIET_WINDOW`.
45pub const OBJECT_QUIET_WINDOW: Duration = Duration::from_secs(300);
46
47/// How long a query step runs before the first notice.
48pub const PLUMBING_NOTICE_GRACE: Duration = Duration::from_secs(5);
49
50/// How long a bulk step runs before the first notice.
51pub const OBJECT_NOTICE_GRACE: Duration = Duration::from_secs(10);
52
53/// Subcommands that only read refs, commits and trees, or write one ref or one tree from the index as it is:
54///  they end in milliseconds whatever the worktree holds.
55///  Every other subcommand, named or not, is a bulk step and gets the longer window instead of an early stop:
56///  `pack-objects`, `index-pack`, `add`, `hash-object` and `maintenance` move bytes, and `reset`, `read-tree`,
57///  `update-index`, `ls-files`, `diff`, `diff-index`, `show` and `ls-remote` refresh the index, compare it with the
58///  worktree or talk to a remote.
59pub const PLUMBING_SUBCOMMANDS: [&str; 13] = [
60    "rev-parse",
61    "rev-list",
62    "symbolic-ref",
63    "for-each-ref",
64    "ls-tree",
65    "cat-file",
66    "diff-tree",
67    "merge-base",
68    "write-tree",
69    "update-ref",
70    "config",
71    "remote",
72    "version",
73];
74
75/// What:
76///  The kind of a Git step, which decides its quiet window and when its first notice comes.
77/// Why:
78///   The two kinds differ by orders of magnitude in how long a healthy step takes, and so in how long it may be quiet.
79///
80/// In TS you'd write (pseudocode):
81/// ```ts
82/// type ChildClass = 'plumbing' | 'object';
83/// ```
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum ChildClass {
86    /// A query of refs, commits and trees.
87    Plumbing,
88    /// A step that writes objects, compares the index with the worktree, or is not known to be a query.
89    Object,
90}
91
92impl ChildClass {
93    /// How a notice or a stop sentence names the kind of step.
94    pub fn label(self) -> &'static str {
95        return match self {
96            Self::Plumbing => "a quick query step",
97            Self::Object => "a bulk step",
98        };
99    }
100
101    /// How long a step of this class may show no sign of work when the variable is absent or unusable.
102    pub fn default_limit(self) -> Duration {
103        return match self {
104            Self::Plumbing => PLUMBING_QUIET_WINDOW,
105            Self::Object => OBJECT_QUIET_WINDOW,
106        };
107    }
108
109    /// How long a step of this class runs before its first notice.
110    pub fn notice_grace(self) -> Duration {
111        return match self {
112            Self::Plumbing => PLUMBING_NOTICE_GRACE,
113            Self::Object => OBJECT_NOTICE_GRACE,
114        };
115    }
116
117    /// The window for a value of the variable: the person's whole seconds, or this class's default for a missing,
118    ///  zero or malformed value, never "no window".
119    pub fn limit_from(self, value: Option<&OsStr>) -> Duration {
120        let Some(text) = value else {
121            return self.default_limit();
122        };
123        return positive_seconds(CHILD_WAIT_VARIABLE, text).unwrap_or_else(|| { return self.default_limit(); });
124    }
125
126    /// The window this process runs this class under: the variable in its environment, or the class default.
127    pub fn limit(self) -> Duration {
128        return self.limit_from(std::env::var_os(CHILD_WAIT_VARIABLE).as_deref());
129    }
130}
131
132/// What:
133///  The class of the Git step a request runs.
134/// Why:
135///   A step is a query only when its subcommand is named in `PLUMBING_SUBCOMMANDS`; every other step, a request with
136///  no subcommand included, gets the longer bound.
137///
138/// In TS you'd write (pseudocode):
139/// ```ts
140/// QUERIES.includes(subcommandOf(args)) ? 'plumbing' : 'object'
141/// ```
142pub fn class_of(arguments: &[OsString]) -> ChildClass {
143    let Some((_, name)) = subcommand_of(arguments) else {
144        return ChildClass::Object;
145    };
146    if PLUMBING_SUBCOMMANDS.contains(&name.as_str()) {
147        return ChildClass::Plumbing;
148    }
149    return ChildClass::Object;
150}
151
152/// Class controls stay out of the release executable.
153#[cfg(test)]
154#[path = "child_class_tests.rs"]
155mod tests;