monochromatic-git-policy-cli 0.5.31

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
Documentation
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
//! Controls for the landing's follow-up with real Git: the index entries of a commit Git built, whatever its names and
//!  however many, and the conclusion state removal.

use super::*;
use crate::recording_git::{recorded_starts, recording_context};
use crate::repo_read_hook::SWITCH_VARIABLE;
use crate::index_reset_file::support::git_with;
use crate::test_support::{fixture, git, git_context, git_output, git_text, remove, repository};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;

/// A repository whose index is at the first commit while the branch is at a second commit that adds the given files.
fn index_behind(name: &str, files: &[Vec<u8>]) -> (PathBuf, PathBuf, String, String) {
    let root: PathBuf = fixture(name);
    let repo: PathBuf = repository(root.as_path(), "repo");
    let first: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
    for file in files {
        let path: PathBuf = repo.join(std::ffi::OsStr::from_bytes(file.as_slice()));
        std::fs::create_dir_all(path.parent().expect("parent")).expect("directories");
        std::fs::write(path, b"x\n").expect("write");
    }
    git(repo.as_path(), &["add", "--all"]);
    git(repo.as_path(), &["commit", "--quiet", "--message=second"]);
    let second: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
    git(repo.as_path(), &["read-tree", first.as_str()]);
    return (root, repo, first, second);
}


/// What the index still differs from the commit by.
fn differs(repo: &Path, commit: &str) -> bool {
    return !git_output(repo, &["diff", "--cached", "--quiet", commit]).status.success();
}

/// The paths that differ between the old branch and the commit are updated, as bytes, whatever their number.
#[test]
fn derived_paths_cover_names_that_are_not_text_and_lists_past_the_request_limit() {
    let mut files: Vec<Vec<u8>> = vec![b"not-utf8-\xff\xfe.txt".to_vec(), b"back\\slash \"quoted\".txt".to_vec(), b"line\nbreak.txt".to_vec()];
    files.extend((0..crate::service_protocol::MAX_CHANGES + 50).map(|number| { return format!("many/file-{number}.txt").into_bytes(); }));
    let (root, repo, first, second) = index_behind("service-follow-derived", files.as_slice());
    assert!(differs(repo.as_path(), second.as_str()), "the index starts behind the commit");
    update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Derived { old: first.as_str() }).expect("updated");
    assert!(!differs(repo.as_path(), second.as_str()));
    remove(root.as_path());
}

/// A branch that did not exist has an all-zero old value, and the commit's whole tree is what the index takes.
#[test]
fn a_root_commit_is_updated_from_the_all_zero_old_value() {
    let (root, repo, first, _second) = index_behind("service-follow-root", &[b"new.txt".to_vec()]);
    git(repo.as_path(), &["read-tree", "--empty"]);
    let zero: String = "0".repeat(first.len());
    update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), first.as_str(), IndexPaths::Derived { old: zero.as_str() }).expect("updated");
    assert!(!differs(repo.as_path(), first.as_str()));
    remove(root.as_path());
}

/// An empty list changes nothing, because `reset` with no path would reset the whole index.
#[test]
fn an_empty_list_leaves_the_index_alone() {
    let (root, repo, first, second) = index_behind("service-follow-empty", &[b"new.txt".to_vec()]);
    update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Listed(&[])).expect("nothing to do");
    assert!(differs(repo.as_path(), second.as_str()));
    update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Derived { old: second.as_str() }).expect("no difference");
    assert!(differs(repo.as_path(), second.as_str()), "{first}");
    remove(root.as_path());
}

