monochromatic-git-policy-cli 0.5.30

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
//! Controls for the configuration snapshot:
//!  the listing parser, each conversion, each lookup, and generated cases checked against real Git as the oracle.

use super::*;
use crate::maintenance_decision::parse_config_records;
use crate::test_support::{fixture, git, git_context, isolated_overlay, remove, repository};
use std::ffi::OsString;
use std::path::PathBuf;

/// Build a listing in `git config --list -z --show-scope` form from `(scope, record)` pairs.
fn listing(records: &[(&str, &str)]) -> Vec<u8> {
    let mut bytes: Vec<u8> = Vec::new();
    for (scope, record) in records {
        bytes.extend_from_slice(scope.as_bytes());
        bytes.push(0);
        bytes.extend_from_slice(record.as_bytes());
        bytes.push(0);
    }
    return bytes;
}

/// Parse a listing that must be well formed.
fn snapshot_of(records: &[(&str, &str)]) -> ConfigSnapshot {
    return ConfigSnapshot::parse(listing(records).as_slice(), true).expect("a well-formed listing");
}

/// A key with a value, a valueless key and a value holding a newline keep their shape.
#[test]
fn parse_reads_values_valueless_keys_and_newlines_in_values() {
    let snapshot: ConfigSnapshot = snapshot_of(&[
        ("global", "a.b\nvalue"),
        ("local", "c.d"),
        ("local", "e.f\nline one\nline two"),
        ("command", "g.h\n"),
    ]);
    let entries: &[ConfigEntry] = snapshot.entries();
    assert_eq!(entries.len(), 4);
    assert_eq!(entries[0].scope, "global");
    assert_eq!(entries[0].value.as_deref(), Some(&b"value"[..]));
    assert_eq!(entries[1].value, None);
    assert_eq!(entries[2].value.as_deref(), Some(&b"line one\nline two"[..]));
    assert_eq!(entries[3].value.as_deref(), Some(&b""[..]));
}

/// An empty listing is an empty configuration, and any malformed listing is refused whole.
#[test]
fn parse_refuses_a_listing_that_is_not_scope_record_pairs() {
    assert_eq!(snapshot_of(&[]).entries().len(), 0);
    assert!(ConfigSnapshot::parse(b"local\0a.b\nc", true).is_none(), "no final NUL");
    assert!(ConfigSnapshot::parse(b"local\0a.b\nc\0local\0", true).is_none(), "a scope without its record");
    assert!(ConfigSnapshot::parse(b"local\0\nvalue\0", true).is_none(), "an empty key");
    assert!(ConfigSnapshot::parse(b"local\0\xff.b\nc\0", true).is_none(), "a key that is not UTF-8");
}

/// The last entry of a key wins, and a key nobody set is unset.
#[test]
fn the_last_entry_of_a_key_wins() {
    let snapshot: ConfigSnapshot = snapshot_of(&[
        ("global", "core.hookspath\n/first"),
        ("local", "core.hookspath\n/second"),
        ("command", "core.hookspath\n/third"),
    ]);
    assert_eq!(snapshot.path_value("core.hookspath"), Lookup::Found(String::from("/third")));
    assert_eq!(snapshot.path_value("core.other"), Lookup::Unset);
}

/// One entry the lookup cannot reproduce makes the lookup unhandled even when it is not the last.
#[test]
fn an_unhandled_earlier_entry_makes_the_whole_lookup_unhandled() {
    let snapshot: ConfigSnapshot = snapshot_of(&[
        ("global", "core.hookspath\n~/hooks"),
        ("local", "core.hookspath\n/plain"),
    ]);
    assert_eq!(snapshot.path_value("core.hookspath"), Lookup::Unhandled);
}

