amont-fleet 1.2.1

The amont fleet dashboard: see and repair hook coverage across many repositories
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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
//! Executing a `FixPlan`. The only code here that deletes anything.
//!
//! Three rules, each bought with an incident:
//!
//! 1. **Re-plan, never trust a stale plan.** The caller shows a preview and the
//!    user confirms; between those moments the tree can change. Applying the
//!    plan that was displayed would act on a world that no longer exists, so
//!    apply takes the plan it just computed and verifies its refusals again.
//!
//! 2. **A refusal suppresses the whole repository.** Not the offending file —
//!    the repo. A half-applied fix is precisely how a repo ends up holding both
//!    `pre-commit-ruff.zsh` and `pre-commit-ruff`, running ruff twice, silently,
//!    on whichever repos a loop happened to reach.
//!
//! 3. **Remove before write, and stop the repo on the first error.** Writing
//!    first would leave the retired file next to its replacement, which is the
//!    double-run state again.
//!
//! `make install` destroyed tracked source twice in this repo's history, both
//! times because its guard failed OPEN. Nothing here deletes a path it has not
//! positively established is safe.
//!
//! ## Rule 1 was a claim, not a fact
//!
//! For two releases the doc above said "verifies its refusals again" and only
//! ONE of the five was re-checked. `Unmanaged`, `ForeignHook` and the hooks
//! directory's location were all taken on trust from a plan built before the
//! user was even shown a preview, and the write itself went through
//! `std::fs::write`, which FOLLOWS SYMLINKS.
//!
//! The verified consequence: a repository with four tracked dispatchers under
//! `shared/` and `.git/hooks/*` symlinked to them. `fix --apply` reported
//! `4 written` and left all four TRACKED FILES MODIFIED. The plan's own
//! symlink guard had never been asked, and the one guard that did run — the
//! tracked check — was asking about the link, which is untracked, sits in
//! `.git`, and has nothing alarming about it.
//!
//! So every write and every remove now goes through
//! [`amont_runtime::hookfile`], which is the single owner of "is this ours,
//! and may we touch it?" — it never follows a link, never guesses, and stages
//! every write to a sibling temporary that is `rename`d into place, so
//! REPLACING a link is the only thing that can happen and writing THROUGH one
//! is not a code path that exists. That is a stronger statement than "we check
//! first", because a check can be raced and this cannot.
//!
//! Deleting `write_executable` in favour of `hookfile::stage` + `commit_all`
//! also strengthens `written_shims_are_executable`: the mode is set on the
//! temporary before the rename, so the file is never visible at its
//! destination in a non-executable state — where git would silently not run it.

use serde::Serialize;

use crate::fix::{tracked_refusal, FixPlan, Intent};
use crate::scan::HooksDir;
use crate::shim;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum Outcome {
    /// Files were removed and/or written.
    Applied { removed: usize, written: usize },
    /// Nothing needed doing.
    Unchanged,
    /// The plan refused; nothing was touched.
    Refused,
    /// Something failed partway. Names what stopped it.
    Failed { error: String, at: String },
}

#[derive(Debug, Clone, Serialize)]
pub struct ApplyReport {
    pub repo: std::path::PathBuf,
    pub outcome: Outcome,
}

