git_policy_cli/maintenance_run.rs
1//! What:
2//! Running automatic maintenance after a landing:
3//! one `git config` read for the maintenance keys,
4//! then `git maintenance run --auto --quiet`, detached or not, in the real worktree.
5//! Its exit status is returned for the caller to ignore, as native Git does.
6//! Why:
7//! Every landing migrates its objects into one pack;
8//! without this step the pack count grows by one per commit and every later Git process searches every pack.
9//! Failures are logged and never fail the landed commit.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! await runAutoMaintenance({ gitPath, cwd, globalArgs });
14//! ```
15
16/// The decision and its error type.
17use super::maintenance_decision::{
18 MAINTENANCE_CONFIG_PATTERN, MaintenanceDecision, MaintenanceError, auto_maintenance_arguments,
19 parse_config_records,
20};
21/// Diagnostics on standard error, switched like the incumbent's console sink.
22use super::diagnostic_log;
23/// The real-Git context and the captured output of one start.
24use super::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
25/// `Path` is a borrowed filesystem path.
26use std::path::Path;
27
28/// Tag the incumbent's logger prints for this function, so diagnostics match.
29const TAG: &str = "runAutoMaintenance";
30
31/// Exit status `parse_options` uses for an unknown option,
32/// which Git before 2.47 reports for `maintenance run --detach`.
33const USAGE_EXIT_CODE: i32 = 129;
34
35/// Exit status of `git config --get-regexp` when no key matches.
36const CONFIG_UNSET_EXIT_CODE: i32 = 1;
37
38/// What:
39/// What automatic maintenance did.
40/// Why:
41/// Configuration can turn it off, which is not an exit status;
42/// the caller distinguishes the two.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// type Outcome = typeof AUTO_MAINTENANCE_DISABLED | number;
47/// ```
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum AutoMaintenanceOutcome {
50 /// Configuration disabled maintenance; no maintenance Git ran.
51 Disabled,
52 /// Git's exit status for the maintenance run, or for the config read when that read failed.
53 Exited(i32),
54}
55
56/// What:
57/// Git's exit status, where a signal ending Git counts as `1`.
58/// Why:
59/// The incumbent reads a missing exit code as `1` (`exitCode ?? 1`).
60///
61/// In TS you'd write (pseudocode):
62/// ```ts
63/// child.exitCode ?? 1
64/// ```
65fn exit_status(code: Option<i32>) -> i32 {
66 return code.unwrap_or(1);
67}
68
69/// What:
70/// Map a start failure to a maintenance error that names the command.
71/// Why:
72/// The caller logs one line that says which Git start could not run.
73///
74/// In TS you'd write (pseudocode):
75/// ```ts
76/// throw new CommitTransactionGitError(`${command} could not start`);
77/// ```
78fn start_failure(command: &str, error: std::io::Error) -> MaintenanceError {
79 return MaintenanceError::Start(format!("{command} could not start: {error}"));
80}
81
82/// What:
83/// Run automatic maintenance in the real worktree with the caller's global options.
84/// Why:
85/// The read and the run happen in the same order the incumbent uses,
86/// so the decision and its effect agree on one configuration snapshot per read.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// // config read, then `git maintenance run --auto --quiet --detach|--no-detach`; retry without the flag on 129
91/// ```
92pub fn run_auto_maintenance(
93 context: &GitContext,
94 cwd: &Path,
95) -> Result<AutoMaintenanceOutcome, MaintenanceError> {
96 // Config lookup; exit 1 means no key is set.
97 let lookup: GitOutput = run_git(
98 context,
99 &GitRequest::new(
100 cwd,
101 &[
102 "config",
103 "--type=bool-or-int",
104 "--null",
105 "--get-regexp",
106 MAINTENANCE_CONFIG_PATTERN,
107 ],
108 ),
109 )
110 .map_err(|error| return start_failure("git config", error))?;
111 let lookup_code: i32 = exit_status(lookup.code);
112 if lookup_code != 0 && lookup_code != CONFIG_UNSET_EXIT_CODE {
113 diagnostic_log::warn(
114 TAG,
115 format!(
116 "skipping automatic maintenance: git config exited {lookup_code}: {}",
117 String::from_utf8_lossy(&lookup.stderr).trim()
118 )
119 .as_str(),
120 );
121 return Ok(AutoMaintenanceOutcome::Exited(lookup_code));
122 }
123 // An unset key prints nothing, so an exit of 1 parses as an empty configuration.
124 let config_bytes: &[u8] = if lookup_code == 0 {
125 lookup.stdout.as_slice()
126 } else {
127 &[]
128 };
129 let config = parse_config_records(config_bytes)?;
130 // Native maintenance arguments, or the disabled outcome.
131 let arguments: Vec<&'static str> = match auto_maintenance_arguments(&config) {
132 MaintenanceDecision::Disabled => {
133 diagnostic_log::debug(
134 TAG,
135 "automatic maintenance disabled by configuration",
136 );
137 return Ok(AutoMaintenanceOutcome::Disabled);
138 }
139 MaintenanceDecision::Run(arguments) => arguments,
140 };
141 diagnostic_log::debug(TAG, format!("running git {}", arguments.join(" ")).as_str());
142 // Maintenance outcome; a detached run returns once the background process is forked.
143 let result: GitOutput = run_git(
144 context,
145 &GitRequest::new(cwd, arguments.as_slice()),
146 )
147 .map_err(|error| return start_failure("git maintenance run", error))?;
148 let result_code: i32 = exit_status(result.code);
149 if result_code != USAGE_EXIT_CODE {
150 diagnostic_log::debug(
151 TAG,
152 format!("automatic maintenance exited {result_code}").as_str(),
153 );
154 return Ok(AutoMaintenanceOutcome::Exited(result_code));
155 }
156 // Git before 2.47 has no detach option; its native form omits it and gc detaches on its own.
157 let flag: &str = arguments.last().copied().unwrap_or("");
158 diagnostic_log::debug(
159 TAG,
160 format!(
161 "git rejected {flag}; retrying in the pre-2.47 form: {}",
162 String::from_utf8_lossy(&result.stderr).trim()
163 )
164 .as_str(),
165 );
166 let fallback_arguments: &[&str] = &arguments[..arguments.len() - 1];
167 let fallback: GitOutput = run_git(
168 context,
169 &GitRequest::new(cwd, fallback_arguments),
170 )
171 .map_err(|error| return start_failure("git maintenance run", error))?;
172 let fallback_code: i32 = exit_status(fallback.code);
173 diagnostic_log::debug(
174 TAG,
175 format!("automatic maintenance exited {fallback_code}").as_str(),
176 );
177 return Ok(AutoMaintenanceOutcome::Exited(fallback_code));
178}
179
180/// Runner controls live beside the runner.
181#[cfg(test)]
182#[path = "maintenance_run_tests.rs"]
183mod tests;