Skip to main content

git_policy_cli/
config_schema.rs

1//! What:
2//!  The typed settings a validated `cli-git.config.jsonc` produces,
3//!  with defaults.
4//! Why:
5//!  The policy engine and transactions read plain typed values,
6//!  never JSON.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // type CliGitConfig = { policies: PolicyConfig; concurrency: ConcurrencyConfig };
11//! ```
12
13/// What:
14///  Import the compiled-in registry and its identity and severity types.
15/// Why:
16///   Defaults are derived from the registry so the two can never disagree.
17///
18/// In TS you'd write (pseudocode):
19/// ```ts
20/// import { POLICY_REGISTRY, type PolicyId, type Severity } from './policy-registry.ts';
21/// ```
22use super::policy_registry::{
23    POLICY_REGISTRY, PolicyDescriptor, PolicyId, Severity, policy_descriptor,
24};
25
26/// What:
27///  `u64` constant,
28///  an unsigned 64-bit integer (siblings `u32`,
29///  `usize`,
30///  `i64`).
31/// Why:
32///   Milliseconds up to 2^53 - 1 are accepted,
33///  which `u32` cannot hold.
34///       This is the incumbent's default patience for an unproven `index.lock` owner.
35///
36/// In TS you'd write (pseudocode):
37/// ```ts
38/// const DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS = 1_000;
39/// ```
40pub const DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS: u64 = 1_000;
41
42/// Incumbent default:
43///  reserve the next landing slot after one lost landing race.
44pub const DEFAULT_RESERVE_AFTER_LOST_RACES: u64 = 1;
45
46/// What:
47///  The effective severity of one shipped policy.
48///       `#[derive(...)]` generates copy,
49///  debug printing and `==` for the struct.
50/// Why:
51///   `explicit` separates "the repository chose this" from "the default applied";
52///       only an explicit unsafe `warn` earns a configuration warning.
53///
54/// In TS you'd write (pseudocode):
55/// ```ts
56/// type PolicySetting = { id: PolicyId; severity: Severity; explicit: boolean };
57/// ```
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub struct PolicySetting {
60    /// Which shipped policy this row configures.
61    pub id: PolicyId,
62    /// Severity in effect for this invocation.
63    pub severity: Severity,
64    /// Whether the repository configuration named this policy.
65    pub explicit: bool,
66}
67
68/// What:
69///  The closed set of Markdown rules the commit-time policy may run.
70/// Why:
71///   The shipped policy runs the owned linter with a fixed rule list;
72///  an unknown
73///       rule name is a configuration error,
74///  never a pass-through argument.
75///
76/// In TS you'd write (pseudocode):
77/// ```ts
78/// type MarkdownRule = 'lfs-image-url';
79/// ```
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum MarkdownRule {
82    /// `lfs-image-url`:
83    ///  rewrite image links to LFS-tracked files to immutable object URLs.
84    LfsImageUrl,
85}
86
87/// What:
88///  Validated options of `markdown/autofix`.
89///       `Vec<T>` is an owned growable list (siblings:
90///  `&[T]` borrowed,
91///  `[T; N]` fixed).
92///       `String` is owned text (sibling:
93///  `&str` borrowed).
94/// Why:
95///   The settings outlive the parsed document,
96///  so they own their lists and text.
97///       There is deliberately no command or executable field.
98///
99/// In TS you'd write (pseudocode):
100/// ```ts
101/// type MarkdownAutofixOptions = { rules: MarkdownRule[]; exclude: string[] };
102/// ```
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct MarkdownAutofixOptions {
105    /// Rules to run,
106    ///  without repeats;
107    ///  defaults to the LFS image rewrite alone.
108    pub rules: Vec<MarkdownRule>,
109    /// gitignore-syntax patterns,
110    ///  relative to the repository root,
111    ///  left untouched.
112    pub exclude: Vec<String>,
113}
114
115/// What:
116///  Validated options of `security/forbidden-strings`.
117///  `Option<String>` is "a
118///       configured file name or nothing".
119/// Why:
120///   The scanner is linked into cli-git,
121///  so the remaining choices are whether its
122///       embedded baseline rules run and which repository file holds the private rules.
123///       There is deliberately no executable path.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// type ForbiddenStringsOptions = { builtinRules: boolean; rulesFile?: string };
128/// ```
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct ForbiddenStringsOptions {
131    /// Whether the scanner's embedded baseline rules are loaded;
132    ///  default `true`.
133    pub builtin_rules: bool,
134    /// The private rules file,
135    ///  relative to the repository's top level,
136    ///  already checked to
137    /// stay inside it;
138    ///  when absent,
139    ///  `FORBIDDEN_STRINGS_RULES` and then the default file apply.
140    pub rules_file: Option<String>,
141}
142
143/// What:
144///  Every policy's severity plus the option records of option-bearing policies.
145/// Why:
146///   One value answers "does this policy run,
147///  how strictly,
148///  with what options".
149///
150/// In TS you'd write (pseudocode):
151/// ```ts
152/// type PolicyConfig = { settings: PolicySetting[];
153///   forbiddenStrings: ForbiddenStringsOptions; markdownAutofix: MarkdownAutofixOptions };
154/// ```
155#[derive(Clone, Debug, Eq, PartialEq)]
156pub struct PolicyConfig {
157    /// One row per registry policy,
158    ///  in registry order.
159    pub settings: Vec<PolicySetting>,
160    /// Options of `security/forbidden-strings`,
161    ///  defaults applied.
162    pub forbidden_strings: ForbiddenStringsOptions,
163    /// Options of `markdown/autofix`,
164    ///  defaults applied.
165    pub markdown_autofix: MarkdownAutofixOptions,
166}
167
168/// What:
169///  Hook serialization tuning (`hooks` key).
170/// Why:
171///   `false` serializes preparation and post-landing hooks through the hook lock.
172///
173/// In TS you'd write (pseudocode):
174/// ```ts
175/// type HooksConfig = { concurrentCommits: boolean };
176/// ```
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178pub struct HooksConfig {
179    /// Whether hooks of concurrent commits may overlap;
180    ///  default `false`.
181    pub concurrent_commits: bool,
182}
183
184/// What:
185///  Foreign `index.lock` patience (`indexLock` key).
186/// Why:
187///   A lock whose owner cannot be proven alive is waited on only this long.
188///
189/// In TS you'd write (pseudocode):
190/// ```ts
191/// type IndexLockConfig = { unprovenOwnerTimeoutMs: number };
192/// ```
193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
194pub struct IndexLockConfig {
195    /// Backoff budget in milliseconds;
196    ///  whole number of at least 0.
197    pub unproven_owner_timeout_ms: u64,
198}
199
200/// What:
201///  Landing starvation tuning (`landing` key).
202/// Why:
203///   A commit that keeps losing landing races eventually reserves the next slot.
204///
205/// In TS you'd write (pseudocode):
206/// ```ts
207/// type LandingConfig = { reserveAfterLostRaces: number };
208/// ```
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub struct LandingConfig {
211    /// Lost races before reserving;
212    ///  whole number of at least 1.
213    pub reserve_after_lost_races: u64,
214}
215
216/// What:
217///  The three concurrent-commit tuning groups together.
218/// Why:
219///   None of them disables the commit transaction;
220///  they only tune it.
221///
222/// In TS you'd write (pseudocode):
223/// ```ts
224/// type ConcurrencyConfig = { hooks: HooksConfig; indexLock: IndexLockConfig; landing: LandingConfig };
225/// ```
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub struct ConcurrencyConfig {
228    /// Hook serialization.
229    pub hooks: HooksConfig,
230    /// Foreign index-lock patience.
231    pub index_lock: IndexLockConfig,
232    /// Landing starvation protection.
233    pub landing: LandingConfig,
234}
235
236/// What:
237///  The complete validated configuration of one repository.
238/// Why:
239///   A repository without a file and a repository with an empty `{}` file both yield
240///       `CliGitConfig::defaults()`:
241///  a policy that is off unless listed stays off either way.
242///
243/// In TS you'd write (pseudocode):
244/// ```ts
245/// type CliGitConfig = { policies: PolicyConfig; concurrency: ConcurrencyConfig };
246/// ```
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct CliGitConfig {
249    /// Policy severities and options.
250    pub policies: PolicyConfig,
251    /// Concurrent-commit tuning.
252    pub concurrency: ConcurrencyConfig,
253}
254
255/// What:
256///  `impl ConcurrencyConfig { ... }` attaches functions to the struct.
257/// Why:
258///   Startup recovery runs before configuration loads and always uses these values.
259///
260/// In TS you'd write (pseudocode):
261/// ```ts
262/// const DEFAULT_CONCURRENCY_CONFIG: ConcurrencyConfig = { ... };
263/// ```
264impl ConcurrencyConfig {
265    /// What:
266    ///  Build the incumbent's default tuning.
267    /// Why:
268    ///   One function is the single source of the defaults for every caller.
269    ///
270    /// In TS you'd write (pseudocode):
271    /// ```ts
272    /// static defaults(): ConcurrencyConfig;
273    /// ```
274    pub fn defaults() -> ConcurrencyConfig {
275        return ConcurrencyConfig {
276            hooks: HooksConfig {
277                concurrent_commits: false,
278            },
279            index_lock: IndexLockConfig {
280                unproven_owner_timeout_ms: DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS,
281            },
282            landing: LandingConfig {
283                reserve_after_lost_races: DEFAULT_RESERVE_AFTER_LOST_RACES,
284            },
285        };
286    }
287}
288
289/// What:
290///  The severity of a policy the repository configuration does not name.
291///  `&PolicyDescriptor`
292///       borrows the policy's registry row.
293/// Why:
294///   The five built-in policies run everywhere at their default severity.
295///  The four
296///       policies that plugins used to provide run only where `cli-git.config.jsonc` names
297///       them,
298///  whether or not a file exists,
299///  so an empty file and no file behave alike.
300///
301/// In TS you'd write (pseudocode):
302/// ```ts
303/// const unlistedSeverity = descriptor.offUnlessListed ? 'off' : descriptor.defaultSeverity;
304/// ```
305pub fn unlisted_severity(descriptor: &PolicyDescriptor) -> Severity {
306    if descriptor.off_unless_listed {
307        return Severity::Off;
308    }
309    return descriptor.default_severity;
310}
311
312/// Defaults and lookup for policy settings.
313impl PolicyConfig {
314    /// What:
315    ///  Build the settings of a repository whose configuration names no policy,
316    ///  or
317    ///       that has no configuration file at all.
318    /// Why:
319    ///   Parsing starts from these values and replaces only the policies the file names.
320    ///
321    /// In TS you'd write (pseudocode):
322    /// ```ts
323    /// static defaults(): PolicyConfig;
324    /// ```
325    pub fn defaults() -> PolicyConfig {
326        // What: `Vec::<PolicySetting>::with_capacity(n)` is an empty list with room for
327        //       `n` rows; `mut` permits `push`.
328        // Why:  One row per registry policy, known up front.
329        //
330        // In TS you'd write (pseudocode):
331        // ```ts
332        // const settings: PolicySetting[] = [];
333        // ```
334        let mut settings: Vec<PolicySetting> =
335            Vec::<PolicySetting>::with_capacity(POLICY_REGISTRY.len());
336        // `for ... in` borrows each registry row in order.
337        for descriptor in POLICY_REGISTRY {
338            settings.push(PolicySetting {
339                id: descriptor.id,
340                severity: unlisted_severity(descriptor),
341                explicit: false,
342            });
343        }
344        return PolicyConfig {
345            settings,
346            forbidden_strings: ForbiddenStringsOptions {
347                builtin_rules: true,
348                // `None` is the "absent" variant: no file is configured.
349                rules_file: None,
350            },
351            markdown_autofix: MarkdownAutofixOptions {
352                // `vec![...]` is a macro building an owned list from literal items.
353                rules: vec![MarkdownRule::LfsImageUrl],
354                // `Vec::new()` is the empty owned list.
355                exclude: Vec::<String>::new(),
356            },
357        };
358    }
359
360    /// What:
361    ///  Read the effective setting of one policy.
362    ///  `&self` borrows this record.
363    /// Why:
364    ///   Callers ask by typed identity instead of scanning the list themselves.
365    ///
366    /// In TS you'd write (pseudocode):
367    /// ```ts
368    /// setting(id: PolicyId): PolicySetting;
369    /// ```
370    pub fn setting(&self, id: PolicyId) -> PolicySetting {
371        // `&self.settings` lends the list; `*setting` copies one small row out of it.
372        for setting in &self.settings {
373            if setting.id == id {
374                return *setting;
375            }
376        }
377        // Unreachable while `settings` holds one row per registry policy; fall back to
378        // the unlisted severity rather than aborting a Git command.
379        return PolicySetting {
380            id,
381            severity: unlisted_severity(policy_descriptor(id)),
382            explicit: false,
383        };
384    }
385}
386
387/// Whole-configuration defaults.
388impl CliGitConfig {
389    /// What:
390    ///  Build the configuration of a repository without `cli-git.config.jsonc`,
391    ///  which
392    ///       is also what an empty `{}` file produces.
393    /// Why:
394    ///   Parsing starts from these values and replaces only what the file states.
395    ///
396    /// In TS you'd write (pseudocode):
397    /// ```ts
398    /// static defaults(): CliGitConfig;
399    /// ```
400    pub fn defaults() -> CliGitConfig {
401        return CliGitConfig {
402            policies: PolicyConfig::defaults(),
403            concurrency: ConcurrencyConfig::defaults(),
404        };
405    }
406}
407
408/// What:
409///  Translate a configured rule name into its typed rule.
410///       `Option<T>` is "value or nothing":
411///  `Some(rule)` or `None`.
412/// Why:
413///   Only shipped rules exist;
414///  the caller reports any other name with its key.
415///
416/// In TS you'd write (pseudocode):
417/// ```ts
418/// function markdownRuleFromName(name: string): MarkdownRule | undefined;
419/// ```
420pub fn markdown_rule_from_name(name: &str) -> Option<MarkdownRule> {
421    if name == "lfs-image-url" {
422        // `Some(...)` is the "present" variant.
423        return Some(MarkdownRule::LfsImageUrl);
424    }
425    // `None` is the "absent" variant.
426    return None;
427}
428
429/// What:
430///  Translate a typed rule back to its configuration spelling.
431///       `&'static str` borrows text compiled into the executable for its whole run.
432/// Why:
433///   Diagnostics and the later linter invocation print the accepted spelling.
434///
435/// In TS you'd write (pseudocode):
436/// ```ts
437/// function markdownRuleName(rule: MarkdownRule): string;
438/// ```
439pub fn markdown_rule_name(rule: MarkdownRule) -> &'static str {
440    // `match` must name every variant, so a new rule cannot be forgotten here.
441    match rule {
442        MarkdownRule::LfsImageUrl => return "lfs-image-url",
443    }
444}
445
446/// Value controls stay out of the release executable.
447#[cfg(test)]
448#[path = "config_schema_tests.rs"]
449mod tests;