/// Every boolean spelling the lookup models converts as Git converts it.
#[test]
fn booleans_convert_as_git_converts_them() {
    for (value, expected) in [
        (Some(&b"true"[..]), Some(true)),
        (Some(&b"TRUE"[..]), Some(true)),
        (Some(&b"Yes"[..]), Some(true)),
        (Some(&b"on"[..]), Some(true)),
        (Some(&b"false"[..]), Some(false)),
        (Some(&b"No"[..]), Some(false)),
        (Some(&b"OFF"[..]), Some(false)),
        (Some(&b""[..]), Some(false)),
        (None, Some(true)),
        (Some(&b"0"[..]), Some(false)),
        (Some(&b"1"[..]), Some(true)),
        (Some(&b"42"[..]), Some(true)),
        (Some(&b"1k"[..]), None),
        (Some(&b"-1"[..]), None),
        (Some(&b"007"[..]), None),
        (Some(&b"0x10"[..]), None),
        (Some(&b" 1"[..]), None),
        (Some(&b"maybe"[..]), None),
        (Some(&b"\xff"[..]), None),
        (Some(&b"1234567890"[..]), None),
    ] {
        assert_eq!(canonical_bool(value), expected, "{value:?}");
    }
}

/// The bool-or-int text is a word, or the decimal number itself.
#[test]
fn bool_or_int_prints_words_and_plain_numbers() {
    for (value, expected) in [
        (Some(&b"yes"[..]), Some("true")),
        (Some(&b"Off"[..]), Some("false")),
        (Some(&b""[..]), Some("false")),
        (None, Some("true")),
        (Some(&b"0"[..]), Some("0")),
        (Some(&b"6700"[..]), Some("6700")),
        (Some(&b"1k"[..]), None),
        (Some(&b"007"[..]), None),
        (Some(&b"x"[..]), None),
    ] {
        assert_eq!(canonical_bool_or_int(value), expected.map(String::from), "{value:?}");
    }
}

/// Paths Git would expand, an empty path and a valueless path are left to Git.
#[test]
fn paths_that_git_expands_or_refuses_are_left_to_git() {
    for (value, expected) in [
        (Some(&b"relative/hooks"[..]), Some("relative/hooks")),
        (Some(&b"/abs/hooks"[..]), Some("/abs/hooks")),
        (Some(&b"a~b"[..]), Some("a~b")),
        (Some(&b"~/hooks"[..]), None),
        (Some(&b"~user/hooks"[..]), None),
        (Some(&b"%(prefix)/hooks"[..]), None),
        (Some(&b":(optional)hooks"[..]), None),
        (Some(&b""[..]), None),
        (None, None),
        (Some(&b"\xff"[..]), None),
    ] {
        assert_eq!(plain_path(value), expected.map(String::from), "{value:?}");
    }
}

/// The maintenance records hold the four keys' last values in Git's canonical text.
#[test]
fn maintenance_records_hold_the_last_value_of_each_key() {
    let snapshot: ConfigSnapshot = snapshot_of(&[
        ("global", "gc.auto\n6700"),
        ("local", "maintenance.auto\nno"),
        ("local", "maintenance.auto\nyes"),
        ("local", "gc.autodetach"),
        ("local", "other.key\nx"),
    ]);
    let records = snapshot.maintenance_records().expect("modelled values");
    assert_eq!(records.get("maintenance.auto").map(String::as_str), Some("true"));
    assert_eq!(records.get("gc.auto").map(String::as_str), Some("6700"));
    assert_eq!(records.get("gc.autodetach").map(String::as_str), Some("true"));
    assert_eq!(records.len(), 3);
    let unhandled: ConfigSnapshot = snapshot_of(&[("local", "gc.auto\n1k")]);
    assert_eq!(unhandled.maintenance_records(), None);
}

/// A listing read with global options never proves the ref storage, and one that names the key never does.
#[test]
fn ref_storage_is_proven_only_without_options_and_without_the_key() {
    assert!(snapshot_of(&[("local", "core.repositoryformatversion\n0")]).proves_files_ref_storage());
    assert!(!snapshot_of(&[("local", "extensions.refstorage\nfiles")]).proves_files_ref_storage());
    let with_options: ConfigSnapshot =
        ConfigSnapshot::parse(listing(&[("local", "core.bare\nfalse")]).as_slice(), false).expect("listing");
    assert!(!with_options.proves_files_ref_storage());
    assert!(!with_options.without_global_options());
}

/// Real Git's answer to `git config <type> --get <key>` in the same context.
fn oracle(context: &GitContext, repo: &Path, kind: &str, key: &str) -> (Option<i32>, String) {
    let arguments: [&str; 4] = ["config", kind, "--get", key];
    let output = run_git(context, &GitRequest::new(repo, &arguments)).expect("git config starts");
    let text: String = String::from_utf8(output.stdout).expect("UTF-8");
    return (output.code, String::from(text.strip_suffix('\n').unwrap_or(text.as_str())));
}

