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
//! Idempotently disable git's own background maintenance/gc on a managed
//! checkout, once, at provisioning time (#7171).
//!
//! Why: [`trusty_common::git`] protects every git invocation THIS codebase
//! makes by passing `-c maintenance.auto=false -c gc.auto=0` on argv, which
//! outranks config — but it protects nothing an OPERATOR types directly into
//! a shell inside one of the ~25 worktrees a base clone can carry. Ordinary
//! git commands (`fetch`, `commit`, `checkout`, …) still decide whether to
//! trigger `git maintenance run --auto` by reading `maintenance.auto` from
//! config, so an operator's own `git pull` in a worktree was exactly as able
//! to fire the storm as the daemon's own code. Writing the setting into the
//! repo's shared `.git/config` closes that gap: every worktree of one base
//! clone shares `GIT_COMMON_DIR`, so ONE write at clone/registration time
//! covers every worktree the base will ever host, the same sharing property
//! `core::push_guard` already relies on for its `pre-push` hook install.
//! What: [`disable_auto_maintenance`] runs `git config --local
//! maintenance.auto false` and `git config --local gc.auto 0` against a
//! repository path — plain `git config --local`, not the argv-level `-c`
//! form, because this needs to persist in the checkout rather than apply to
//! one invocation. [`disable_and_log`] wraps it exactly like
//! [`super::push_guard::install_and_log`]: best-effort, never fails the
//! caller's clone or registration, `warn!`-logs any failure. Both writes are
//! plain `git config --local` calls, so re-running either is a no-op — safe
//! to call on every clone/registration, not just the first.
//! Test: `disable_auto_maintenance_writes_both_keys`,
//! `disable_auto_maintenance_is_idempotent`,
//! `disable_auto_maintenance_reports_a_non_repo_path`.
use std::path::Path;
/// The `git config --local <key> <value>` pairs this module writes.
///
/// Why: kept as data rather than duplicated call sites so
/// [`disable_auto_maintenance`]'s "both keys or a named failure" contract
/// cannot drift from what it actually iterates.
/// What: mirrors [`trusty_common::git::MAINTENANCE_DISABLE_ARGS`]'s two keys,
/// spelled as `git config` values (`"false"`/`"0"`) rather than `-c` argv
/// pairs (`"maintenance.auto=false"`/`"gc.auto=0"`) — the persisted and the
/// per-invocation forms use git's two different value spellings for the same
/// setting.
/// Test: `disable_auto_maintenance_writes_both_keys`.
const MAINTENANCE_CONFIG: &[(&str, &str)] = &[("maintenance.auto", "false"), ("gc.auto", "0")];
/// Write [`MAINTENANCE_CONFIG`] into `repo_path`'s local git config.
///
/// Why: see the module doc — this is the provisioning-time half of #7171.
/// What: runs `git -C <repo_path> config --local <key> <value>` once per
/// entry in [`MAINTENANCE_CONFIG`], via [`trusty_common::git::command_in`] so
/// the write itself cannot trigger a maintenance run either. Fails on the
/// first key that cannot be written (not a git repo, an unreadable config, a
/// spawn failure) rather than partially applying.
/// Test: `disable_auto_maintenance_writes_both_keys`,
/// `disable_auto_maintenance_is_idempotent`,
/// `disable_auto_maintenance_reports_a_non_repo_path`.
pub fn disable_auto_maintenance(repo_path: &Path) -> Result<(), String> {
for (key, value) in MAINTENANCE_CONFIG {
let out = trusty_common::git::command_in(repo_path)
.args(["config", "--local", key, value])
.output()
.map_err(|e| format!("git config --local {key} failed to spawn: {e}"))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(format!(
"git config --local {key} {value} failed ({}): {}",
out.status,
stderr.trim()
));
}
}
Ok(())
}
/// Best-effort [`disable_auto_maintenance`]: `warn!`-logs a failure, never
/// returns one.
///
/// Why: provisioning a managed checkout (a fresh base clone, an explicit
/// `mpm.projects.register`) must not fail — or partially roll back — over a
/// setting that is a storm-prevention nicety, not a correctness requirement,
/// exactly the same call this module mirrors [`super::push_guard::install_and_log`]
/// on.
/// What: logs `info!` on success, `warn!` (naming `repo_path` and the error)
/// on failure — including the ordinary case of `repo_path` not being a git
/// repository at all, which `register_project` cannot rule out ahead of the
/// call.
/// Test: covered indirectly by the call sites' own integration tests; the
/// pure success/failure branches are exercised directly by
/// `disable_auto_maintenance`'s own tests above.
pub fn disable_and_log(repo_path: &Path) {
match disable_auto_maintenance(repo_path) {
Ok(()) => {
tracing::info!(
repo = %repo_path.display(),
"git auto-maintenance disabled on managed checkout (#7171)"
);
}
Err(e) => {
tracing::warn!(
repo = %repo_path.display(),
"git auto-maintenance NOT disabled (non-fatal, #7171): {e}"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn git_ok(dir: &Path, args: &[&str]) -> bool {
trusty_common::git::command_in(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn git_config_get(dir: &Path, key: &str) -> Option<String> {
let out = trusty_common::git::command_in(dir)
.args(["config", "--get", key])
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
/// A real repo, or `None` when `git` is unavailable on the runner — mirrors
/// the established pattern in `session_manager::decommission_worktree_tests`.
fn repo() -> Option<tempfile::TempDir> {
let dir = tempfile::tempdir().ok()?;
if !git_ok(dir.path(), &["init", "-q", "."]) {
return None;
}
Some(dir)
}
#[test]
fn disable_auto_maintenance_writes_both_keys() {
let Some(dir) = repo() else {
return;
};
disable_auto_maintenance(dir.path()).expect("config write succeeds");
assert_eq!(
git_config_get(dir.path(), "maintenance.auto").as_deref(),
Some("false")
);
assert_eq!(git_config_get(dir.path(), "gc.auto").as_deref(), Some("0"));
}
#[test]
fn disable_auto_maintenance_is_idempotent() {
let Some(dir) = repo() else {
return;
};
disable_auto_maintenance(dir.path()).expect("first write succeeds");
disable_auto_maintenance(dir.path()).expect("second write succeeds");
assert_eq!(
git_config_get(dir.path(), "maintenance.auto").as_deref(),
Some("false")
);
}
#[test]
fn disable_auto_maintenance_reports_a_non_repo_path() {
let Ok(dir) = tempfile::tempdir() else {
return;
};
// Not a git repo at all — `git config --local` must fail, not panic
// or silently succeed.
assert!(disable_auto_maintenance(dir.path()).is_err());
}
#[test]
fn disable_and_log_does_not_panic_on_a_non_repo_path() {
let Ok(dir) = tempfile::tempdir() else {
return;
};
// Best-effort: must not panic even when the underlying write fails.
disable_and_log(dir.path());
}
}