/// A holder proven alive that keeps the lock for longer than every try of the old five put together is waited for: the index is
///  updated once it lets go, and nothing failed.
#[test]
fn a_holder_proven_alive_is_waited_for_past_all_the_tries() {
    let (root, repo, _first, second) = index_behind("service-follow-proven-holder", &[b"new.txt".to_vec()]);
    let index: PathBuf = repo.join(".git/index");
    let lock: PathBuf = repo.join(".git/index.lock");
    std::fs::write(&lock, b"").expect("a lock");
    // The PID file names this process, which runs and started before the lock was made.
    std::fs::write(crate::recovery_files::lock_pid_path(index.as_path()), format!("pid {}\n", std::process::id())).expect("PID file");
    let holding: std::time::Duration = INDEX_PAUSE * u32::try_from(INDEX_ATTEMPTS * 3).expect("a small count");
    let holder = std::thread::spawn(move || {
        std::thread::sleep(holding);
        std::fs::remove_file(&lock).expect("the holder lets go");
    });
    let began: std::time::Instant = std::time::Instant::now();
    update_index(&git_context(), repo.as_path(), index.as_path(), second.as_str(), IndexPaths::Named(&[b"new.txt".to_vec()])).expect("updated once the holder let go");
    holder.join().expect("holder");
    assert!(began.elapsed() >= holding, "{:?}", began.elapsed());
    assert!(!differs(repo.as_path(), second.as_str()));
    remove(root.as_path());
}

/// A lock nothing proves anyone holds is waited for like one proven alive: the index is updated once the lock goes, however much
///  longer than the old backoff that is.
#[test]
fn an_owner_nothing_proves_alive_is_waited_for_like_a_proven_one() {
    let (root, repo, _first, second) = index_behind("service-follow-unproven-unbounded", &[b"new.txt".to_vec()]);
    let index: PathBuf = repo.join(".git/index");
    let lock: PathBuf = repo.join(".git/index.lock");
    std::fs::write(&lock, b"").expect("a lock nobody holds");
    let holding: std::time::Duration = std::time::Duration::from_millis(2500);
    let holder = std::thread::spawn(move || {
        std::thread::sleep(holding);
        std::fs::remove_file(&lock).expect("the lock goes");
    });
    let began: std::time::Instant = std::time::Instant::now();
    update_index(&git_context(), repo.as_path(), index.as_path(), second.as_str(), IndexPaths::Named(&[b"new.txt".to_vec()])).expect("updated once the lock went");
    holder.join().expect("holder");
    assert!(began.elapsed() >= holding, "{:?}", began.elapsed());
    assert!(!differs(repo.as_path(), second.as_str()));
    remove(root.as_path());
}

/// A removal request whose scratch directory is gone has nothing left to compare and is done.
#[test]
fn a_cleanup_without_its_scratch_directory_is_done() {
    let root: PathBuf = fixture("service-follow-gone");
    let repo: PathBuf = repository(root.as_path(), "repo");
    let cleanup: Cleanup = Cleanup { git_dir: repo.join(".git"), scratch: repo.join(".git").join("gone"), reftable: false };
    assert!(remove_conclusion_state(&git_context(), repo.as_path(), &cleanup));
    remove(root.as_path());
}

/// What:
///  Every start of the index update, in each form it takes, as the service makes it.
/// Why:
///   The owner's rule is that no Git start of the service gets `--quiet` or `-q`: the three forms of the update (a listed
///  path, a named path as bytes, a path derived from the old branch) each start `reset`, and each is looked at.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// for (const paths of [listed, named, derived]) await updateIndex(recordingGit, repo, second, paths); expect(quietStarts()).toEqual([]);
/// ```
#[test]
fn the_index_update_starts_reset_without_quiet_in_every_form() {
    let (root, repo, first, second) = index_behind("service-follow-no-quiet", &[b"new.txt".to_vec()]);
    let (mut context, log) = crate::recording_git::recording_context(root.as_path());
    // The named form hands its paths on standard input, which a request makes in the common directory.
    context.common_dir = Some(repo.join(".git"));
    let listed: Vec<String> = vec![String::from("new.txt")];
    let named: Vec<Vec<u8>> = vec![b"new.txt".to_vec()];
    for paths in [IndexPaths::Listed(listed.as_slice()), IndexPaths::Named(named.as_slice()), IndexPaths::Derived { old: first.as_str() }] {
        git(repo.as_path(), &["read-tree", first.as_str()]);
        update_index(&context, repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), paths).expect("updated");
        assert!(!differs(repo.as_path(), second.as_str()), "{paths:?} brings the index to the commit");
    }
    let resets: Vec<String> = crate::recording_git::recorded_starts(log.as_path()).into_iter().filter(|start| { return start.starts_with("reset "); }).collect();
    assert_eq!(resets.len(), 3, "one reset for each of the three forms: {resets:?}");
    assert_eq!(crate::recording_git::quiet_starts(log.as_path()), Vec::<String>::new(), "no start carries --quiet or -q");
    remove(root.as_path());
}