/// Require `lookup` to agree with Git's answer, or decline; return whether it answered.
fn agrees<Value: std::fmt::Debug + PartialEq>(
    lookup: &Lookup<Value>,
    (code, text): &(Option<i32>, String),
    render: impl Fn(&Value) -> String,
    label: &str,
) -> bool {
    match (code, lookup) {
        (Some(1), Lookup::Unset) => return true,
        (Some(0), Lookup::Found(value)) => {
            assert_eq!(&render(value), text, "{label}");
            return true;
        }
        (Some(0), Lookup::Unhandled) => return false,
        (Some(1), other) => panic!("{label}: Git says unset but the snapshot says {other:?}"),
        (Some(0), Lookup::Unset) => panic!("{label}: Git found {text:?} but the snapshot says unset"),
        (_, Lookup::Unhandled) => return false,
        (other_code, answer) => panic!("{label}: Git exited {other_code:?} ({text:?}) but the snapshot answered {answer:?}"),
    }
}

/// A small deterministic generator, so every run draws the same cases.
struct Draw(u64);

impl Draw {
    /// The next pseudo-random number below `bound`.
    fn below(&mut self, bound: usize) -> usize {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        return (self.0 % (bound as u64)) as usize;
    }
}

/// Values the generated cases draw from: boolean words, integers, bad booleans and paths Git expands.
const VALUES: [&str; 24] = [
    "", "true", "TRUE", "yes", "on", "off", "No", "false", "0", "1", "2", "007", "08", "-1", "1k", "0x10", "x",
    "~/h", "%(prefix)/p", ":(optional)p", "rel/p", "/abs/p", " 1", "a~b",
];

/// Keys the generated cases set, with the `--type` the consumer reads each with.
const KEYS: [(&str, &str); 4] = [
    ("core.hooksPath", "--type=path"),
    ("hook.pre-commit.enabled", "--type=bool"),
    ("gc.auto", "--type=bool-or-int"),
    ("maintenance.auto", "--type=bool-or-int"),
];

/// One generated config line for a random key and value, or a valueless key.
fn generated_line(draw: &mut Draw) -> String {
    let (key, _kind): (&str, &str) = KEYS[draw.below(KEYS.len())];
    let (section, variable): (&str, &str) = key.rsplit_once('.').expect("dotted key");
    let section_header: String = match section.split_once('.') {
        Some((name, subsection)) => format!("[{name} \"{subsection}\"]"),
        None => format!("[{section}]"),
    };
    let value: &str = VALUES[draw.below(VALUES.len())];
    if draw.below(8) == 0 {
        return format!("{section_header}\n\t{variable}\n");
    }
    return format!("{section_header}\n\t{variable} = \"{value}\"\n");
}

/// Several generated lines, 0 to 3 of them.
fn generated_block(draw: &mut Draw) -> String {
    let mut text: String = String::new();
    for _ in 0..draw.below(4) {
        text.push_str(generated_line(draw).as_str());
    }
    return text;
}

/// The counts a differential run reports, so a run that declines everything cannot pass.
#[derive(Default)]
struct Tally {
    /// Lookups that answered and matched Git.
    answered: usize,
    /// Lookups that declined.
    declined: usize,
}

/// Compare every consumer lookup of one configured repository with real Git.
fn compare_case(context: &GitContext, repo: &Path, label: &str, tally: &mut Tally) {
    let snapshot: Option<ConfigSnapshot> = read_config_snapshot(context, repo);
    let Some(listing_snapshot) = snapshot else {
        // A configuration Git itself refuses must make every oracle read fail too.
        let (code, _text) = oracle(context, repo, "--type=bool", "gc.auto");
        assert_ne!(code, Some(0), "{label}: the snapshot failed but Git read the configuration");
        tally.declined += 1;
        return;
    };
    let path_answer: bool = agrees(
        &listing_snapshot.path_value("core.hookspath"),
        &oracle(context, repo, "--type=path", "core.hooksPath"),
        |value| { return value.clone(); },
        label,
    );
    let bool_answer: bool = agrees(
        &listing_snapshot.bool_value("hook.pre-commit.enabled"),
        &oracle(context, repo, "--type=bool", "hook.pre-commit.enabled"),
        |value| { return String::from(if *value { "true" } else { "false" }); },
        label,
    );
    for answered in [path_answer, bool_answer] {
        if answered {
            tally.answered += 1;
        } else {
            tally.declined += 1;
        }
    }
    let arguments: [&str; 5] = [
        "config",
        "--type=bool-or-int",
        "--null",
        "--get-regexp",
        r"^(maintenance\.auto|maintenance\.autodetach|gc\.auto|gc\.autodetach)$",
    ];
    let listing_output = run_git(context, &GitRequest::new(repo, &arguments)).expect("git config starts");
    match listing_snapshot.maintenance_records() {
        None => tally.declined += 1,
        Some(records) => {
            assert!(
                matches!(listing_output.code, Some(0 | 1)),
                "{label}: the snapshot answered but Git exited {:?}",
                listing_output.code
            );
            let expected = parse_config_records(listing_output.stdout.as_slice()).expect("UTF-8 records");
            assert_eq!(records, expected, "{label}");
            tally.answered += 1;
        }
    }
}