/// Apply one plan. Returns what actually happened, never what was intended.
pub fn apply(plan: &FixPlan) -> Outcome {
    if plan.refused() {
        return Outcome::Refused;
    }
    if let Some(outcome) = reverify(plan) {
        return outcome;
    }
    if plan.is_noop() {
        return Outcome::Unchanged;
    }

    // Re-check tracked-ness at the moment of action, for a REMOVE and a
    // WRITE alike. The preview may be old, and this is the guard that failed
    // open twice before — both times on a delete, but a write that landed on
    // tracked source (a repo whose `core.hooksPath`, say, points somewhere the
    // planner did not expect when the plan was built) would overwrite it
    // exactly as destructively, just without `rm` in the name.
    for path in plan
        .remove
        .iter()
        .map(|r| &r.path)
        .chain(plan.write.iter().map(|w| &w.path))
    {
        if let Some(refusal) = tracked_refusal(path) {
            return Outcome::Failed {
                error: match refusal {
                    crate::fix::Refusal::TrackedUnknown { why, .. } => {
                        format!("cannot tell whether the path is tracked by git ({why})")
                    }
                    _ => "path is tracked by git".to_string(),
                },
                at: path.display().to_string(),
            };
        }
    }

    // Removals first — writing first would leave a retired file beside its
    // replacement, which is the double-run state. Each goes through
    // `guard_remove` with `expect_ours = false`: the plan has already
    // established what each of these is (ours-but-retired, the node-era
    // `package.json`, or an explicitly opted-in stranger), and `guard_remove`
    // is here for the question the plan cannot answer twice — whether the path
    // is tracked NOW.
    let mut removed = 0;
    for r in &plan.remove {
        if amont_runtime::hookfile::classify(&r.path) == amont_runtime::hookfile::HookFile::Absent {
            // Already gone is the desired state, and must not be counted as
            // work done.
            continue;
        }
        if let Err(refuse) = amont_runtime::hookfile::guard_remove(&r.path, false) {
            return Outcome::Failed {
                error: refuse.explain(),
                at: refuse.path().display().to_string(),
            };
        }
        if let Err(e) = amont_runtime::hookfile::remove_regular(&r.path) {
            return Outcome::Failed {
                error: e.to_string(),
                at: r.path.display().to_string(),
            };
        }
        removed += 1;
    }

    // Every shim is STAGED before any of them lands. Guarding four paths and
    // then writing four files leaves a window in which the third write fails
    // and the repository holds two new dispatchers and two old ones; staging
    // moves every anticipatable failure (no space, no permission, a read-only
    // directory) before the first destination is touched. `commit_all` then
    // `rename`s, which REPLACES a symlink rather than following it.
    let mut staged = Vec::new();
    for write in plan.write.iter().filter(|write| write.changes) {
        // `force = false`, always. There is no `--force` on the fleet path and
        // there should not be: a fleet-wide override is a decision made once
        // about ninety-six repositories nobody looked at.
        if let Err(refuse) = amont_runtime::hookfile::guard_write(&write.path, false) {
            return Outcome::Failed {
                error: refuse.explain(),
                at: refuse.path().display().to_string(),
            };
        }
        match amont_runtime::hookfile::stage(&write.path, &shim::render(&write.baked), true) {
            Ok(s) => staged.push(s),
            Err(e) => {
                return Outcome::Failed {
                    error: e.to_string(),
                    at: write.path.display().to_string(),
                }
            }
        }
    }
    let mut written = match amont_runtime::hookfile::commit_all(staged) {
        Ok(landed) => landed.len(),
        Err(failure) => {
            return Outcome::Failed {
                error: failure.to_string(),
                at: failure.at.display().to_string(),
            }
        }
    };

    if let Some(w) = &plan.write_agents_md {
        // Re-check at the moment of action, same rule as `is_tracked` above:
        // the scan this plan was built from may be stale, and a repo that
        // reached `MatchesGenerated` in between must not be overwritten.
        match amont_runtime::agents_md::check(&w.path) {
            Ok(amont_runtime::agents_md::CheckResult::MatchesGenerated) => {}
            Ok(_) => match amont_runtime::agents_md::write(&w.path) {
                Ok(()) => written += 1,
                Err(e) => {
                    return Outcome::Failed {
                        error: e,
                        at: w.path.display().to_string(),
                    }
                }
            },
            Err(e) => {
                return Outcome::Failed {
                    error: e,
                    at: w.path.display().to_string(),
                }
            }
        }
    }

    Outcome::Applied { removed, written }
}

