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 was the incumbent's default patience for an unproven `index.lock` owner; nothing reads it now.
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; nothing reads it now.
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/// One rules file named by the `rulesFile` option:
117/// its path inside the repository and whether it may be absent.
118/// Why:
119/// The configuration names several files as an array,
120/// and a gitignored local file must be allowed to be missing
121/// on a fresh clone while a committed shared file must not.
122/// The path is the configuration's own text, with no comma or `?` syntax,
123/// because JSON can already say which file is meant.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// type RulesFileEntry = { path: string; optional: boolean };
128/// ```
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct RulesFileEntry {
131 /// The file,
132 /// relative to the repository's top level and already checked to stay inside it.
133 pub path: String,
134 /// Whether the file may be absent;
135 /// an absent optional file is skipped and an absent required file stops the policy.
136 pub optional: bool,
137}
138
139/// What:
140/// Validated options of `security/forbidden-strings`.
141/// `Option<Vec<RulesFileEntry>>` is "a
142/// configured list of files or nothing".
143/// Why:
144/// The scanner is linked into cli-git,
145/// so the remaining choices are whether its
146/// embedded baseline rules run and which repository files hold the private rules.
147/// There is deliberately no executable path.
148///
149/// In TS you'd write (pseudocode):
150/// ```ts
151/// type ForbiddenStringsOptions = { builtinRules: boolean; rulesFile?: string | (string | { path: string; optional?: boolean })[] };
152/// ```
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct ForbiddenStringsOptions {
155 /// Whether the scanner's embedded baseline rules are loaded;
156 /// default `true`.
157 pub builtin_rules: bool,
158 /// The private rules files in the order they are read,
159 /// each relative to the repository's top level and already checked to
160 /// stay inside it;
161 /// never an empty list;
162 /// when absent,
163 /// `FORBIDDEN_STRINGS_RULES` and then the default file apply.
164 pub rules_files: Option<Vec<RulesFileEntry>>,
165}
166
167/// What:
168/// Every policy's severity plus the option records of option-bearing policies.
169/// Why:
170/// One value answers "does this policy run,
171/// how strictly,
172/// with what options".
173///
174/// In TS you'd write (pseudocode):
175/// ```ts
176/// type PolicyConfig = { settings: PolicySetting[];
177/// forbiddenStrings: ForbiddenStringsOptions; markdownAutofix: MarkdownAutofixOptions };
178/// ```
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct PolicyConfig {
181 /// One row per registry policy,
182 /// in registry order.
183 pub settings: Vec<PolicySetting>,
184 /// Options of `security/forbidden-strings`,
185 /// defaults applied.
186 pub forbidden_strings: ForbiddenStringsOptions,
187 /// Options of `markdown/autofix`,
188 /// defaults applied.
189 pub markdown_autofix: MarkdownAutofixOptions,
190}
191
192/// What:
193/// The retired `hooks` key.
194/// Why:
195/// `hooks.concurrentCommits` tuned the hook lock, and the wrapper runs no external hook, so there is no lock to
196/// tune.
197/// Older files that set it still load: the value is checked and ignored, and `git cli-git doctor` says so.
198///
199/// In TS you'd write (pseudocode):
200/// ```ts
201/// type HooksConfig = { concurrentCommits: boolean; present: boolean };
202/// ```
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub struct HooksConfig {
205 /// The value the file gave; never used.
206 pub concurrent_commits: bool,
207 /// Whether the file states `hooks.concurrentCommits`.
208 pub present: bool,
209}
210
211/// What:
212/// The retired `indexLock` key.
213/// Why:
214/// `indexLock.unprovenOwnerTimeoutMs` was how long the wrapper backed off for an `index.lock` whose owner is dead or
215/// unproven. The commit service waits for a foreign `index.lock` as long as `CLI_GIT_LOCK_WAIT_SECONDS` allows and reads no
216/// such value, so the key does nothing.
217/// Older files that set it still load: the value is checked and ignored, and `git cli-git doctor` says so.
218///
219/// In TS you'd write (pseudocode):
220/// ```ts
221/// type IndexLockConfig = { unprovenOwnerTimeoutMs: number; present: boolean };
222/// ```
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub struct IndexLockConfig {
225 /// The value the file gave, a whole number of at least 0; never used.
226 pub unproven_owner_timeout_ms: u64,
227 /// Whether the file states `indexLock.unprovenOwnerTimeoutMs`.
228 pub present: bool,
229}
230
231/// What:
232/// The retired `landing` key.
233/// Why:
234/// `landing.reserveAfterLostRaces` was how many lost landing races passed before a commit asked for the landing
235/// reservation. The commit service lands in arrival order and bounds the retries of a commit itself, so the key does nothing.
236/// Older files that set it still load: the value is checked and ignored, and `git cli-git doctor` says so.
237///
238/// In TS you'd write (pseudocode):
239/// ```ts
240/// type LandingConfig = { reserveAfterLostRaces: number; present: boolean };
241/// ```
242#[derive(Clone, Copy, Debug, Eq, PartialEq)]
243pub struct LandingConfig {
244 /// The value the file gave, a whole number of at least 1; never used.
245 pub reserve_after_lost_races: u64,
246 /// Whether the file states `landing.reserveAfterLostRaces`.
247 pub present: bool,
248}
249
250/// What:
251/// The three retired concurrent-commit tuning groups together.
252/// Why:
253/// Concurrent commits cannot be turned off or tuned: every group is read, checked and ignored, so that an older
254/// file still loads.
255///
256/// In TS you'd write (pseudocode):
257/// ```ts
258/// type ConcurrencyConfig = { hooks: HooksConfig; indexLock: IndexLockConfig; landing: LandingConfig };
259/// ```
260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
261pub struct ConcurrencyConfig {
262 /// Hook serialization.
263 pub hooks: HooksConfig,
264 /// Foreign index-lock patience.
265 pub index_lock: IndexLockConfig,
266 /// Landing starvation protection.
267 pub landing: LandingConfig,
268}
269
270/// What:
271/// The complete validated configuration of one repository.
272/// Why:
273/// A repository without a file and a repository with an empty `{}` file both yield
274/// `CliGitConfig::defaults()`:
275/// a policy that is off unless listed stays off either way.
276///
277/// In TS you'd write (pseudocode):
278/// ```ts
279/// type CliGitConfig = { policies: PolicyConfig; concurrency: ConcurrencyConfig };
280/// ```
281#[derive(Clone, Debug, Eq, PartialEq)]
282pub struct CliGitConfig {
283 /// Policy severities and options.
284 pub policies: PolicyConfig,
285 /// Concurrent-commit tuning.
286 pub concurrency: ConcurrencyConfig,
287}
288
289/// What:
290/// `impl ConcurrencyConfig { ... }` attaches functions to the struct.
291/// Why:
292/// Startup recovery runs before configuration loads and always uses these values.
293///
294/// In TS you'd write (pseudocode):
295/// ```ts
296/// const DEFAULT_CONCURRENCY_CONFIG: ConcurrencyConfig = { ... };
297/// ```
298impl ConcurrencyConfig {
299 /// What:
300 /// Build the incumbent's default tuning.
301 /// Why:
302 /// One function is the single source of the defaults for every caller.
303 ///
304 /// In TS you'd write (pseudocode):
305 /// ```ts
306 /// static defaults(): ConcurrencyConfig;
307 /// ```
308 pub fn defaults() -> ConcurrencyConfig {
309 return ConcurrencyConfig {
310 hooks: HooksConfig {
311 concurrent_commits: false,
312 present: false,
313 },
314 index_lock: IndexLockConfig {
315 unproven_owner_timeout_ms: DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS,
316 present: false,
317 },
318 landing: LandingConfig {
319 reserve_after_lost_races: DEFAULT_RESERVE_AFTER_LOST_RACES,
320 present: false,
321 },
322 };
323 }
324}
325
326/// What:
327/// The severity of a policy the repository configuration does not name.
328/// `&PolicyDescriptor`
329/// borrows the policy's registry row.
330/// Why:
331/// The five built-in policies run everywhere at their default severity.
332/// The four
333/// policies that plugins used to provide, and `mono/catalog-specifiers`, run only where
334/// `cli-git.config.jsonc` names them,
335/// whether or not a file exists,
336/// so an empty file and no file behave alike.
337///
338/// In TS you'd write (pseudocode):
339/// ```ts
340/// const unlistedSeverity = descriptor.offUnlessListed ? 'off' : descriptor.defaultSeverity;
341/// ```
342pub fn unlisted_severity(descriptor: &PolicyDescriptor) -> Severity {
343 if descriptor.off_unless_listed {
344 return Severity::Off;
345 }
346 return descriptor.default_severity;
347}
348
349/// Defaults and lookup for policy settings.
350impl PolicyConfig {
351 /// What:
352 /// Build the settings of a repository whose configuration names no policy,
353 /// or
354 /// that has no configuration file at all.
355 /// Why:
356 /// Parsing starts from these values and replaces only the policies the file names.
357 ///
358 /// In TS you'd write (pseudocode):
359 /// ```ts
360 /// static defaults(): PolicyConfig;
361 /// ```
362 pub fn defaults() -> PolicyConfig {
363 // What: `Vec::<PolicySetting>::with_capacity(n)` is an empty list with room for
364 // `n` rows; `mut` permits `push`.
365 // Why: One row per registry policy, known up front.
366 //
367 // In TS you'd write (pseudocode):
368 // ```ts
369 // const settings: PolicySetting[] = [];
370 // ```
371 let mut settings: Vec<PolicySetting> =
372 Vec::<PolicySetting>::with_capacity(POLICY_REGISTRY.len());
373 // `for ... in` borrows each registry row in order.
374 for descriptor in POLICY_REGISTRY {
375 settings.push(PolicySetting {
376 id: descriptor.id,
377 severity: unlisted_severity(descriptor),
378 explicit: false,
379 });
380 }
381 return PolicyConfig {
382 settings,
383 forbidden_strings: ForbiddenStringsOptions {
384 builtin_rules: true,
385 // `None` is the "absent" variant: no file is configured.
386 rules_files: None,
387 },
388 markdown_autofix: MarkdownAutofixOptions {
389 // `vec![...]` is a macro building an owned list from literal items.
390 rules: vec![MarkdownRule::LfsImageUrl],
391 // `Vec::new()` is the empty owned list.
392 exclude: Vec::<String>::new(),
393 },
394 };
395 }
396
397 /// What:
398 /// Read the effective setting of one policy.
399 /// `&self` borrows this record.
400 /// Why:
401 /// Callers ask by typed identity instead of scanning the list themselves.
402 ///
403 /// In TS you'd write (pseudocode):
404 /// ```ts
405 /// setting(id: PolicyId): PolicySetting;
406 /// ```
407 pub fn setting(&self, id: PolicyId) -> PolicySetting {
408 // `&self.settings` lends the list; `*setting` copies one small row out of it.
409 for setting in &self.settings {
410 if setting.id == id {
411 return *setting;
412 }
413 }
414 // Unreachable while `settings` holds one row per registry policy; fall back to
415 // the unlisted severity rather than aborting a Git command.
416 return PolicySetting {
417 id,
418 severity: unlisted_severity(policy_descriptor(id)),
419 explicit: false,
420 };
421 }
422}
423
424/// Whole-configuration defaults.
425impl CliGitConfig {
426 /// What:
427 /// Build the configuration of a repository without `cli-git.config.jsonc`,
428 /// which
429 /// is also what an empty `{}` file produces.
430 /// Why:
431 /// Parsing starts from these values and replaces only what the file states.
432 ///
433 /// In TS you'd write (pseudocode):
434 /// ```ts
435 /// static defaults(): CliGitConfig;
436 /// ```
437 pub fn defaults() -> CliGitConfig {
438 return CliGitConfig {
439 policies: PolicyConfig::defaults(),
440 concurrency: ConcurrencyConfig::defaults(),
441 };
442 }
443}
444
445/// What:
446/// Translate a configured rule name into its typed rule.
447/// `Option<T>` is "value or nothing":
448/// `Some(rule)` or `None`.
449/// Why:
450/// Only shipped rules exist;
451/// the caller reports any other name with its key.
452///
453/// In TS you'd write (pseudocode):
454/// ```ts
455/// function markdownRuleFromName(name: string): MarkdownRule | undefined;
456/// ```
457pub fn markdown_rule_from_name(name: &str) -> Option<MarkdownRule> {
458 if name == "lfs-image-url" {
459 // `Some(...)` is the "present" variant.
460 return Some(MarkdownRule::LfsImageUrl);
461 }
462 // `None` is the "absent" variant.
463 return None;
464}
465
466/// What:
467/// Translate a typed rule back to its configuration spelling.
468/// `&'static str` borrows text compiled into the executable for its whole run.
469/// Why:
470/// Diagnostics and the later linter invocation print the accepted spelling.
471///
472/// In TS you'd write (pseudocode):
473/// ```ts
474/// function markdownRuleName(rule: MarkdownRule): string;
475/// ```
476pub fn markdown_rule_name(rule: MarkdownRule) -> &'static str {
477 // `match` must name every variant, so a new rule cannot be forgotten here.
478 match rule {
479 MarkdownRule::LfsImageUrl => return "lfs-image-url",
480 }
481}
482
483/// Value controls stay out of the release executable.
484#[cfg(test)]
485#[path = "config_schema_tests.rs"]
486mod tests;