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;