/// Ask the plan's own refusals again, against the filesystem as it is NOW.
///
/// `Some(Outcome::Refused)` means the world stopped matching the plan between
/// the scan and this moment — which is not a failure, it is the plan correctly
/// declining to act on a world that no longer exists. `None` means proceed.
///
/// Each of the three was, until this function existed, taken on trust:
///
/// - **Unmanaged.** `plan` reads `repo.managed` out of a scan that may be
///   minutes old and, in the TUI, may predate several keystrokes. A repository
///   that stopped being ours in that window would be repaired anyway — the one
///   thing standing between this tool and an application's data repository.
/// - **ForeignHook.** Activation is the mode that writes four dispatchers into
///   a repository nobody had installed into, and the check that somebody else's
///   `pre-commit` is not sitting at one of those paths ran once, at plan time.
/// - **The hooks directory.** Re-resolved rather than re-read from the plan,
///   because `core.hooksPath` is config and config changes. If it now resolves
///   anywhere other than where the plan resolved it, every path in the plan
///   names a file in the wrong directory.
fn reverify(plan: &FixPlan) -> Option<Outcome> {
    let hooks = match crate::scan::hooks_dir_for(&plan.repo_abs) {
        HooksDir::In { path } => path,
        // Outside, or unresolvable. Either way not somewhere we write.
        _ => return Some(Outcome::Refused),
    };
    if plan.hooks.inside() != Some(hooks.as_path()) {
        return Some(Outcome::Refused);
    }
    if plan.intent == Intent::Repair && !crate::scan::is_managed(&hooks) {
        return Some(Outcome::Refused);
    }
    if plan.intent == Intent::Activate
        && !shim::DISPATCHERS
            .iter()
            .all(|n| crate::fix::is_absent_or_ours(&hooks.join(n)))
    {
        return Some(Outcome::Refused);
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fix::Intent;
    use crate::fix::{plan, FixPlan, Refusal, WriteShim};
    use crate::scan;
    use std::path::{Path, PathBuf};

    /// Build a repo on disk and scan it, so the plan is derived the same way
    /// the real tool derives it rather than hand-assembled.
    fn fixture(name: &str) -> (PathBuf, PathBuf) {
        let root = std::env::temp_dir().join(format!("apply-{}-{name}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        let hooks = root.join("r/.git/hooks");
        std::fs::create_dir_all(&hooks).unwrap();
        (root, hooks)
    }

    fn scan_one(root: &Path, binary: &str) -> (scan::Repo, PathBuf) {
        let s = scan::scan(root, 3, binary);
        let r = s.repos.into_iter().next().expect("one repo");
        let abs = root.join(&r.path);
        (r, abs)
    }

    /// A retired per-check shim of OURS — it carries the marker, which is the
    /// whole reason `fix` may remove it.
    ///
    /// These fixtures used to omit the marker line, so what they were actually
    /// exercising was the removal of a file this tool did NOT write. That path
    /// is gone by default (see `Warning::UnrecognizedSubHook`), and the tests
    /// that meant to cover `stale_ours` now say so.
    const STALE_OURS: &str =
        "#!/bin/sh\n# git-templates hook shim.\nexec x --hooks-dir y pre-commit-ruff\n";

    fn healthy_shims(hooks: &Path, binary: &str) {
        for n in shim::DISPATCHERS {
            std::fs::write(hooks.join(n), shim::render(binary)).unwrap();
        }
    }

    #[test]
    fn removes_the_stale_and_writes_the_shims() {
        let (root, hooks) = fixture("basic");
        healthy_shims(&hooks, "/bin/gh");
        std::fs::write(hooks.join("pre-commit-ruff"), STALE_OURS).unwrap();
        std::fs::write(hooks.join("package.json"), "{\"//\":\"Forces Node\"}").unwrap();

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false);
        let out = apply(&p);

        assert!(
            matches!(out, Outcome::Applied { removed: 2, .. }),
            "{out:?}"
        );
        assert!(!hooks.join("pre-commit-ruff").exists());
        assert!(!hooks.join("package.json").exists());
        for n in shim::DISPATCHERS {
            assert!(hooks.join(n).exists(), "{n} must survive");
        }
        let _ = std::fs::remove_dir_all(&root);
    }

    /// The property that makes a fix safe to re-run, and the one that catches a
    /// plan which "succeeds" by doing the same work forever.
    #[test]
    fn applying_twice_is_a_no_op_the_second_time() {
        let (root, hooks) = fixture("idempotent");
        healthy_shims(&hooks, "/bin/gh");
        std::fs::write(hooks.join("pre-commit-ruff"), STALE_OURS).unwrap();

        let (repo, abs) = scan_one(&root, "/bin/gh");
        assert!(matches!(
            apply(&plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false)),
            Outcome::Applied { .. }
        ));

        // Re-scan: the world changed, so the plan must be recomputed.
        let (repo2, abs2) = scan_one(&root, "/bin/gh");
        let p2 = plan(&repo2, &abs2, "/bin/gh", Intent::Repair, false, false);
        assert!(p2.is_noop(), "second plan should be empty: {p2:?}");
        assert_eq!(apply(&p2), Outcome::Unchanged);
        let _ = std::fs::remove_dir_all(&root);
    }

    /// What the tool does for a repo whose shims are missing entirely — the
    /// state `Coinbase-OAuth2` is in, minus the managed flag.
    #[test]
    fn writes_missing_dispatchers() {
        let (root, hooks) = fixture("missing");
        healthy_shims(&hooks, "/bin/gh");
        std::fs::remove_file(hooks.join("pre-push")).unwrap();

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let out = apply(&plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false));
        assert!(
            matches!(out, Outcome::Applied { written: 1, .. }),
            "{out:?}"
        );
        assert_eq!(
            std::fs::read_to_string(hooks.join("pre-push")).unwrap(),
            shim::render("/bin/gh")
        );
        let _ = std::fs::remove_dir_all(&root);
    }

    /// The block is a plain text file, never chmod'd — unlike a shim, git
    /// never executes it.
    #[test]
    fn a_planned_agents_md_write_is_a_plain_file_not_a_shim() {
        let (root, hooks) = fixture("agents-md-write");
        healthy_shims(&hooks, "/bin/gh");

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Repair, true, false);
        let out = apply(&p);
        assert!(matches!(out, Outcome::Applied { .. }), "{out:?}");

        let agents_md = abs.join("AGENTS.md");
        assert_eq!(
            std::fs::read_to_string(&agents_md).unwrap(),
            amont_runtime::agents_md::generate_block()
        );
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(&agents_md).unwrap().permissions().mode();
            assert_eq!(mode & 0o111, 0, "AGENTS.md must not be made executable");
        }
        let _ = std::fs::remove_dir_all(&root);
    }

    /// The world can change between the scan a plan was built from and the
    /// moment it is applied — the same reason `is_tracked` re-checks. A repo
    /// that reached the generated block in that window must not be
    /// overwritten (which would be a no-op anyway, but re-checking is what
    /// makes that true rather than assumed).
    #[test]
    fn an_agents_md_that_became_current_between_scan_and_apply_is_left_alone() {
        let (root, hooks) = fixture("agents-md-race");
        healthy_shims(&hooks, "/bin/gh");

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Repair, true, false);
        assert!(p.write_agents_md.is_some(), "plan expected a write");

        // Something else won the race and wrote the current block first.
        std::fs::write(
            abs.join("AGENTS.md"),
            amont_runtime::agents_md::generate_block(),
        )
        .unwrap();

        let out = apply(&p);
        assert!(
            matches!(out, Outcome::Applied { written: 0, .. }),
            "must not count a write it skipped: {out:?}"
        );
        assert_eq!(
            std::fs::read_to_string(abs.join("AGENTS.md")).unwrap(),
            amont_runtime::agents_md::generate_block(),
            "must still be exactly the generated block, not doubled or corrupted"
        );
        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn a_refused_plan_touches_nothing() {
        let (root, hooks) = fixture("refused");
        // No shim dispatches to the binary, so the repo is unmanaged.
        std::fs::write(hooks.join("pre-commit"), "#!/bin/zsh\necho legacy\n").unwrap();
        let before = std::fs::read_to_string(hooks.join("pre-commit")).unwrap();

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false);
        assert_eq!(p.refuse, vec![Refusal::Unmanaged]);
        assert_eq!(apply(&p), Outcome::Refused);
        assert_eq!(
            std::fs::read_to_string(hooks.join("pre-commit")).unwrap(),
            before,
            "an unmanaged repo must be left exactly as found"
        );
        let _ = std::fs::remove_dir_all(&root);
    }

    /// Removal happens before writing. Reversing them would leave a retired
    /// file beside its replacement — the double-run state.
    #[test]
    fn removals_precede_writes() {
        let (root, hooks) = fixture("order");
        healthy_shims(&hooks, "/bin/gh");
        std::fs::write(hooks.join("pre-push-old"), STALE_OURS).unwrap();
        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false);
        let removals: Vec<_> = p.remove.iter().map(|r| r.path.clone()).collect();
        assert!(!removals.is_empty());
        apply(&p);
        for r in removals {
            assert!(!r.exists(), "{} should be gone", r.display());
        }
        let _ = std::fs::remove_dir_all(&root);
    }

    #[cfg(unix)]
    #[test]
    fn written_shims_are_executable() {
        use std::os::unix::fs::PermissionsExt;
        let (root, hooks) = fixture("mode");
        healthy_shims(&hooks, "/bin/gh");
        std::fs::remove_file(hooks.join("commit-msg")).unwrap();
        let (repo, abs) = scan_one(&root, "/bin/gh");
        apply(&plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false));
        let mode = std::fs::metadata(hooks.join("commit-msg"))
            .unwrap()
            .permissions()
            .mode();
        assert_eq!(
            mode & 0o111,
            0o111,
            "git will not run a non-executable hook"
        );
        let _ = std::fs::remove_dir_all(&root);
    }

    /// The write-side counterpart to the re-check `apply` already does for a
    /// `remove`. Built by hand rather than through `plan()` — `plan()`
    /// already refuses a write like this at PLAN time, so going through it
    /// would only prove the two layers agree, not that `apply` refuses on
    /// its own the way its own module doc says every action here must.
    #[test]
    fn a_write_that_became_tracked_is_refused_not_overwritten() {
        let dir = std::env::temp_dir().join(format!("apply-tracked-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        let git = |args: &[&str]| {
            std::process::Command::new("git")
                .args(args)
                .current_dir(&dir)
                .output()
                .expect("git");
        };
        git(&["init", "-q", "--template=", "."]);
        git(&["config", "user.email", "t@t"]);
        git(&["config", "user.name", "t"]);
        let target = dir.join("tracked-shim");
        std::fs::write(&target, "original\n").unwrap();
        git(&["add", "tracked-shim"]);
        git(&["commit", "-q", "--no-verify", "-m", "chore: seed"]);
        // The repository has to be one `apply` will still act on after its
        // re-verification pass — which is the point of that pass, and did not
        // exist when this test was written. One real shim makes it managed.
        let hooks = dir.join(".git/hooks");
        std::fs::create_dir_all(&hooks).unwrap();
        std::fs::write(hooks.join("pre-commit"), shim::render("/bin/gh")).unwrap();

        let p = FixPlan {
            repo: PathBuf::from("r"),
            repo_abs: dir.clone(),
            intent: Intent::Repair,
            hooks: crate::scan::HooksDir::In {
                path: hooks.clone(),
            },
            refuse: Vec::new(),
            warn: Vec::new(),
            remove: Vec::new(),
            write: vec![WriteShim {
                path: target.clone(),
                baked: "/bin/gh".to_string(),
                changes: true,
            }],
            write_agents_md: None,
        };

        assert_eq!(
            apply(&p),
            Outcome::Failed {
                error: "path is tracked by git".into(),
                at: target.display().to_string(),
            }
        );
        assert_eq!(
            std::fs::read_to_string(&target).unwrap(),
            "original\n",
            "a tracked file must never be overwritten by apply"
        );
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// `Refusal::Unmanaged` is decided from a scan, and a scan is a photograph.
    /// In the TUI it can predate several keystrokes; on the CLI it predates the
    /// preview the user read. A repository that stopped being ours in that
    /// window would have been repaired anyway, because `apply` took the plan's
    /// word for it — and "never adopt a repo" is the rule standing between this
    /// tool and an application's data repository.
    ///
    /// Lives here rather than in `tests/write_safety.rs` for one reason: the
    /// window between plan and apply cannot be opened from outside the process.
    /// `fix --apply` computes and applies in one breath, so a CLI-driven test
    /// could only ever prove that the two layers agree.
    #[test]
    fn a_repo_that_became_unmanaged_between_plan_and_apply_is_refused() {
        let (root, hooks) = fixture("unmanaged-race");
        healthy_shims(&hooks, "/bin/gh");
        std::fs::remove_file(hooks.join("pre-push")).unwrap();

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Repair, false, false);
        assert!(!p.is_noop() && !p.refused(), "fixture: {p:?}");

        // Somebody took our shims out — an `uninstall`, a `git clean`, a
        // colleague — after the plan was built.
        for n in shim::DISPATCHERS {
            let _ = std::fs::remove_file(hooks.join(n));
        }
        std::fs::write(hooks.join("pre-commit"), "#!/bin/sh\necho mine\n").unwrap();
        let before = std::fs::read_to_string(hooks.join("pre-commit")).unwrap();

        assert_eq!(apply(&p), Outcome::Refused);
        assert_eq!(
            std::fs::read_to_string(hooks.join("pre-commit")).unwrap(),
            before,
            "a repo that stopped being ours must be left exactly as found"
        );
        assert!(
            !hooks.join("pre-push").exists(),
            "and nothing may be written into it"
        );
        let _ = std::fs::remove_dir_all(&root);
    }

    /// The same window, on the activation side. `install` is the mode that
    /// writes four dispatchers into a repository nobody had installed into, and
    /// the check that somebody else's `pre-commit` is not already sitting at one
    /// of those paths ran once, at plan time.
    #[test]
    fn a_dispatcher_that_became_foreign_between_plan_and_apply_is_refused() {
        let (root, hooks) = fixture("foreign-race");

        let (repo, abs) = scan_one(&root, "/bin/gh");
        let p = plan(&repo, &abs, "/bin/gh", Intent::Activate, false, false);
        assert!(!p.is_noop() && !p.refused(), "fixture: {p:?}");

        // Somebody wrote their own hook in the meantime.
        let theirs = "#!/bin/sh\n# my own commit-msg, thanks\nexec my-linter \"$@\"\n";
        std::fs::write(hooks.join("commit-msg"), theirs).unwrap();

        assert_eq!(apply(&p), Outcome::Refused);
        assert_eq!(
            std::fs::read_to_string(hooks.join("commit-msg")).unwrap(),
            theirs,
            "activation must never overwrite a hook somebody wrote"
        );
        assert!(
            !hooks.join("pre-commit").exists(),
            "and one foreign dispatcher suppresses the WHOLE repo, not just itself"
        );
        let _ = std::fs::remove_dir_all(&root);
    }
}