/// Generated configurations, over files, includes, conditional includes, `-c` and `GIT_CONFIG_*`, agree with Git.
#[test]
fn generated_configurations_agree_with_git() {
    let root: PathBuf = fixture("config-snapshot-differential");
    let repo: PathBuf = repository(root.as_path(), "repo");
    let git_dir: PathBuf = repo.join(".git");
    let global: PathBuf = root.join("global-config");
    let mut draw: Draw = Draw(0x2545_f491_4f6c_dd1d);
    let mut tally: Tally = Tally::default();
    for case in 0..60 {
        let mut local: String = String::from("[core]\n\trepositoryformatversion = 0\n");
        local.push_str(generated_block(&mut draw).as_str());
        std::fs::write(git_dir.join("included"), generated_block(&mut draw)).expect("include file");
        std::fs::write(git_dir.join("conditional"), generated_block(&mut draw)).expect("conditional file");
        std::fs::write(global.as_path(), generated_block(&mut draw)).expect("global file");
        if draw.below(2) == 0 {
            local.push_str("[include]\n\tpath = included\n");
        }
        local.push_str(generated_block(&mut draw).as_str());
        if draw.below(2) == 0 {
            local.push_str(format!("[includeIf \"gitdir:{}\"]\n\tpath = conditional\n", git_dir.display()).as_str());
        }
        if draw.below(3) == 0 {
            local.push_str("[includeIf \"gitdir:/nowhere/\"]\n\tpath = conditional\n");
        }
        local.push_str(generated_block(&mut draw).as_str());
        std::fs::write(git_dir.join("config"), local).expect("local config");
        let mut overlay: Vec<(OsString, OsString)> = isolated_overlay();
        overlay.retain(|(name, _value)| { return name != "GIT_CONFIG_GLOBAL"; });
        overlay.push((OsString::from("GIT_CONFIG_GLOBAL"), global.clone().into_os_string()));
        if draw.below(3) == 0 {
            let (key, _kind) = KEYS[draw.below(KEYS.len())];
            overlay.push((OsString::from("GIT_CONFIG_COUNT"), OsString::from("1")));
            overlay.push((OsString::from("GIT_CONFIG_KEY_0"), OsString::from(key)));
            overlay.push((OsString::from("GIT_CONFIG_VALUE_0"), OsString::from(VALUES[draw.below(VALUES.len())])));
        }
        let mut prefix: Vec<OsString> = Vec::new();
        if draw.below(3) == 0 {
            let (key, _kind) = KEYS[draw.below(KEYS.len())];
            prefix.push(OsString::from("-c"));
            prefix.push(OsString::from(format!("{key}={}", VALUES[draw.below(VALUES.len())])));
        }
        let context: GitContext = GitContext {
            real_git: PathBuf::from(crate::test_support::REAL_GIT),
            overlay,
            global_prefix: prefix,
            common_dir: None,
        };
        compare_case(&context, repo.as_path(), format!("case {case}").as_str(), &mut tally);
    }
    assert!(tally.answered > 60, "the lookups answered only {} times; they decline too much", tally.answered);
    assert!(tally.declined > 5, "only {} lookups declined; the bad shapes were not drawn", tally.declined);
    remove(root.as_path());
}