/// What `reset` says while the index is updated (the list of files still changed in the worktree) is kept in the service's log.
#[test]
fn what_reset_says_is_kept_in_the_journal() {
    let (root, repo, _first, second) = index_behind("service-follow-words", &[b"new.txt".to_vec()]);
    let shim: PathBuf = root.join("saying-git");
    let words: &str = "Unstaged changes after reset:";
    let script: String = format!("#!/bin/sh\ncase \" $* \" in *\" reset \"*) printf '%s\\n' '{words}';; esac\nexec {} \"$@\"\n", crate::test_support::REAL_GIT);
    crate::test_support::executable(shim.as_path(), script.as_bytes());
    let mut context: GitContext = git_context();
    context.real_git = shim;
    let listed: Vec<String> = vec![String::from("new.txt")];
    let entries = crate::service_timing::capture::entries_of(|| {
        update_index(&context, repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Listed(listed.as_slice())).expect("updated");
    });
    assert_eq!(entries.len(), 1, "{entries:?}");
    assert!(entries[0].message.contains(words), "{:?}", entries[0]);
    remove(root.as_path());
}

/// The landing's index update starts no Git `reset` and no Git `diff-tree`, whichever way the paths are given, and the index it
/// builds equals the commit for those paths.
#[test]
fn the_follow_up_starts_no_git_reset_and_no_git_diff_tree() {
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec(), b"dir/inner.txt".to_vec()];
    let listed: Vec<String> = vec![String::from("top.txt"), String::from("dir/inner.txt")];
    for form in ["listed", "named", "derived"] {
        let (root, repo, first, second) = index_behind(format!("service-follow-starts-{form}").as_str(), files.as_slice());
        let (mut context, log) = recording_context(root.as_path());
        // The recorder switches the in-process readers off; the update under test must run with them on.
        context.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
        let paths: IndexPaths<'_> = match form {
            "listed" => IndexPaths::Listed(listed.as_slice()),
            "named" => IndexPaths::Named(files.as_slice()),
            _ => IndexPaths::Derived { old: first.as_str() },
        };
        let index: PathBuf = repo.join(".git").join("index");
        // The service builds the index while it holds the live index lock.
        let held = crate::service_index_lock::try_acquire(index.as_path(), 1).expect("the lock is free");
        let built = build_index(&context, repo.as_path(), second.as_str(), paths, &held, 1).expect("built");
        let words: Vec<String> = recorded_starts(log.as_path()).iter().map(|start| { return start.split(' ').next().unwrap_or_default().to_string(); }).collect();
        assert!(!words.iter().any(|word| { return word == "reset" || word == "diff-tree"; }), "{form} started {words:?}");
        held.release_with(built.path()).expect("installed");
        assert!(!differs(repo.as_path(), second.as_str()), "{form} left the index behind the commit");
        remove(root.as_path());
    }
}


/// The names Git lists as staged against `commit`.
fn staged_against(repo: &Path, commit: &str) -> Vec<String> {
    return git_text(repo, &["diff", "--cached", "--name-only", commit]).lines().map(str::to_string).collect();
}

/// A change another session staged before the lock was taken, to a path the commit does not name, survives the in-process
/// build and its installation; a stage attempted after the lock was taken is refused until the installation; and no Git starts.
#[test]
fn what_was_staged_before_the_lock_survives_and_a_stage_after_it_is_refused() {
    use crate::service_index_lock::{lock_path_of, try_acquire};
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec(), b"dir/inner.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-staged", files.as_slice());
    std::fs::write(repo.join("unselected.txt"), b"staged by another session\n").expect("write");
    // An entry recorded in the second of the lock's copy is racily clean and sends the update to Git; this one is older.
    crate::test_support::age(repo.join("unselected.txt").as_path());
    git(repo.as_path(), &["add", "unselected.txt"]);
    let (mut context, log) = recording_context(root.as_path());
    context.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
    context.common_dir = Some(repo.join(".git"));
    let index: PathBuf = repo.join(".git").join("index");
    let held = try_acquire(index.as_path(), 3).expect("the lock is free");
    std::fs::write(repo.join("late.txt"), b"staged too late\n").expect("write");
    assert!(!git_output(repo.as_path(), &["add", "late.txt"]).status.success(), "a stage after the lock is taken is refused");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 3).expect("built");
    assert!(lock_path_of(index.as_path()).exists(), "the lock is held until the installation");
    held.release_with(built.path()).expect("installed");
    let words: Vec<String> = recorded_starts(log.as_path()).iter().map(|start| { return start.split(' ').next().unwrap_or_default().to_string(); }).collect();
    assert!(!words.iter().any(|word| { return word == "reset"; }), "no Git reset started: {words:?}");
    assert_eq!(staged_against(repo.as_path(), second.as_str()), vec![String::from("unselected.txt")], "only the earlier stage is left, and it survived");
    assert!(git_output(repo.as_path(), &["add", "late.txt"]).status.success(), "the lock is gone, so the stage works now");
    remove(root.as_path());
}

