monochromatic-git-policy-cli 0.5.14

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
//! What:
//!  The typed settings a validated `cli-git.config.jsonc` produces,
//!  with defaults.
//! Why:
//!  The policy engine and transactions read plain typed values,
//!  never JSON.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // type CliGitConfig = { policies: PolicyConfig; concurrency: ConcurrencyConfig };
//! ```

/// What:
///  Import the compiled-in registry and its identity and severity types.
/// Why:
///   Defaults are derived from the registry so the two can never disagree.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { POLICY_REGISTRY, type PolicyId, type Severity } from './policy-registry.ts';
/// ```
use super::policy_registry::{
    POLICY_REGISTRY, PolicyDescriptor, PolicyId, Severity, policy_descriptor,
};

/// What:
///  `u64` constant,
///  an unsigned 64-bit integer (siblings `u32`,
///  `usize`,
///  `i64`).
/// Why:
///   Milliseconds up to 2^53 - 1 are accepted,
///  which `u32` cannot hold.
///       This was the incumbent's default patience for an unproven `index.lock` owner; nothing reads it now.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS = 1_000;
/// ```
pub const DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS: u64 = 1_000;

/// Incumbent default:
///  reserve the next landing slot after one lost landing race; nothing reads it now.
pub const DEFAULT_RESERVE_AFTER_LOST_RACES: u64 = 1;

/// What:
///  The effective severity of one shipped policy.
///       `#[derive(...)]` generates copy,
///  debug printing and `==` for the struct.
/// Why:
///   `explicit` separates "the repository chose this" from "the default applied";
///       only an explicit unsafe `warn` earns a configuration warning.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PolicySetting = { id: PolicyId; severity: Severity; explicit: boolean };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PolicySetting {
    /// Which shipped policy this row configures.
    pub id: PolicyId,
    /// Severity in effect for this invocation.
    pub severity: Severity,
    /// Whether the repository configuration named this policy.
    pub explicit: bool,
}

/// What:
///  The closed set of Markdown rules the commit-time policy may run.
/// Why:
///   The shipped policy runs the owned linter with a fixed rule list;
///  an unknown
///       rule name is a configuration error,
///  never a pass-through argument.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type MarkdownRule = 'lfs-image-url';
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MarkdownRule {
    /// `lfs-image-url`:
    ///  rewrite image links to LFS-tracked files to immutable object URLs.
    LfsImageUrl,
}

/// What:
///  Validated options of `markdown/autofix`.
///       `Vec<T>` is an owned growable list (siblings:
///  `&[T]` borrowed,
///  `[T; N]` fixed).
///       `String` is owned text (sibling:
///  `&str` borrowed).
/// Why:
///   The settings outlive the parsed document,
///  so they own their lists and text.
///       There is deliberately no command or executable field.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type MarkdownAutofixOptions = { rules: MarkdownRule[]; exclude: string[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MarkdownAutofixOptions {
    /// Rules to run,
    ///  without repeats;
    ///  defaults to the LFS image rewrite alone.
    pub rules: Vec<MarkdownRule>,
    /// gitignore-syntax patterns,
    ///  relative to the repository root,
    ///  left untouched.
    pub exclude: Vec<String>,
}

/// What:
///  One rules file named by the `rulesFile` option:
///  its path inside the repository and whether it may be absent.
/// Why:
///   The configuration names several files as an array,
///  and a gitignored local file must be allowed to be missing
///       on a fresh clone while a committed shared file must not.
///       The path is the configuration's own text, with no comma or `?` syntax,
///  because JSON can already say which file is meant.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type RulesFileEntry = { path: string; optional: boolean };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RulesFileEntry {
    /// The file,
    ///  relative to the repository's top level and already checked to stay inside it.
    pub path: String,
    /// Whether the file may be absent;
    ///  an absent optional file is skipped and an absent required file stops the policy.
    pub optional: bool,
}

/// What:
///  Validated options of `security/forbidden-strings`.
///  `Option<Vec<RulesFileEntry>>` is "a
///       configured list of files or nothing".
/// Why:
///   The scanner is linked into cli-git,
///  so the remaining choices are whether its
///       embedded baseline rules run and which repository files hold the private rules.
///       There is deliberately no executable path.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ForbiddenStringsOptions = { builtinRules: boolean; rulesFile?: string | (string | { path: string; optional?: boolean })[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ForbiddenStringsOptions {
    /// Whether the scanner's embedded baseline rules are loaded;
    ///  default `true`.
    pub builtin_rules: bool,
    /// The private rules files in the order they are read,
    ///  each relative to the repository's top level and already checked to
    /// stay inside it;
    ///  never an empty list;
    ///  when absent,
    ///  `FORBIDDEN_STRINGS_RULES` and then the default file apply.
    pub rules_files: Option<Vec<RulesFileEntry>>,
}