/// A repeated key, a valueless key and a bad boolean each give the answer Git gives, or are declined.
#[test]
fn repeated_valueless_and_bad_values_agree_with_git() {
    let root: PathBuf = fixture("config-snapshot-shapes");
    let repo: PathBuf = repository(root.as_path(), "repo");
    let context: GitContext = git_context();
    let mut tally: Tally = Tally::default();
    for (label, text) in [
        ("repeated", "[hook \"pre-commit\"]\n\tenabled = false\n\tenabled = true\n"),
        ("valueless", "[hook \"pre-commit\"]\n\tenabled\n"),
        ("bad bool", "[hook \"pre-commit\"]\n\tenabled = maybe\n"),
        ("bad then good", "[hook \"pre-commit\"]\n\tenabled = maybe\n\tenabled = true\n"),
        ("empty", "[hook \"pre-commit\"]\n\tenabled =\n"),
        ("case", "[Hook \"pre-commit\"]\n\tEnabled = OFF\n"),
        ("old subsection form", "[hook.pre-commit]\n\tenabled = no\n"),
        ("other subsection case", "[hook \"Pre-Commit\"]\n\tenabled = false\n"),
        ("path valueless", "[core]\n\thooksPath\n"),
        ("path empty", "[core]\n\thooksPath =\n"),
        ("path tilde", "[core]\n\thooksPath = ~/hooks\n"),
        ("path repeated", "[core]\n\thooksPath = a\n\thooksPath = b\n"),
    ] {
        std::fs::write(repo.join(".git/config"), format!("[core]\n\trepositoryformatversion = 0\n{text}")).expect("config");
        compare_case(&context, repo.as_path(), label, &mut tally);
    }
    assert!(tally.answered > 0 && tally.declined > 0, "answered {} declined {}", tally.answered, tally.declined);
    remove(root.as_path());
}

/// The files backend is proven for an ordinary repository, and a reftable repository is not.
#[test]
fn ref_storage_proof_agrees_with_rev_parse() {
    let root: PathBuf = fixture("config-snapshot-ref-storage");
    let files: PathBuf = repository(root.as_path(), "files");
    let reftable: PathBuf = root.join("reftable");
    std::fs::create_dir(&reftable).expect("directory");
    git(reftable.as_path(), &["init", "--quiet", "--ref-format=reftable"]);
    let context: GitContext = git_context();
    for (name, repo, expected_format) in [("files", &files, "files"), ("reftable", &reftable, "reftable")] {
        let format = oracle(&context, repo.as_path(), "--type=bool", "core.bare");
        assert_eq!(format.0, Some(0), "{name}: the repository is readable");
        let reported = run_git(&context, &GitRequest::new(repo.as_path(), &["rev-parse", "--show-ref-format"]))
            .expect("rev-parse starts");
        assert_eq!(String::from_utf8_lossy(reported.stdout.as_slice()).trim(), expected_format);
        let snapshot: ConfigSnapshot = read_config_snapshot(&context, repo.as_path()).expect("snapshot");
        assert_eq!(snapshot.proves_files_ref_storage(), expected_format == "files", "{name}");
    }
    remove(root.as_path());
}

/// A listing that Git refuses (a syntax error in the configuration) gives no snapshot.
#[test]
fn a_configuration_git_refuses_gives_no_snapshot() {
    let root: PathBuf = fixture("config-snapshot-refused");
    let repo: PathBuf = repository(root.as_path(), "repo");
    std::fs::write(repo.join(".git/config"), "[core\n").expect("broken config");
    assert!(read_config_snapshot(&git_context(), repo.as_path()).is_none());
    remove(root.as_path());
}

