Skip to main content

git_policy_cli/policy/
policy_trigger.rs

1//! What:
2//!  The lifecycle points at which policies run,
3//!  which shipped policy runs at which
4//!       point,
5//!  and which points this executable can serve.
6//! Why:
7//!  A policy runs only for the triggers it declares.
8//!  A trigger whose lifecycle is not
9//!      ported must be answered as "unavailable",
10//!  never as an empty result that would read
11//!      as "checked and clean".
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! // type PolicyTrigger = 'pre-forward' | 'post-commit' | 'manual-push' | 'direct-check' | 'direct-fix';
16//! ```
17
18/// What:
19///  `use` brings a name from a sibling file into this file;
20///  `super::` means "the
21///       parent module",
22///  where every sibling file of this crate is declared.
23/// Why:
24///   The trigger table is keyed by the registry's typed policy identity.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// import { PolicyId } from './policy_registry.ts';
29/// ```
30use crate::policy_registry::PolicyId;
31
32/// What:
33///  One lifecycle point.
34///  An `enum` is a closed set of named alternatives.
35///       `#[derive(...)]` asks the compiler to generate copying (`Clone`,
36///  `Copy`),
37///  debug
38///       printing (`Debug`) and `==` (`Eq`,
39///  `PartialEq`).
40/// Why:
41///   The set is fixed by the event format,
42///  where each event names its trigger.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// type PolicyTrigger = 'pre-forward' | 'post-commit' | 'manual-push' | 'direct-check' | 'direct-fix';
47/// ```
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum Trigger {
50    /// Before a wrapped Git command is forwarded.
51    PreForward,
52    /// After a commit landed,
53    ///  against the landed commit.
54    PostCommit,
55    /// Before a manual `git push`,
56    ///  against newly published content.
57    ManualPush,
58    /// `git cli-git check`.
59    DirectCheck,
60    /// `git cli-git fix`.
61    DirectFix,
62}
63
64/// What:
65///  The wire spelling of a trigger.
66///  `&'static str` is text baked into the program
67///       for its whole run (sibling `String` would be an owned copy).
68/// Why:
69///   Events print exactly the spellings the incumbent emitted.
70///
71/// In TS you'd write (pseudocode):
72/// ```ts
73/// function triggerName(trigger: PolicyTrigger): string { return trigger; }
74/// ```
75pub fn trigger_name(trigger: Trigger) -> &'static str {
76    // `match` picks one arm per variant; the compiler refuses the code if one is forgotten.
77    match trigger {
78        Trigger::PreForward => return "pre-forward",
79        Trigger::PostCommit => return "post-commit",
80        Trigger::ManualPush => return "manual-push",
81        Trigger::DirectCheck => return "direct-check",
82        Trigger::DirectFix => return "direct-fix",
83    }
84}
85
86/// What:
87///  Every trigger.
88///  `&[Trigger]` is a borrowed,
89///  read-only list (siblings:
90///  `Vec<T>`,
91///       owned and growable;
92///  `[T; N]`,
93///  fixed length in the type).
94/// Why:
95///   Two shipped policies check content at every lifecycle point.
96///
97/// In TS you'd write (pseudocode):
98/// ```ts
99/// const EVERY_TRIGGER = ['pre-forward', 'post-commit', 'manual-push', 'direct-check', 'direct-fix'] as const;
100/// ```
101const EVERY_TRIGGER: &[Trigger] = &[
102    Trigger::PreForward,
103    Trigger::PostCommit,
104    Trigger::ManualPush,
105    Trigger::DirectCheck,
106    Trigger::DirectFix,
107];
108
109/// What:
110///  The triggers a shipped policy declares,
111///  copied from the incumbent definitions.
112/// Why:
113///   The engine skips a policy at every other lifecycle point,
114///  exactly as the
115///       incumbent's stage did.
116///
117/// In TS you'd write (pseudocode):
118/// ```ts
119/// function policyTriggers(id: PolicyId): readonly PolicyTrigger[];
120/// ```
121pub fn policy_triggers(id: PolicyId) -> &'static [Trigger] {
122    // `|` joins patterns that share one arm; `&[...]` is a list baked into the program.
123    match id {
124        PolicyId::RequireRoot | PolicyId::ForbiddenRootContext => {
125            return &[Trigger::PreForward, Trigger::DirectCheck];
126        }
127        PolicyId::LinkedWorktreeOnly | PolicyId::BranchWorktreeOnly | PolicyId::AddExplicit => {
128            return &[Trigger::PreForward];
129        }
130        PolicyId::FinalNewline | PolicyId::MarkdownAutofix => return EVERY_TRIGGER,
131        PolicyId::DependentVersionBump => {
132            return &[
133                Trigger::PreForward,
134                Trigger::DirectCheck,
135                Trigger::DirectFix,
136            ];
137        }
138        // Content policies that check what is committed, pushed or named, and fix nothing, run at the
139        // same points: a manifest edited and committed, a landed commit, a push, and `check`.
140        PolicyId::CatalogSpecifiers | PolicyId::ForbiddenStrings => {
141            return &[
142                Trigger::PreForward,
143                Trigger::PostCommit,
144                Trigger::ManualPush,
145                Trigger::DirectCheck,
146            ];
147        }
148    }
149}
150
151/// What:
152///  Whether a policy declares a trigger.
153///  `bool` is true or false.
154/// Why:
155///   The stage asks this once per policy before anything else.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// const runs = policy.triggers.includes(trigger);
160/// ```
161pub fn policy_runs_on(id: PolicyId, trigger: Trigger) -> bool {
162    // `.contains(&trigger)` borrows the trigger for the lookup.
163    return policy_triggers(id).contains(&trigger);
164}
165
166/// What:
167///  Whether this executable implements the lifecycle a trigger belongs to.
168/// Why:
169///  Every lifecycle is ported: the exhaustive match below answers true for each variant, and a new
170///  variant fails to compile until it is answered here.
171///  The engine still checks this before running a lifecycle, so a future unported variant gets a typed
172///  "unavailable" instead of running zero policies and looking clean.
173///
174/// In TS you'd write (pseudocode):
175/// ```ts
176/// const ported = (trigger: Trigger): boolean => true;
177/// ```
178pub fn trigger_is_ported(trigger: Trigger) -> bool {
179    match trigger {
180        Trigger::PreForward
181        | Trigger::PostCommit
182        | Trigger::ManualPush
183        | Trigger::DirectCheck
184        | Trigger::DirectFix => {
185            return true;
186        }
187    }
188}
189
190/// Trigger sets,
191///  spellings and the ported set stay out of the release executable.
192#[cfg(test)]
193#[path = "policy_trigger_tests.rs"]
194mod tests;