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;