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