/// What:
///  Every policy's severity plus the option records of option-bearing policies.
/// Why:
///   One value answers "does this policy run,
///  how strictly,
///  with what options".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PolicyConfig = { settings: PolicySetting[];
///   forbiddenStrings: ForbiddenStringsOptions; markdownAutofix: MarkdownAutofixOptions };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyConfig {
    /// One row per registry policy,
    ///  in registry order.
    pub settings: Vec<PolicySetting>,
    /// Options of `security/forbidden-strings`,
    ///  defaults applied.
    pub forbidden_strings: ForbiddenStringsOptions,
    /// Options of `markdown/autofix`,
    ///  defaults applied.
    pub markdown_autofix: MarkdownAutofixOptions,
}

/// What:
///  The retired `hooks` key.
/// Why:
///   `hooks.concurrentCommits` tuned the hook lock, and the wrapper runs no external hook, so there is no lock to
///  tune.
///  Older files that set it still load: the value is checked and ignored, and `git cli-git doctor` says so.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type HooksConfig = { concurrentCommits: boolean; present: boolean };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HooksConfig {
    /// The value the file gave; never used.
    pub concurrent_commits: bool,
    /// Whether the file states `hooks.concurrentCommits`.
    pub present: bool,
}

/// What:
///  The retired `indexLock` key.
/// Why:
///   `indexLock.unprovenOwnerTimeoutMs` was how long the wrapper backed off for an `index.lock` whose owner is dead or
///  unproven. The commit service waits for a foreign `index.lock` as long as `CLI_GIT_LOCK_WAIT_SECONDS` allows and reads no
///  such value, so the key does nothing.
///  Older files that set it still load: the value is checked and ignored, and `git cli-git doctor` says so.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type IndexLockConfig = { unprovenOwnerTimeoutMs: number; present: boolean };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IndexLockConfig {
    /// The value the file gave, a whole number of at least 0; never used.
    pub unproven_owner_timeout_ms: u64,
    /// Whether the file states `indexLock.unprovenOwnerTimeoutMs`.
    pub present: bool,
}

/// What:
///  The retired `landing` key.
/// Why:
///   `landing.reserveAfterLostRaces` was how many lost landing races passed before a commit asked for the landing
///  reservation. The commit service lands in arrival order and bounds the retries of a commit itself, so the key does nothing.
///  Older files that set it still load: the value is checked and ignored, and `git cli-git doctor` says so.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type LandingConfig = { reserveAfterLostRaces: number; present: boolean };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LandingConfig {
    /// The value the file gave, a whole number of at least 1; never used.
    pub reserve_after_lost_races: u64,
    /// Whether the file states `landing.reserveAfterLostRaces`.
    pub present: bool,
}

/// What:
///  The three retired concurrent-commit tuning groups together.
/// Why:
///   Concurrent commits cannot be turned off or tuned: every group is read, checked and ignored, so that an older
///  file still loads.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ConcurrencyConfig = { hooks: HooksConfig; indexLock: IndexLockConfig; landing: LandingConfig };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ConcurrencyConfig {
    /// Hook serialization.
    pub hooks: HooksConfig,
    /// Foreign index-lock patience.
    pub index_lock: IndexLockConfig,
    /// Landing starvation protection.
    pub landing: LandingConfig,
}

/// What:
///  The complete validated configuration of one repository.
/// Why:
///   A repository without a file and a repository with an empty `{}` file both yield
///       `CliGitConfig::defaults()`:
///  a policy that is off unless listed stays off either way.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type CliGitConfig = { policies: PolicyConfig; concurrency: ConcurrencyConfig };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CliGitConfig {
    /// Policy severities and options.
    pub policies: PolicyConfig,
    /// Concurrent-commit tuning.
    pub concurrency: ConcurrencyConfig,
}

/// What:
///  `impl ConcurrencyConfig { ... }` attaches functions to the struct.
/// Why:
///   Startup recovery runs before configuration loads and always uses these values.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const DEFAULT_CONCURRENCY_CONFIG: ConcurrencyConfig = { ... };
/// ```
impl ConcurrencyConfig {
    /// What:
    ///  Build the incumbent's default tuning.
    /// Why:
    ///   One function is the single source of the defaults for every caller.
    ///
    /// In TS you'd write (pseudocode):
    /// ```ts
    /// static defaults(): ConcurrencyConfig;
    /// ```
    pub fn defaults() -> ConcurrencyConfig {
        return ConcurrencyConfig {
            hooks: HooksConfig {
                concurrent_commits: false,
                present: false,
            },
            index_lock: IndexLockConfig {
                unproven_owner_timeout_ms: DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS,
                present: false,
            },
            landing: LandingConfig {
                reserve_after_lost_races: DEFAULT_RESERVE_AFTER_LOST_RACES,
                present: false,
            },
        };
    }
}