/// An installation that cannot be made leaves the old index and removes the lock; a lock another program replaced keeps the
/// index and stays with that program.
#[test]
fn a_failed_installation_keeps_the_index_and_a_replaced_lock_is_left_alone() {
    use crate::service_index_lock::{lock_path_of, try_acquire};
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-install", files.as_slice());
    let mut context = git_context();
    context.common_dir = Some(repo.join(".git"));
    let index: PathBuf = repo.join(".git").join("index");
    let before: Vec<u8> = std::fs::read(&index).expect("index");
    // A lock replaced by another program's: the check sees it, the index is not replaced, the other program's lock stays.
    let held = try_acquire(index.as_path(), 4).expect("the lock is free");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 4).expect("built");
    std::fs::remove_file(lock_path_of(index.as_path())).expect("removed");
    std::fs::write(lock_path_of(index.as_path()), b"another program's lock").expect("foreign lock");
    assert!(held.release_with(built.path()).is_err());
    assert_eq!(std::fs::read(&index).expect("index"), before, "the index was not replaced");
    assert_eq!(std::fs::read(lock_path_of(index.as_path())).expect("lock"), b"another program's lock".to_vec());
    std::fs::remove_file(lock_path_of(index.as_path())).expect("removed");
    std::fs::remove_file(&built).expect("built removed");
    // A rename that fails: the live index path holds a directory, so the copy cannot take its place; the lock goes with the error.
    let second_hold = try_acquire(index.as_path(), 5).expect("the lock is free");
    let second_built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &second_hold, 5).expect("built");
    std::fs::remove_file(&index).expect("index removed");
    std::fs::create_dir_all(index.join("occupied")).expect("a directory where the index goes");
    assert!(second_hold.release_with(second_built.path()).is_err());
    assert!(!lock_path_of(index.as_path()).exists(), "the lock is removed with the failure");
    assert!(index.join("occupied").is_dir());
    remove(root.as_path());
}

/// An entry that is racily clean for the lock's copy (its modification time is not before the copy's) sends the follow-up to
/// Git's own `reset --no-refresh` on the private copy, which gives the same result: the committed paths at the commit and the
/// racily clean path still staged.
#[test]
fn a_racily_clean_entry_sends_the_follow_up_to_git_with_the_same_result() {
    use crate::service_index_lock::try_acquire;
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-racy", files.as_slice());
    let racy: PathBuf = repo.join("racy.txt");
    std::fs::write(&racy, b"staged an instant ago\n").expect("write");
    // A time an hour ahead is never before the copy's, however slow the run is.
    let ahead: std::time::SystemTime = std::time::SystemTime::now() + std::time::Duration::from_secs(3600);
    std::fs::OpenOptions::new().write(true).open(&racy).expect("open").set_modified(ahead).expect("set the time");
    git(repo.as_path(), &["add", "racy.txt"]);
    let (mut context, log) = recording_context(root.as_path());
    context.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
    context.common_dir = Some(repo.join(".git"));
    let index: PathBuf = repo.join(".git").join("index");
    let held = try_acquire(index.as_path(), 6).expect("the lock is free");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 6).expect("built");
    let starts: Vec<String> = recorded_starts(log.as_path());
    assert!(starts.iter().any(|start| { return start.starts_with("reset --no-refresh "); }), "Git was not asked: {starts:?}");
    held.release_with(built.path()).expect("installed");
    assert_eq!(staged_against(repo.as_path(), second.as_str()), vec![String::from("racy.txt")]);
    remove(root.as_path());
}