/// `core.fsync` names the index only through `index`, `added` or `all`, read as Git 2.56.0 reads it: the last entry decides alone,
/// from the default (`committed,-loose-object`) which names no index; a word names every component it is a prefix of; the result
/// is the set without the removed words plus the added ones; `none` clears the set when it is the whole rest of the value.
#[test]
fn core_fsync_names_the_index_as_git_reads_it() {
    let names = |values: &[&str]| -> Option<bool> {
        let records: Vec<(&str, String)> = values.iter().map(|value| { return ("local", format!("core.fsync\n{value}")); }).collect();
        let borrowed: Vec<(&str, &str)> = records.iter().map(|(scope, record)| { return (*scope, record.as_str()); }).collect();
        return snapshot_of(borrowed.as_slice()).hardens_index();
    };
    assert_eq!(snapshot_of(&[]).hardens_index(), Some(false), "the default names no index");
    assert_eq!(names(&["committed"]), Some(false));
    assert_eq!(names(&["objects,reference"]), Some(false));
    assert_eq!(names(&["added"]), Some(true));
    assert_eq!(names(&["all"]), Some(true));
    assert_eq!(names(&["index"]), Some(true));
    assert_eq!(names(&["ind"]), Some(true), "a word names every component it is a prefix of");
    assert_eq!(names(&["all,-index"]), Some(true), "the added components win over the removed, as in Git's last line");
    assert_eq!(names(&["-index"]), Some(false));
    assert_eq!(names(&["pack,none"]), Some(false), "none clears the set when it ends the value");
    assert_eq!(names(&["added", "committed"]), Some(false), "the last entry decides alone");
    assert_eq!(names(&["added", ""]), Some(false), "an empty value is the default again");
    assert_eq!(names(&["bogus"]), Some(false), "a word that names no component is ignored, as Git ignores it");
}

/// The import step of a landing is given a `core.fsync` without the packs only where loose objects are not flushed: the default
/// and any set without `loose-object` get it, a set with `loose-object`, the deprecated switch for object files, or a value
/// that is not modelled leaves the import as the repository has it.
#[test]
fn the_import_gets_a_value_without_packs_only_where_loose_objects_are_not_flushed() {
    let value = |records: &[(&str, &str)]| -> Option<String> { return snapshot_of(records).import_fsync_value(); };
    assert_eq!(value(&[]), Some(String::from("commit-graph,none")), "the default keeps the commit graph and drops the packs");
    assert_eq!(value(&[("local", "core.fsync\nreference")]), Some(String::from("commit-graph,reference,none")), "the person's reference flush stays");
    assert_eq!(value(&[("local", "core.fsync\nnone")]), None, "a set with no pack has nothing to drop");
    assert_eq!(value(&[("local", "core.fsync\nloose-object")]), None, "loose objects are flushed there");
    assert_eq!(value(&[("local", "core.fsync\nadded")]), None);
    assert_eq!(value(&[("local", "core.fsync\nall")]), None);
    assert_eq!(value(&[("local", "core.fsync\nobjects")]), None, "objects includes loose objects");
    assert_eq!(value(&[("local", "core.fsyncobjectfiles\ntrue")]), None, "the deprecated switch flushes loose objects");
    assert_eq!(value(&[("local", "core.fsyncobjectfiles\nfalse")]), Some(String::from("commit-graph,none")));
    assert_eq!(value(&[("local", "core.fsyncobjectfiles\nsometimes")]), None, "a value that is not a boolean is not modelled");
    assert_eq!(value(&[("local", "core.fsync")]), None, "a key without a value is refused by Git, so it is left to Git");
    // Loose objects flushed without the packs: a pack the import keeps is flushed too, with its index.
    let with_packs = |entries: &[(&str, &str)]| -> Option<String> { return value(entries); };
    assert_eq!(with_packs(&[("local", "core.fsync\nloose-object,none")]), Some(String::from("loose-object,pack,pack-metadata,none")));
    assert_eq!(with_packs(&[("local", "core.fsync\nobjects,none")]), Some(String::from("loose-object,pack,pack-metadata,none")));
    assert_eq!(with_packs(&[("local", "core.fsync\nloose-object,pack,none")]), None, "the word pack names the pack metadata too, so both are flushed already");
    assert_eq!(with_packs(&[("local", "core.fsync\nloose-object,reference,none")]), Some(String::from("loose-object,pack,pack-metadata,reference,none")));
    assert_eq!(with_packs(&[("local", "core.fsync\nnone"), ("local", "core.fsyncobjectfiles\ntrue")]), Some(String::from("pack,pack-metadata,none")));
    assert_eq!(with_packs(&[("local", "core.fsyncobjectfiles\nyes")]), None, "the default already flushes the packs");
    assert_eq!(value(&[("global", "core.fsync\nadded"), ("local", "core.fsync\nreference")]), Some(String::from("commit-graph,reference,none")), "the last entry decides alone");
    assert_eq!(value(&[("local", "core.fsync\ncommitted,-loose-object")]), None, "an added component stays when it is also removed, so loose objects are flushed there");
}