monochromatic-git-policy-cli 0.1.4

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
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
//! What:
//!  The shipped policies as the engine sees them:
//!  one check per policy,
//!  built from
//!       the pure rule cores and the repository facts they ask for.
//! Why:
//!  The engine owns order,
//!  severity and stopping;
//!  the rule cores own decisions from
//!      arguments;
//!  this module joins them and fetches a repository fact only when a core
//!      asks.
//!  The content policies read the lifecycle's candidates:
//!  they report nothing
//!      where a lifecycle has none.
//!  The Markdown policy checks the candidates it is given
//!      with the native Markdown linter.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const findings = await policy.check({ context });
//! ```

/// What:
///  `use` brings names from sibling files into this file;
///  `super::` means "the parent
///       module",
///  where every sibling file of this crate is declared.
/// Why:
///   Each check combines one rule core with the facts interface.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { decideRequireRoot, resolveRequireRoot } from './rule_require_root.ts';
/// ```
use super::diagnostics::EngineFailureCode;
/// Which worktree a command targets,
///  for the linked-worktree rule.
use super::effective_target::{EffectiveTarget, classify_effective_target};
/// The Markdown autofix check,
///  its state and the linter the executable gives it.
use super::markdown_linter::executable_linter;
/// What the lifecycle offers content policies,
///  and its prepared candidates.
use super::policy_content::{ContentState, LifecycleContent};
/// The dependent-version check over the candidate state.
use super::policy_dependent_version::check_dependent_version;
/// The engine's check interface,
///  findings and outcomes.
use super::policy_engine::{PolicyChecks, PolicyFinding, PolicyOutcome};
/// The built-in final-newline check over candidates.
use super::policy_final_newline::check_final_newline;
/// The forbidden-strings check,
///  its settings and its scanner.
use super::policy_forbidden_strings::{
    ScannerSettings, ScannerState, check_forbidden_strings, default_scanner_settings,
    unloaded_scanner,
};
use super::policy_markdown::{MarkdownState, check_markdown, markdown_state};
/// The typed policy identities.
use super::policy_registry::PolicyId;
/// The optional root-context check over candidates.
use super::policy_root_context::check_root_context;
/// The lifecycle points.
use super::policy_trigger::Trigger;
/// The facts interface every check asks.
use super::repository_facts::RepositoryFacts;
/// Where a command runs,
///  and the directory Git works in.
use super::repository_location::{RepositoryLocation, effective_directory};
/// The add-explicit rule core.
use super::rule_add_explicit::{BULK_ADD_CODE, decide_add_explicit};
/// The branch-worktree rule core.
use super::rule_branch_worktree::{
    BRANCH_CREATION_CODE, BranchWorktreeDecision, branch_creation_message, decide_branch_worktree,
};
/// The linked-worktree rule core.
use super::rule_linked_worktree::{
    LINKED_WORKTREE_REQUIRED_CODE, LinkedWorktreeDecision, decide_linked_worktree,
    resolve_linked_worktree,
};
/// The require-root rule core.
use super::rule_require_root::{
    NOT_AT_ROOT_CODE, RequireRootDecision, RequireRootFacts, RequireRootVerdict,
    decide_require_root, resolve_require_root,
};
/// The worktree's top level.
use super::worktree_identity::worktree_root;
/// What:
///  `OsString` is owned operating-system text of raw bytes.
///  Sibling the reader might
///       expect:
///  `String`,
///  which must be valid UTF-8.
/// Why:
///   The command's arguments are kept exactly as Git will receive them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // string, but byte-preserving.
/// ```
use std::ffi::OsString;
/// `PathBuf` is an owned filesystem path of raw bytes.
use std::path::PathBuf;