/// The record of the installed index is made after the index is installed and while the lock is still held; a record that cannot
/// be made keeps the lock in place (the restart reclaims it) and the installed index, and the error is returned.
#[test]
fn the_record_is_made_between_the_installation_and_the_unlock_and_a_failed_record_keeps_the_lock() {
    use crate::service_index_lock::{lock_path_of, try_acquire};
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-record", files.as_slice());
    let mut context = git_context();
    context.common_dir = Some(repo.join(".git"));
    let index: PathBuf = repo.join(".git").join("index");
    let lock: PathBuf = lock_path_of(index.as_path());
    let held = try_acquire(index.as_path(), 8).expect("the lock is free");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 8).expect("built");
    let built_bytes: Vec<u8> = std::fs::read(&built).expect("built bytes");
    let seen: std::cell::RefCell<Vec<(bool, bool)>> = std::cell::RefCell::new(Vec::new());
    let record = || -> std::io::Result<()> {
        seen.borrow_mut().push((lock.exists(), std::fs::read(&index)? == built_bytes));
        return Ok(());
    };
    held.release_with_record(built.path(), &record).expect("installed and recorded");
    assert_eq!(seen.into_inner(), vec![(true, true)], "the lock was held and the index installed when the record was made");
    assert!(!lock.exists());
    // A record that cannot be made.
    let second_hold = try_acquire(index.as_path(), 9).expect("the lock is free");
    let second_built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &second_hold, 9).expect("built");
    let broken = || -> std::io::Result<()> { return Err(std::io::Error::other("no space left")); };
    assert!(second_hold.release_with_record(second_built.path(), &broken).is_err());
    assert!(lock.exists(), "the lock stays for the restart to reclaim");
    assert!(!differs(repo.as_path(), second.as_str()), "the installed index stays");
    std::fs::remove_file(&lock).expect("the restart reclaims the lock");
    remove(root.as_path());
}

/// The landing's private copy is a file of its own: when the live index is itself named like a private copy (a client's
/// `GIT_INDEX_FILE`), the live index is neither removed nor taken for the copy, and nothing staged in it is lost.
#[test]
fn a_live_index_named_like_a_private_copy_is_not_taken_for_one() {
    use crate::service_index_lock::try_acquire;
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-alias-live", files.as_slice());
    std::fs::write(repo.join("unselected.txt"), b"staged\n").expect("write");
    crate::test_support::age(repo.join("unselected.txt").as_path());
    git(repo.as_path(), &["add", "unselected.txt"]);
    let live: PathBuf = repo.join(".git").join("index.cli-git-7");
    std::fs::copy(repo.join(".git").join("index"), &live).expect("a client's index named like a private copy");
    let before: Vec<u8> = std::fs::read(&live).expect("live");
    let mut context = git_context();
    context.common_dir = Some(repo.join(".git"));
    let held = try_acquire(live.as_path(), 7).expect("the lock is free");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 7).expect("built");
    assert_ne!(built.path(), live, "the private copy is not the live index");
    assert_eq!(std::fs::read(&live).expect("the live index stays until the installation"), before);
    held.release_with(built.path()).expect("installed");
    let output = git_with(repo.as_path(), &[("GIT_INDEX_FILE", live.as_os_str().to_os_string())], &["diff", "--cached", "--name-only", second.as_str()], None);
    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unselected.txt", "the unselected stage is still in the client's index");
    remove(root.as_path());
}

/// Another client's index that happens to have the name of the private copy is not overwritten or removed.
#[test]
fn another_clients_file_with_the_private_name_is_left_alone() {
    use crate::service_index_lock::try_acquire;
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-alias-other", files.as_slice());
    let index: PathBuf = repo.join(".git").join("index");
    let other: PathBuf = repo.join(".git").join("index.cli-git-7");
    std::fs::write(&other, b"another client's index").expect("other");
    let mut context = git_context();
    context.common_dir = Some(repo.join(".git"));
    let held = try_acquire(index.as_path(), 7).expect("the lock is free");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 7).expect("built");
    assert_ne!(built.path(), other);
    held.release_with(built.path()).expect("installed");
    assert_eq!(std::fs::read(&other).expect("other"), b"another client's index".to_vec());
    assert!(!differs(repo.as_path(), second.as_str()));
    remove(root.as_path());
}

