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
//! 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 is the incumbent's default patience for an unproven `index.lock` owner.
///
/// 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.
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:
/// Validated options of `security/forbidden-strings`.
/// `Option<String>` is "a
/// configured file name or nothing".
/// Why:
/// The scanner is linked into cli-git,
/// so the remaining choices are whether its
/// embedded baseline rules run and which repository file holds the private rules.
/// There is deliberately no executable path.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ForbiddenStringsOptions = { builtinRules: boolean; rulesFile?: string };
/// ```
#[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 file,
/// relative to the repository's top level,
/// already checked to
/// stay inside it;
/// when absent,
/// `FORBIDDEN_STRINGS_RULES` and then the default file apply.
pub rules_file: Option<String>,
}
/// 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:
/// Hook serialization tuning (`hooks` key).
/// Why:
/// `false` serializes preparation and post-landing hooks through the hook lock.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type HooksConfig = { concurrentCommits: boolean };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HooksConfig {
/// Whether hooks of concurrent commits may overlap;
/// default `false`.
pub concurrent_commits: bool,
}
/// What:
/// Foreign `index.lock` patience (`indexLock` key).
/// Why:
/// A lock whose owner cannot be proven alive is waited on only this long.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type IndexLockConfig = { unprovenOwnerTimeoutMs: number };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IndexLockConfig {
/// Backoff budget in milliseconds;
/// whole number of at least 0.
pub unproven_owner_timeout_ms: u64,
}
/// What:
/// Landing starvation tuning (`landing` key).
/// Why:
/// A commit that keeps losing landing races eventually reserves the next slot.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type LandingConfig = { reserveAfterLostRaces: number };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LandingConfig {
/// Lost races before reserving;
/// whole number of at least 1.
pub reserve_after_lost_races: u64,
}
/// What:
/// The three concurrent-commit tuning groups together.
/// Why:
/// None of them disables the commit transaction;
/// they only tune it.
///
/// 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,
},
index_lock: IndexLockConfig {
unproven_owner_timeout_ms: DEFAULT_UNPROVEN_OWNER_TIMEOUT_MS,
},
landing: LandingConfig {
reserve_after_lost_races: DEFAULT_RESERVE_AFTER_LOST_RACES,
},
};
}
}
/// 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 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_file: 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;