/// What:
///  The shipped policies over one invocation.
///  `<F: RepositoryFacts>` says the record
///       works with any one type `F` that provides the facts interface,
///  chosen where the
///       record is built:
///  real Git in the executable,
///  a script in tests.
/// Why:
///   Holding the provider by value keeps its remembered answers and its query count
///       for the whole invocation,
///  and lets a test read them back afterwards.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ShippedChecks<F extends RepositoryFacts> = { facts: F; arguments: string[]; candidates: LifecycleContent; allowedWorktreeDirs: string[]; content: ContentState; markdown: MarkdownState };
/// ```
pub struct ShippedChecks<F: RepositoryFacts> {
    /// Whether a pass may propose corrections that its caller applies; the commit path's convergence sets it.
    pub can_apply_patches: bool,
    /// Where repository facts come from.
    pub facts: F,
    /// The command's arguments without wrapper controls;
    ///  for a direct command,
    ///  the Git
    /// global options written before `cli-git`.
    pub arguments: Vec<OsString>,
    /// What the lifecycle offers content policies.
    pub candidates: LifecycleContent,
    /// Tool-cache directories exempt from linked-worktree enforcement.
    pub allowed_worktree_dirs: Vec<PathBuf>,
    /// The candidates once a content policy prepared them,
    ///  shared by every later one.
    pub content: ContentState,
    /// Where the forbidden-strings rules come from:
    ///  options and environment.
    pub scanner_settings: ScannerSettings,
    /// The forbidden-strings scanner once loaded,
    ///  shared by every later scan.
    pub scanner: ScannerState,
    /// The Markdown autofix policy's options,
    ///  linter and exclude patterns.
    pub markdown: MarkdownState,
}

/// What:
///  The shipped checks for one invocation,
///  with no candidates yet prepared.
///       `<F: RepositoryFacts>` accepts any facts provider.
/// Why:
///   Every lifecycle builds its checks the same way;
///  it only chooses what the
///       content policies may read.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function shippedChecks(facts, args, candidates, allowedWorktreeDirs): ShippedChecks;
/// ```
pub fn shipped_checks<F: RepositoryFacts>(
    facts: F,
    arguments: Vec<OsString>,
    candidates: LifecycleContent,
    allowed_worktree_dirs: Vec<PathBuf>,
) -> ShippedChecks<F> {
    return ShippedChecks {
        can_apply_patches: false,
        facts,
        arguments,
        candidates,
        allowed_worktree_dirs,
        content: ContentState::new(),
        scanner_settings: default_scanner_settings(),
        scanner: unloaded_scanner(),
        // An empty environment has no PATH: until a lifecycle gives the invocation's
        // environment, a Markdown candidate fails as unchecked rather than passing.
        markdown: markdown_state(Box::new(executable_linter(&[]))),
    };
}

/// What:
///  The outcome of a check whose repository fact could not be read.
///       `String` is the owned reason the facts provider gave.
/// Why:
///   Every fact these checks ask for is read from the repository,
///  so its absence is
///       `content-unavailable`,
///  never a failure of the policy itself.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const factUnavailable = (message: string): PolicyOutcome => ({ kind: 'failed', code: 'content-unavailable', message });
/// ```
fn fact_unavailable(message: String) -> PolicyOutcome {
    return PolicyOutcome::Failed {
        code: EngineFailureCode::ContentUnavailable,
        message,
        // `None`: a repository fact is about no single file.
        path: None,
    };
}

/// What:
///  The outcome "nothing found".
///  `Vec::<PolicyFinding>::new()` is an empty owned list.
/// Why:
///   Most checks end this way;
///  one helper keeps them identical.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const clean = (): PolicyOutcome => ({ kind: 'findings', findings: [] });
/// ```
fn clean() -> PolicyOutcome {
    return PolicyOutcome::Findings(Vec::<PolicyFinding>::new());
}

/// What:
///  The outcome "exactly one finding about the command".
///  `&'static str` is text
///       baked into the program;
///  `String` is owned text.
/// Why:
///   The four command policies each report at most one finding,
///  without a path.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const rejected = (code: string, message: string): PolicyOutcome => ({ kind: 'findings', findings: [{ code, message }] });
/// ```
fn rejected(code: &'static str, message: String) -> PolicyOutcome {
    // `vec![...]` is a macro building an owned list from literal items.
    return PolicyOutcome::Findings(vec![PolicyFinding {
        code,
        message,
        // `None` is the "absent" case of `Option`.
        path: None,
        location: None,
        fix_available: false,
    }]);
}