/// The live index of a repository whose index is behind, and a lock on it.
fn locked_index(name: &str) -> (PathBuf, PathBuf, PathBuf, crate::service_index_lock::IndexLock) {
    let (root, repo, _first, _second) = index_behind(name, &[b"top.txt".to_vec()]);
    let index: PathBuf = repo.join(".git").join("index");
    let held = crate::service_index_lock::try_acquire(index.as_path(), 1).expect("the lock is free");
    return (root, repo, index, held);
}

/// The private copy's name does not grow with the live index's: the longest index name the lock accepts (the lock's own staging
/// name overflows a file name first, at `service_index_staging.rs`, long before the 246 bytes the review named) still gets a copy.
#[test]
fn a_long_index_name_still_gets_a_private_copy() {
    let (root, repo, _first, _second) = index_behind("service-follow-long-name", &[b"top.txt".to_vec()]);
    let dot_git: PathBuf = repo.join(".git");
    let (_long, held) = (1..=246_usize)
        .rev()
        .find_map(|length| {
            let long: PathBuf = dot_git.join("i".repeat(length));
            std::fs::copy(dot_git.join("index"), &long).ok()?;
            let held = crate::service_index_lock::try_acquire(long.as_path(), 1)?;
            return Some((long, held));
        })
        .expect("some index name takes the lock");
    let copy = PrivateCopy::make(&held, 1).expect("a private copy of the longest index name that takes the lock");
    assert!(copy.path().exists());
    drop(copy);
    drop(held);
    remove(root.as_path());
}

/// Copies other programs left under any name the landing once used never use up its names, and none of them is touched.
#[test]
fn leftovers_under_the_old_names_do_not_exhaust_the_private_copies() {
    let (root, repo, _index, held) = locked_index("service-follow-exhausted");
    let dot_git: PathBuf = repo.join(".git");
    let mut foreign: Vec<PathBuf> = vec![dot_git.join("index.cli-git-1")];
    foreign.extend((1..32).map(|attempt| { return dot_git.join(format!("index.cli-git-1-{attempt}")); }));
    for path in &foreign {
        std::fs::write(path, b"another program's file").expect("foreign file");
    }
    let copy = PrivateCopy::make(&held, 1).expect("a private copy although every old name is taken");
    assert!(copy.path().exists());
    for path in &foreign {
        assert_eq!(std::fs::read(path).expect("the foreign file stays"), b"another program's file".to_vec());
    }
    drop(copy);
    drop(held);
    remove(root.as_path());
}

