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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! Auto-`git init` when `tm` is invoked in a directory that is not a git
//! repository (#6274).
//!
//! Why: `tm` used to stop at the front door of a plain directory. Bare `tm`
//! printed [`super::misc::NON_GIT_FALLBACK_HINT`] ("not in a git project") and
//! did nothing; `tm launch` failed with "no git origin remote found". The
//! operator's next move was always the same — run `git init`, re-run `tm`. This
//! module takes that step for them and then lets the ordinary flow continue
//! exactly as it does for a git repository with no origin remote — which since
//! #6276 means a working session in that checkout, not a second refusal (see
//! [`super::origin_plan`]).
//! What: [`ensure_git_repo`] probes for an existing repository, applies the
//! sanity guards, runs `git init`, and prints a one-line notice. The decision
//! is the pure [`plan_auto_init`]; the "is there a repository here" stderr
//! discrimination is the pure [`stderr_means_no_repository`]. Nothing here
//! installs the `git` executable — a missing `git` is a prerequisite error.
//! #7749: the upward probe is not enough. A directory with a repository BENEATH
//! it is a workspace parent, and a `.git` written here becomes an ancestor
//! repository for every project under it, so the guards also run the downward
//! [`trusty_mpm::core::child_repo_scan::scan_for_child_repo`] — the same scan
//! the `CLAUDE.md` seed guard uses — and refuse on a child repository or on a
//! scan that could not finish.
//! #9495: git's "no repository" wording is corroborated by an ancestor-`.git`
//! witness before it counts as absence; see [`classify_rev_parse_failure`].
//! Test: `auto_git_init_tests.rs`.
use ;
use ;
/// The git executable this module drives.
const GIT_PROGRAM: &str = "git";
/// The stderr prefix that means git found no repository at or above a
/// directory.
///
/// Why: the SHORT phrase `not a git repository` is ambiguous in this codebase —
/// git answers `fatal: not a git repository: (null)` for a STALE WORKTREE
/// POINTER, which is a broken repository rather than no repository at all. The
/// opening parenthesis is what separates the two. Anything else that fails is
/// reported, never auto-initialized.
/// What: covers both wordings of git's upward-discovery failure —
/// `(or any of the parent directories)` and `(or any parent up to mount point
/// <dir>)` — and neither colon form. The same prefix `untracked_sync` matches.
/// Test: `no_repo_stderr_matches_the_long_phrase`,
/// `classify_accepts_the_mount_boundary_wording_with_no_git_above`.
// #9495: match the shared prefix; the full clause missed git's mount-boundary wording.
const NO_REPO_STDERR_PREFIX: &str = "not a git repository (or any ";
/// Whether git already has a repository for the directory.
///
/// Why: "is this a repo" must catch MORE than a work tree — a bare repository
/// answers `rev-parse --show-toplevel` with a failure while being very much a
/// repository, and `git init` there re-initializes it. `rev-parse --git-dir`
/// answers the broader question once: a work tree at any depth, a bare repo, or
/// the inside of a `.git` directory all report `Present`.
/// Test: `auto_init_leaves_a_repo_root_alone`,
/// `auto_init_does_not_init_inside_another_work_tree`,
/// `auto_init_leaves_a_bare_repository_alone`.
pub
/// Why a sanity guard refused to create a repository.
///
/// Why: `git init` writes a `.git` directory that git then treats as a
/// project root for every descendant. In `$HOME` or at `/` that is a
/// long-lived mistake nobody asked for, so those two directories refuse
/// rather than initialize. #7749: a workspace parent is the same mistake one
/// level down — the `.git` adopts every child project instead of every sibling
/// directory — and a scan that could not finish cannot rule one out.
/// Test: `plan_refuses_the_home_directory`, `plan_refuses_the_filesystem_root`,
/// `plan_refuses_a_workspace_parent`, `plan_refuses_an_unfinished_scan`.
pub
/// The decision [`ensure_git_repo`] acts on.
pub
/// What [`ensure_git_repo`] actually did.
pub
/// Does a failed `git rev-parse` stderr mean "there is no repository here"?
///
/// Why: see [`NO_REPO_STDERR_PREFIX`] — a substring test on the short phrase
/// also matches a BROKEN repository, and auto-initializing over one of those is
/// the worst thing this module could do. Fails closed: an unrecognised failure
/// is not "no repository". The match is necessary, not sufficient:
/// [`classify_rev_parse_failure`] also requires no `.git` above.
/// What: `true` only when [`NO_REPO_STDERR_PREFIX`] appears in `stderr`.
/// Test: `no_repo_stderr_matches_the_long_phrase`,
/// `no_repo_stderr_rejects_the_stale_worktree_pointer`.
pub
/// Decide what to do about `dir`, given git's verdict, the home directory, and
/// a downward scan for child repositories.
///
/// Why: the whole decision, free of process spawning and filesystem access, so
/// every arm is asserted directly. `context` is checked FIRST so a home
/// directory (or `/`) that IS already a repository keeps behaving exactly as it
/// does today — the guards only ever prevent a NEW repository.
/// What: `AlreadyGit` when git reports a repository; `Refuse` when `dir` is the
/// filesystem root or the home directory; `Init` otherwise. #7749: `Init` is
/// reached only through `scan`, which is `Clear` exactly when a completed walk
/// found no repository beneath `dir` — `Found` and `Incomplete` both refuse, so
/// a workspace parent never gets a `.git` and an unfinished walk never reads as
/// absence. `scan` is a closure, so the walk costs nothing on the arms decided
/// before it. `dir` and `home` are expected canonicalized by the caller so the
/// comparison is not defeated by a symlinked `$HOME`.
/// Test: `plan_initializes_a_plain_directory`, `plan_refuses_the_home_directory`,
/// `plan_refuses_the_filesystem_root`,
/// `plan_leaves_an_existing_repo_alone_even_in_the_home_directory`,
/// `plan_refuses_a_workspace_parent`, `plan_refuses_an_unfinished_scan`,
/// `plan_does_not_scan_when_an_earlier_guard_already_decided`.
pub
/// The one-line notice printed after a successful `git init`.
///
/// Test: `initialized_message_names_the_directory`.
pub
/// The stderr notice printed when a sanity guard refuses.
///
/// Why: a refusal that does not say WHY reads as a malfunction. Each arm names
/// the directory and the specific reason it is not a project. #7749: the two
/// scan arms carry what the scan learned — the child repository, or what
/// stopped the walk — so the operator can act on it rather than re-deriving it.
/// #7749 review: they also carry their OWN remedy. "Run tm from a project
/// directory" is right for `$HOME` and `/`, and wrong for a scan arm — tm was
/// run from a directory the operator considers a project, and
/// [`trusty_mpm::core::child_repo_scan::WORKSPACE_SCAN_BUDGET`] is 256
/// directories, so a sizeable plain directory hits
/// [`AutoInitRefusal::ScanIncomplete`] routinely. Both scan arms give the
/// remedy `SeedRefusal::ScanIncomplete` gives.
/// Test: `refusal_message_names_the_home_directory`,
/// `refusal_message_names_the_filesystem_root`,
/// `refusal_message_names_the_child_repository`,
/// `refusal_message_says_what_stopped_the_scan`.
pub
/// The prerequisite error for a missing `git` executable.
///
/// Why: the owner directive is "install git in the directory" — a repository,
/// not the git binary. When `git` itself is absent there is nothing this
/// module can do, and the error has to name git so the operator's next move is
/// obvious. Nothing has been written at the point this fires.
/// Test: `missing_git_error_names_git_and_the_directory`,
/// `ensure_reports_a_missing_git_executable_without_writing`.
pub
/// Run `git -C <dir> <args>` and capture its output.
///
/// Why: both git calls in this module need the same spawn-failure mapping, and
/// a `NotFound` spawn error is the prerequisite case rather than a git failure.
/// `program` is a parameter purely so the missing-executable path is testable
/// without mutating `PATH` for the whole test binary.
/// Test: `ensure_reports_a_missing_git_executable_without_writing`.
/// Ask git whether `dir` already has a repository.
///
/// What: `git -C <dir> rev-parse --git-dir`. Success is [`RepoContext::Present`];
/// a failure is classified by [`classify_rev_parse_failure`].
/// Test: `auto_init_leaves_a_bare_repository_alone`,
/// `auto_init_initializes_a_plain_directory`.
/// What a failed `git rev-parse --git-dir` means for `dir`.
///
/// Why: git's "no repository" message is not proof there is no repository.
/// Git prints it whenever discovery stopped before it reached one — an
/// unreadable `.git`, `GIT_CEILING_DIRECTORIES`, or a filesystem boundary — and
/// a `git init` there nests a second repository inside the real checkout
/// (#9495). `symlink_metadata` on an ancestor `.git` needs only the parent's
/// search bit, so it is a witness git does not use. Mirrors
/// `untracked_sync::classify_rev_parse_failure`.
/// What: [`RepoContext::Absent`] only when [`stderr_means_no_repository`]
/// holds AND no ancestor of the canonical `dir` (itself included) has a `.git`
/// entry. Every other failure is an `Err` carrying git's own stderr, the same
/// contract as any unreadable verdict: reported, never initialized over. A
/// failed canonicalization is an `Err` too, because `Path::ancestors` walks
/// lexically and a non-canonical chain can skip the real `.git`.
/// Test: `auto_init_refuses_the_classic_no_repo_wording_under_a_real_git`,
/// `classify_accepts_the_mount_boundary_wording_with_no_git_above`,
/// `classify_refuses_the_mount_boundary_wording_under_a_real_git`,
/// `classify_keeps_every_other_failure_an_error`.
/// Make `dir` a git repository when it is a plain directory (#6274).
///
/// Why: the entry point both `tm` (guided default) and `tm launch` call before
/// project detection, so a plain directory becomes an ordinary no-origin git
/// project instead of a refusal. It never touches a directory that already has
/// a repository, so every path that works today is unchanged.
/// What: probes with [`repo_context`], decides with [`plan_auto_init`], runs
/// `git init` and prints [`initialized_message`] for the `Init` arm, prints
/// [`refusal_message`] for the `Refuse` arm. #7749: the `scan` seam
/// [`plan_auto_init`] consults is [`scan_for_child_repo`] over `dir` itself,
/// evaluated only when no cheaper guard already decided. Errors only when `git`
/// is missing (see [`missing_git_error`]), when git's verdict is unreadable, or
/// when `git init` itself fails.
/// Test: `auto_init_initializes_a_plain_directory`,
/// `auto_init_refuses_a_workspace_parent_with_a_deep_child_repository`,
/// `auto_init_refuses_when_the_downward_scan_cannot_finish`, and the rest of
/// `auto_git_init_tests.rs`.
pub
/// [`ensure_git_repo`] with the home directory and the git executable injected.
///
/// Why: the two pieces of ambient state this decision depends on. Injecting
/// them lets the guard and prerequisite arms be tested without mutating `$HOME`
/// or `PATH` for the whole test binary.
/// Test: `auto_init_refuses_the_home_directory`,
/// `ensure_reports_a_missing_git_executable_without_writing`.
pub