/// What:
///  The require-root check.
///  `&mut dyn RepositoryFacts` lends "any facts provider" for
///       writing (`dyn` means the concrete type is chosen at run time,
///  like a TS interface
///       value);
///  `&[OsString]` borrows the arguments.
/// Why:
///   A forwarded command is first judged by its arguments,
///  because many commands are
///       exempt.
///  A direct check has no Git command whose exemption could apply.
///  The root
///       and the directory both come from Git's own answer.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function checkRequireRoot(facts: RepositoryFacts, args: string[], trigger: PolicyTrigger): Promise<PolicyOutcome>;
/// ```
fn check_require_root(
    facts: &mut dyn RepositoryFacts,
    arguments: &[OsString],
    trigger: Trigger,
) -> PolicyOutcome {
    // `&[]` is an empty flag list: wrapper controls were removed before this point.
    if trigger == Trigger::PreForward
        && decide_require_root(arguments, &[]) != RequireRootDecision::NeedsRepositoryRoot
    {
        return clean();
    }
    // `match` unpacks the answer; `Ok`/`Err` are the success/failure variants.
    let location: RepositoryLocation = match facts.location() {
        Ok(found) => found,
        Err(message) => return fact_unavailable(message),
    };
    // `let Some(x) = ... else { ... };` unwraps the top level or returns: no worktree, no root.
    let Some(root) = worktree_root(&location.identity) else {
        return clean();
    };
    let measured: RequireRootFacts = RequireRootFacts {
        effective_directory: effective_directory(root, location.prefix.as_slice()),
        // `Some(x)` is the "present" case; `.to_path_buf()` copies the borrowed path.
        repository_root: Some(root.to_path_buf()),
    };
    // `&measured` lends the measurements read-only.
    match resolve_require_root(&measured) {
        RequireRootVerdict::Pass => return clean(),
        RequireRootVerdict::NotAtRoot(violation) => {
            return rejected(NOT_AT_ROOT_CODE, violation.message);
        }
    }
}

/// What:
///  The linked-worktree-only check.
///  `&[PathBuf]` borrows the exempt tool caches.
/// Why:
///   Only a guarded,
///  state-changing command needs to know which worktree it targets.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function checkLinkedWorktree(facts, args, allowedDirs): Promise<PolicyOutcome>;
/// ```
fn check_linked_worktree(
    facts: &mut dyn RepositoryFacts,
    arguments: &[OsString],
    allowed_worktree_dirs: &[PathBuf],
) -> PolicyOutcome {
    // `let Variant(x) = ... else { ... };` unwraps the guarded command or returns.
    let LinkedWorktreeDecision::NeedsEffectiveTarget(command) = decide_linked_worktree(arguments)
    else {
        return clean();
    };
    let location: RepositoryLocation = match facts.location() {
        Ok(found) => found,
        Err(message) => return fact_unavailable(message),
    };
    let target: EffectiveTarget =
        classify_effective_target(&location.identity, allowed_worktree_dirs);
    match resolve_linked_worktree(command, target) {
        // `String::from` copies the compiled-in text into owned text.
        Some(message) => return rejected(LINKED_WORKTREE_REQUIRED_CODE, String::from(message)),
        None => return clean(),
    }
}