/// A private copy left by a service that died is removed by the next landing, identified by the directory name, its owner and
/// the death of whoever held it: a directory with the name pattern that is not ours, one a live owner holds, and a plain file with
/// the name are not touched. A marker names a process number, which a living process may have taken over since, and a service
/// that died before it wrote a marker left none; neither decides anything.
#[test]
fn only_a_dead_services_leftover_is_removed() {
    use std::os::unix::fs::PermissionsExt as _;
    let (root, repo, _index, held) = locked_index("service-follow-sweep");
    let dot_git: PathBuf = repo.join(".git");
    let named = |tail: &str| -> PathBuf { return dot_git.join(format!("cli-git-private-{tail}")); };
    let dead: PathBuf = named("00000000-0000-4000-8000-000000000001");
    let unmarked: PathBuf = named("00000000-0000-4000-8000-000000000002");
    let reused: PathBuf = named("00000000-0000-4000-8000-000000000003");
    let plain: PathBuf = named("00000000-0000-4000-8000-000000000004");
    let not_ours: PathBuf = named("00000000-0000-4000-8000-000000000005");
    for directory in [&dead, &unmarked, &reused, &not_ours] {
        std::fs::create_dir(directory).expect("leftover");
        std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700)).expect("owner only");
        std::fs::write(directory.join("index"), b"copy").expect("copy");
    }
    // Opened to others: not a directory this service makes.
    std::fs::set_permissions(&not_ours, std::fs::Permissions::from_mode(0o755)).expect("open to others");
    // A marker of a process that is gone, and one that names a process that lives now: the number was taken over.
    std::fs::write(dead.join("owner"), format!("cli-git-private-copy v1\npid={}\n", i32::MAX)).expect("marker");
    std::fs::write(reused.join("owner"), format!("cli-git-private-copy v1\npid={}\n", std::process::id())).expect("marker");
    std::fs::write(&plain, b"not a directory").expect("plain");
    // A copy a live owner holds, whatever its marker says.
    std::fs::copy(dot_git.join("index"), dot_git.join("other-index")).expect("another index");
    let owner = crate::service_index_lock::try_acquire(dot_git.join("other-index").as_path(), 2).expect("a lock on another index in the same directory");
    let alive = PrivateCopy::make(&owner, 2).expect("a live owner's copy");
    std::fs::write(alive.directory().join("owner"), "cli-git-private-copy v1\npid=1\n").ok();
    let copy = PrivateCopy::make(&held, 1).expect("a private copy");
    assert!(!dead.exists(), "the dead service's copy is removed");
    assert!(!unmarked.exists(), "the copy of a service that died before it wrote a marker is removed");
    assert!(!reused.exists(), "a marker whose process number a living process took over decides nothing");
    assert!(not_ours.join("index").exists() && plain.exists(), "nothing else is touched");
    assert!(alive.path().exists(), "a copy a live owner holds stays, which only the owner's hold can tell");
    drop(copy);
    drop(alive);
    drop(owner);
    drop(held);
    remove(root.as_path());
}

/// Another client's lock at the name a private copy used to have does not stop a landing whose live index is absent: the copy
/// is in a directory of its own, so no name beside the index is shared.
#[test]
fn a_foreign_lock_at_the_old_copy_name_does_not_stop_a_landing_without_an_index() {
    use crate::service_index_lock::try_acquire;
    let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
    let (root, repo, _first, second) = index_behind("service-follow-foreign-lock", files.as_slice());
    let index: PathBuf = repo.join(".git").join("index");
    std::fs::remove_file(&index).expect("no live index");
    let foreign: PathBuf = repo.join(".git").join("index.cli-git-7.lock");
    std::fs::write(&foreign, b"another client holds this").expect("foreign lock");
    let mut context = git_context();
    context.common_dir = Some(repo.join(".git"));
    let held = try_acquire(index.as_path(), 7).expect("the lock is free");
    let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 7);
    assert!(built.is_ok(), "the landing builds its index: {:?}", built.as_ref().err());
    assert_eq!(std::fs::read(&foreign).expect("the foreign lock stays"), b"another client holds this".to_vec());
    drop(built);
    drop(held);
    remove(root.as_path());
}

/// The private copy is made where only its owner can open it, and the index it installs when nothing changed has the mode the
/// live index has, whatever the process mask is.
#[test]
fn the_private_copy_is_owner_only_and_a_no_change_keeps_the_live_mode() {
    if crate::umask_child_test_support::ran_in_child(module_path!(), "the_private_copy_is_owner_only_and_a_no_change_keeps_the_live_mode", 0o000) {
        return;
    }
    use std::os::unix::fs::PermissionsExt as _;
    let (root, repo, index, held) = locked_index("service-follow-private-mode");
    std::fs::set_permissions(&index, std::fs::Permissions::from_mode(0o640)).expect("a live mode");
    let mut context = git_context();
    context.common_dir = Some(repo.join(".git"));
    let none: Vec<Vec<u8>> = Vec::new();
    let built = build_index(&context, repo.as_path(), "0".repeat(40).as_str(), IndexPaths::Named(&none), &held, 1).expect("built");
    let path: PathBuf = AsRef::<Path>::as_ref(&built).to_path_buf();
    let directory: &Path = path.parent().expect("a directory");
    assert_ne!(directory, repo.join(".git").as_path(), "the copy is not loose in the live index's directory");
    assert_eq!(std::fs::metadata(directory).expect("directory").permissions().mode() & 0o777, 0o700, "only the owner opens the directory");
    assert_eq!(std::fs::metadata(&path).expect("copy").permissions().mode() & 0o777, 0o640, "the live index's mode");
    drop(built);
    drop(held);
    remove(root.as_path());
}