/// What:
///  The severity of a policy the repository configuration does not name.
///  `&PolicyDescriptor`
///       borrows the policy's registry row.
/// Why:
///   The five built-in policies run everywhere at their default severity.
///  The four
///       policies that plugins used to provide, and `mono/catalog-specifiers`, run only where
///       `cli-git.config.jsonc` names them,
///  whether or not a file exists,
///  so an empty file and no file behave alike.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const unlistedSeverity = descriptor.offUnlessListed ? 'off' : descriptor.defaultSeverity;
/// ```
pub fn unlisted_severity(descriptor: &PolicyDescriptor) -> Severity {
    if descriptor.off_unless_listed {
        return Severity::Off;
    }
    return descriptor.default_severity;
}

/// Defaults and lookup for policy settings.
impl PolicyConfig {
    /// What:
    ///  Build the settings of a repository whose configuration names no policy,
    ///  or
    ///       that has no configuration file at all.
    /// Why:
    ///   Parsing starts from these values and replaces only the policies the file names.
    ///
    /// In TS you'd write (pseudocode):
    /// ```ts
    /// static defaults(): PolicyConfig;
    /// ```
    pub fn defaults() -> PolicyConfig {
        // What: `Vec::<PolicySetting>::with_capacity(n)` is an empty list with room for
        //       `n` rows; `mut` permits `push`.
        // Why:  One row per registry policy, known up front.
        //
        // In TS you'd write (pseudocode):
        // ```ts
        // const settings: PolicySetting[] = [];
        // ```
        let mut settings: Vec<PolicySetting> =
            Vec::<PolicySetting>::with_capacity(POLICY_REGISTRY.len());
        // `for ... in` borrows each registry row in order.
        for descriptor in POLICY_REGISTRY {
            settings.push(PolicySetting {
                id: descriptor.id,
                severity: unlisted_severity(descriptor),
                explicit: false,
            });
        }
        return PolicyConfig {
            settings,
            forbidden_strings: ForbiddenStringsOptions {
                builtin_rules: true,
                // `None` is the "absent" variant: no file is configured.
                rules_files: None,
            },
            markdown_autofix: MarkdownAutofixOptions {
                // `vec![...]` is a macro building an owned list from literal items.
                rules: vec![MarkdownRule::LfsImageUrl],
                // `Vec::new()` is the empty owned list.
                exclude: Vec::<String>::new(),
            },
        };
    }

    /// What:
    ///  Read the effective setting of one policy.
    ///  `&self` borrows this record.
    /// Why:
    ///   Callers ask by typed identity instead of scanning the list themselves.
    ///
    /// In TS you'd write (pseudocode):
    /// ```ts
    /// setting(id: PolicyId): PolicySetting;
    /// ```
    pub fn setting(&self, id: PolicyId) -> PolicySetting {
        // `&self.settings` lends the list; `*setting` copies one small row out of it.
        for setting in &self.settings {
            if setting.id == id {
                return *setting;
            }
        }
        // Unreachable while `settings` holds one row per registry policy; fall back to
        // the unlisted severity rather than aborting a Git command.
        return PolicySetting {
            id,
            severity: unlisted_severity(policy_descriptor(id)),
            explicit: false,
        };
    }
}

/// Whole-configuration defaults.
impl CliGitConfig {
    /// What:
    ///  Build the configuration of a repository without `cli-git.config.jsonc`,
    ///  which
    ///       is also what an empty `{}` file produces.
    /// Why:
    ///   Parsing starts from these values and replaces only what the file states.
    ///
    /// In TS you'd write (pseudocode):
    /// ```ts
    /// static defaults(): CliGitConfig;
    /// ```
    pub fn defaults() -> CliGitConfig {
        return CliGitConfig {
            policies: PolicyConfig::defaults(),
            concurrency: ConcurrencyConfig::defaults(),
        };
    }
}

/// What:
///  Translate a configured rule name into its typed rule.
///       `Option<T>` is "value or nothing":
///  `Some(rule)` or `None`.
/// Why:
///   Only shipped rules exist;
///  the caller reports any other name with its key.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function markdownRuleFromName(name: string): MarkdownRule | undefined;
/// ```
pub fn markdown_rule_from_name(name: &str) -> Option<MarkdownRule> {
    if name == "lfs-image-url" {
        // `Some(...)` is the "present" variant.
        return Some(MarkdownRule::LfsImageUrl);
    }
    // `None` is the "absent" variant.
    return None;
}

/// What:
///  Translate a typed rule back to its configuration spelling.
///       `&'static str` borrows text compiled into the executable for its whole run.
/// Why:
///   Diagnostics and the later linter invocation print the accepted spelling.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function markdownRuleName(rule: MarkdownRule): string;
/// ```
pub fn markdown_rule_name(rule: MarkdownRule) -> &'static str {
    // `match` must name every variant, so a new rule cannot be forgotten here.
    match rule {
        MarkdownRule::LfsImageUrl => return "lfs-image-url",
    }
}

/// Value controls stay out of the release executable.
#[cfg(test)]
#[path = "config_schema_tests.rs"]
mod tests;