/// What:
///  The branch-worktree-only check.
/// Why:
///   Explicit creation is rejected from the arguments;
///  a bare name is rejected only
///       when Git would create a branch for it from the one remote that has it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function checkBranchWorktree(facts, args): Promise<PolicyOutcome>;
/// ```
fn check_branch_worktree(facts: &mut dyn RepositoryFacts, arguments: &[OsString]) -> PolicyOutcome {
    match decide_branch_worktree(arguments) {
        BranchWorktreeDecision::Pass => return clean(),
        BranchWorktreeDecision::Creates(command) => {
            return rejected(BRANCH_CREATION_CODE, branch_creation_message(command, None));
        }
        BranchWorktreeDecision::NeedsRemoteGuess { command, target } => {
            // `.as_os_str()` lends the owned name as a borrowed one.
            match facts.remote_guess_creates_branch(target.as_os_str()) {
                Err(message) => return fact_unavailable(message),
                Ok(false) => return clean(),
                Ok(true) => {
                    return rejected(
                        BRANCH_CREATION_CODE,
                        branch_creation_message(command, Some(target.as_os_str())),
                    );
                }
            }
        }
    }
}

/// What:
///  The add-explicit check.
/// Why:
///   The decision needs the arguments only.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function checkAddExplicit(args: string[]): PolicyOutcome;
/// ```
fn check_add_explicit(arguments: &[OsString]) -> PolicyOutcome {
    match decide_add_explicit(arguments) {
        Some(message) => return rejected(BULK_ADD_CODE, message),
        None => return clean(),
    }
}

/// What:
///  `impl<F: RepositoryFacts> PolicyChecks for ShippedChecks<F>` provides the engine's
///       interface for the shipped policies,
///  for any facts provider `F`.
/// Why:
///   One `match` over the typed policy identity is the whole registry of behavior:
///       the compiler refuses a policy without a check.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// check(policy: PolicyId, trigger: PolicyTrigger): Promise<PolicyOutcome> { switch (policy) { /* ... */ } }
/// ```
impl<F: RepositoryFacts> PolicyChecks for ShippedChecks<F> {
    /// What:
    ///  The outcome of one policy at one lifecycle point.
    /// Why:
    ///   Each arm calls the one check that owns the policy.
    ///
    /// In TS you'd write (pseudocode):
    /// ```ts
    /// check(policy: PolicyId, trigger: PolicyTrigger): PolicyOutcome;
    /// ```
    fn check(&mut self, policy: PolicyId, trigger: Trigger) -> PolicyOutcome {
        // `&mut self.facts` lends the provider for writing; `.as_slice()` lends a list.
        match policy {
            PolicyId::RequireRoot => {
                return check_require_root(&mut self.facts, self.arguments.as_slice(), trigger);
            }
            PolicyId::LinkedWorktreeOnly => {
                return check_linked_worktree(
                    &mut self.facts,
                    self.arguments.as_slice(),
                    self.allowed_worktree_dirs.as_slice(),
                );
            }
            PolicyId::BranchWorktreeOnly => {
                return check_branch_worktree(&mut self.facts, self.arguments.as_slice());
            }
            PolicyId::AddExplicit => return check_add_explicit(self.arguments.as_slice()),
            PolicyId::FinalNewline => {
                return check_final_newline(
                    &mut self.content,
                    &self.candidates,
                    &mut self.facts,
                    trigger,
                    self.can_apply_patches,
                );
            }
            PolicyId::ForbiddenRootContext => {
                return check_root_context(&mut self.content, &self.candidates, &mut self.facts);
            }
            PolicyId::MarkdownAutofix => {
                return check_markdown(
                    &mut self.content,
                    &self.candidates,
                    &mut self.facts,
                    &mut self.markdown,
                    trigger,
                );
            }
            PolicyId::DependentVersionBump => {
                return check_dependent_version(
                    &mut self.content,
                    &self.candidates,
                    &mut self.facts,
                    trigger,
                );
            }
            PolicyId::ForbiddenStrings => {
                return check_forbidden_strings(
                    &mut self.content,
                    &self.candidates,
                    &mut self.facts,
                    &self.scanner_settings,
                    &mut self.scanner,
                );
            }
        }
    }
}

/// Per-policy outcomes and lazy fact use stay out of the release executable.
#[cfg(test)]
#[path = "policy_checks_tests.rs"]
mod tests;