Skip to main content

git_policy_cli/
policy_pass.rs

1//! What:
2//!  One complete policy pass over the shipped policies:
3//!  the built-in policies,
4//!  then
5//!       the fixed argument transforms of a forwarded command,
6//!  then the optional policies.
7//! Why:
8//!  Every lifecycle runs the same three stages in the same order,
9//!  and each stage can
10//!      end the pass.
11//!  Keeping the sequence in one place means a wrapped command and a
12//!      direct `git cli-git check` cannot drift apart in what they run or when they stop.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! // const result = await runPolicyEngine({ args, trigger, config, selectedPolicyIds });
17//! ```
18
19/// What:
20///  `use` brings names from sibling files into this file;
21///  `super::` means "the parent
22///       module",
23///  where every sibling file of this crate is declared.
24/// Why:
25///   The pass joins the stage runner,
26///  the shipped checks and the fixed transforms.
27///
28/// In TS you'd write (pseudocode):
29/// ```ts
30/// import { runPolicyStage } from './policy_engine.ts';
31/// ```
32use super::policy_checks::ShippedChecks;
33use super::policy_engine::{StageEnd, StageRequest, StageResult, run_policy_stage};
34use super::policy_events::PolicyEvent;
35use super::policy_registry::{POLICY_REGISTRY, PolicyId};
36use super::policy_transforms::{TransformResult, apply_fixed_transforms};
37use super::policy_trigger::Trigger;
38use super::repository_facts::RepositoryFacts;
39/// What:
40///  `OsString` is owned operating-system text of raw bytes.
41///  Sibling the reader might
42///       expect:
43///  `String`,
44///  which must be valid UTF-8.
45/// Why:
46///   The pass hands back the arguments Git will receive,
47///  byte for byte.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// // string, but byte-preserving.
52/// ```
53use std::ffi::OsString;
54
55/// What:
56///  What one pass produced.
57///  A `struct` is a record with named fields;
58///  `Vec<T>` is an
59///       owned list.
60///  `#[derive(...)]` asks the compiler to generate copying,
61///  debug
62///       printing and `==`.
63/// Why:
64///   The caller needs the arguments to forward,
65///  every event in emission order,
66///  and
67///       how the pass ended.
68///
69/// In TS you'd write (pseudocode):
70/// ```ts
71/// type PassResult = { args: string[]; events: PolicyEvent[]; end: StageEnd };
72/// ```
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct PassResult {
75    /// The arguments after the fixed transforms;
76    ///  the input arguments when the pass ended
77    /// before or inside them.
78    pub arguments: Vec<OsString>,
79    /// Every event of the pass,
80    ///  in emission order.
81    pub events: Vec<PolicyEvent>,
82    /// How the pass ended.
83    pub end: StageEnd,
84}
85
86/// What:
87///  The shipped policies of one kind,
88///  in registry order.
89///  `optional` selects the
90///       policies that are off unless the configuration lists them;
91///  `Vec<PolicyId>` is the
92///       owned result.
93/// Why:
94///   The registry is the only list of policies;
95///  the two stages are its two halves,
96///  so
97///       a policy added there lands in exactly one stage without a second list to update.
98///
99/// In TS you'd write (pseudocode):
100/// ```ts
101/// const policiesOfKind = (optional: boolean) => POLICY_REGISTRY.filter(p => p.offUnlessListed === optional).map(p => p.id);
102/// ```
103pub fn policies_of_kind(optional: bool) -> Vec<PolicyId> {
104    // `Vec::<PolicyId>::new()` is an empty owned list; `mut` allows pushing.
105    let mut policies: Vec<PolicyId> = Vec::<PolicyId>::new();
106    // `for descriptor in POLICY_REGISTRY` borrows each compiled-in row in order.
107    for descriptor in POLICY_REGISTRY {
108        if descriptor.off_unless_listed == optional {
109            policies.push(descriptor.id);
110        }
111    }
112    return policies;
113}
114
115/// What:
116///  Run one pass for `request` over `checks`.
117///  `<F: RepositoryFacts>` says the function
118///       works with any one facts provider `F`;
119///  `&mut ShippedChecks<F>` lends the shipped
120///       policies and their provider for writing,
121///  because checks cache repository facts.
122/// Why:
123///   The order is the installed wrapper's.
124///  Built-in policies read the command before
125///       it is rewritten.
126///  The fixed transforms run only for a forwarded command.
127///  Optional
128///       policies run last.
129///  A stage that did not complete ends the pass with the
130///       arguments it was given,
131///  and nothing after it runs.
132///
133/// In TS you'd write (pseudocode):
134/// ```ts
135/// async function runPolicyPass(request: StageRequest, checks: ShippedChecks): Promise<PassResult>;
136/// ```
137pub fn run_policy_pass<F: RepositoryFacts>(
138    request: &StageRequest,
139    checks: &mut ShippedChecks<F>,
140) -> PassResult {
141    // `.as_slice()` lends an owned list as a borrowed view.
142    let built_in: StageResult =
143        run_policy_stage(request, policies_of_kind(false).as_slice(), checks);
144    // `mut` allows the later stages to append their events.
145    let mut events: Vec<PolicyEvent> = built_in.events;
146    if built_in.end != StageEnd::Completed {
147        return PassResult {
148            // `.clone()` copies the untouched input arguments for the caller.
149            arguments: checks.arguments.clone(),
150            events,
151            end: built_in.end,
152        };
153    }
154    // `mut` allows the transforms to replace the arguments.
155    let mut arguments: Vec<OsString> = checks.arguments.clone();
156    if request.trigger == Trigger::PreForward {
157        // `&mut checks.facts` lends the provider for writing: a transform may ask Git one fact.
158        let transformed: TransformResult = apply_fixed_transforms(
159            arguments.as_slice(),
160            request.controls.commit_only_escaped,
161            &mut checks.facts,
162        );
163        // A complete transform stage reports nothing but rejections, so any event of a
164        // complete stage is one; an incomplete stage is handled first.
165        let rejected: bool = !transformed.events.is_empty();
166        // `.extend(list)` moves every item of `list` onto the end.
167        events.extend(transformed.events);
168        if !transformed.complete {
169            return PassResult {
170                arguments,
171                events,
172                end: StageEnd::Failed,
173            };
174        }
175        if rejected && !request.controls.keep_going {
176            return PassResult {
177                arguments,
178                events,
179                end: StageEnd::Stopped,
180            };
181        }
182        arguments = transformed.arguments;
183    }
184    let optional: StageResult =
185        run_policy_stage(request, policies_of_kind(true).as_slice(), checks);
186    events.extend(optional.events);
187    return PassResult {
188        arguments,
189        events,
190        end: optional.end,
191    };
192}
193
194/// Stage order,
195///  stopping and argument hand-over stay out of the release executable.
196#[cfg(test)]
197#[path = "policy_pass_tests.rs"]
198mod tests;