Skip to main content

git_policy_cli/
direct_fix.rs

1//! What:
2//!  `git cli-git fix`:
3//!  run policy passes over the selected files,
4//!  apply the
5//!       corrections they propose in memory until a pass proposes none,
6//!  then install the
7//!       corrected files into the worktree when that last pass is clean.
8//! Why:
9//!  The installed wrapper's `direct-fix.ts` and `direct-fix-convergence.ts` decide the
10//!      behaviour:
11//!  only the last pass is reported,
12//!  a fix summary follows it only when a
13//!      file changed,
14//!  nothing reaches the worktree unless the last pass exits 0,
15//!  and a
16//!      fix that cycles or keeps changing ends with one engine failure.
17//!  The bounded loop
18//!      itself is `policy_convergence::converge`;
19//!  this module supplies its passes.
20//!      Corrections replace whole file contents in memory instead of being staged into
21//!      the private index,
22//!  so a pass starts no Git process for them.
23//!
24//! In TS you'd write (pseudocode):
25//! ```ts
26//! // const result = await runDirectFix({ gitGlobalArgs, pathspecs, policyOptions });
27//! ```
28
29/// Import the engine-failure codes.
30use super::diagnostics::EngineFailureCode;
31/// Import the change list a converged fix installs,
32///  and the installer.
33use super::direct_fix_install::{InstallChange, install_corrections};
34/// Import the event form of a pathname.
35use super::event_path::EventPath;
36/// Import the shipped checks,
37///  whose content state carries the corrections.
38use super::policy_checks::ShippedChecks;
39/// Import one proposed correction.
40use super::policy_content::Correction;
41/// Import the bounded loop and its interface.
42use super::policy_convergence::{
43    Convergence, FIX_PASS_LIMIT_MESSAGE, FixPasses, PassResult as ConvergencePass, converge,
44};
45/// Import the stage request,
46///  how a stage ended and the exit code of a pass.
47use super::policy_engine::{StageEnd, StageRequest, pass_exit_code};
48/// Import the events a fix reports.
49use super::policy_events::PolicyEvent;
50/// Import one policy pass and its result.
51use super::policy_pass::{PassResult, run_policy_pass};
52/// Import the lifecycle point of a fix.
53use super::policy_trigger::Trigger;
54/// Import the facts interface.
55use super::repository_facts::RepositoryFacts;
56/// `BTreeMap<K, V>` is a map kept sorted by key.
57use std::collections::BTreeMap;
58/// `Path` is a borrowed filesystem path.
59use std::path::Path;
60
61/// The message of a fix whose corrections returned to an earlier state,
62///  as the installed
63/// wrapper's direct fix words it.
64pub const DIRECT_FIX_CYCLE_MESSAGE: &str =
65    "Policy patches entered a repeated candidate-state cycle.";
66
67/// The message of a fix that has no worktree to install into.
68pub const NO_WORKTREE_MESSAGE: &str =
69    "cli-git fix could not install its corrections: the repository has no worktree.";
70
71/// What:
72///  The passes of one direct fix,
73///  as the bounded loop sees them.
74///  `'a` names how
75///       long the borrowed request and checks last.
76/// Why:
77///   The loop owns the control flow;
78///  this owns running a pass,
79///  applying its
80///       corrections and remembering each changed state.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// class DirectFixPasses implements FixPasses { last?: PassResult; states: Map<string, InstallChange>[] }
85/// ```
86pub struct DirectFixPasses<'a, F: RepositoryFacts> {
87    /// What every pass evaluates.
88    request: &'a StageRequest,
89    /// The checks,
90    ///  kept across passes so candidates and rules are prepared once.
91    checks: &'a mut ShippedChecks<F>,
92    /// The most recent pass,
93    ///  which is the one reported.
94    last: Option<PassResult>,
95    /// The corrected files of every state:
96    ///  the first is the uncorrected one.
97    states: Vec<BTreeMap<Vec<u8>, InstallChange>>,
98}
99
100/// What:
101///  How the loop reads a pass's ending.
102/// Why:
103///   A proposal asks for another pass;
104///  a pass that could not decide blocks the fix;
105///       a completed or stopped pass proposes nothing,
106///  so the content is stable.
107///
108/// In TS you'd write (pseudocode):
109/// ```ts
110/// const convergencePass = (end) => end === 'proposed' ? 'proposed' : end === 'failed' || isUnavailable(end) ? 'failed' : 'stable';
111/// ```
112pub fn convergence_pass(end: StageEnd) -> ConvergencePass {
113    match end {
114        StageEnd::Proposed => return ConvergencePass::Proposed,
115        StageEnd::Failed | StageEnd::Unavailable(_) => return ConvergencePass::Failed,
116        StageEnd::Completed | StageEnd::Stopped => return ConvergencePass::Stable,
117    }
118}
119
120/// What:
121///  `impl<F: RepositoryFacts> DirectFixPasses<'_, F> { ... }` attaches the constructor
122///       and the hand-back of what the loop left.
123/// Why:
124///   The states start with the uncorrected one,
125///  which the cycle check compares against.
126///
127/// In TS you'd write (pseudocode):
128/// ```ts
129/// class DirectFixPasses { constructor(request, checks) {} }
130/// ```
131impl<'a, F: RepositoryFacts> DirectFixPasses<'a, F> {
132    /// What:
133    ///  Passes over `checks` with no pass run yet and the uncorrected state stored.
134    /// Why:
135    ///   State number `n` is the state after `n` changes,
136    ///  as the loop numbers them.
137    ///
138    /// In TS you'd write (pseudocode):
139    /// ```ts
140    /// constructor(request: StageRequest, checks: ShippedChecks)
141    /// ```
142    pub fn new(request: &'a StageRequest, checks: &'a mut ShippedChecks<F>) -> Self {
143        return DirectFixPasses {
144            request,
145            checks,
146            last: None,
147            states: vec![BTreeMap::new()],
148        };
149    }
150}
151
152/// What:
153///  `impl FixPasses for DirectFixPasses<'_, F>` gives the loop its three operations.
154/// Why:
155///   The loop is tested without Git;
156///  these are the real passes.
157///
158/// In TS you'd write (pseudocode):
159/// ```ts
160/// class DirectFixPasses implements FixPasses {}
161/// ```
162impl<F: RepositoryFacts> FixPasses for DirectFixPasses<'_, F> {
163    /// What:
164    ///  Run one whole policy pass on the current content and remember it.
165    /// Why:
166    ///   Proposals left from an earlier pass were applied already;
167    ///  a pass starts
168    ///       with none,
169    ///  so it is read only from what this pass proposes.
170    ///
171    /// In TS you'd write (pseudocode):
172    /// ```ts
173    /// async runPass(version: number): Promise<'stable' | 'proposed' | 'failed'>;
174    /// ```
175    fn run_pass(&mut self, _candidate_version: usize) -> ConvergencePass {
176        // What: `let _ = ...` drops proposals no `apply` took.
177        // Why:  Only a pass that ends proposing is followed by `apply`; a pass that
178        //       proposed and then failed must not carry its proposals into another.
179        //
180        // In TS you'd write (pseudocode):
181        // ```ts
182        // content.takeProposals();
183        // ```
184        let _ = self.checks.content.take_proposals();
185        let pass: PassResult = run_policy_pass(self.request, self.checks);
186        let result: ConvergencePass = convergence_pass(pass.end);
187        // `Some(x)` is the "present" case of `Option`.
188        self.last = Some(pass);
189        return result;
190    }
191
192    /// What:
193    ///  Apply the last pass's proposals and store the resulting state.
194    /// Why:
195    ///   Applying in memory cannot fail,
196    ///  so the fix is never blocked here.
197    ///
198    /// In TS you'd write (pseudocode):
199    /// ```ts
200    /// async apply(snapshot: number): Promise<boolean>;
201    /// ```
202    fn apply(&mut self, _snapshot: usize) -> bool {
203        let proposals: Vec<Correction> = self.checks.content.take_proposals();
204        self.checks.content.apply(proposals);
205        self.states.push(self.checks.content.corrected_state());
206        return true;
207    }
208
209    /// What:
210    ///  Whether two stored states hold the same bytes in every file.
211    /// Why:
212    ///   Equal corrected maps mean equal content,
213    ///  because a file whose bytes return to
214    ///       its original is no longer in the map.
215    ///
216    /// In TS you'd write (pseudocode):
217    /// ```ts
218    /// async snapshotsEqual(left: number, right: number): Promise<boolean>;
219    /// ```
220    fn snapshots_equal(&mut self, left: usize, right: usize) -> bool {
221        return self.states[left] == self.states[right];
222    }
223}
224
225/// What:
226///  A fix result made of one engine failure of the fix,
227///  as the installed wrapper
228///       reports a cycle,
229///  a pass limit or an installation failure:
230///  earlier events are
231///       dropped.
232///  `&[std::ffi::OsString]` borrows the command's arguments.
233/// Why:
234///   The person sees why nothing was installed,
235///  and nothing else.
236///
237/// In TS you'd write (pseudocode):
238/// ```ts
239/// const fixFailure = (code, message) => ({ events: [engineFailure(code, message, 'direct-fix')], exitCode: 2 });
240/// ```
241pub fn fix_failure(
242    arguments: &[std::ffi::OsString],
243    code: EngineFailureCode,
244    message: &str,
245) -> PassResult {
246    return PassResult {
247        arguments: arguments.to_vec(),
248        events: vec![PolicyEvent::EngineFailure {
249            code,
250            message: String::from(message),
251            trigger: Some(Trigger::DirectFix),
252            // `None`: the fix itself failed, not one policy, and no file is named.
253            policy: None,
254            path: None,
255        }],
256        end: StageEnd::Failed,
257    };
258}
259
260/// What:
261///  The result of a settled fix:
262///  the last pass as it is,
263///  or that pass followed by a
264///       fix summary once its corrections were installed.
265///  `Option<&Path>` is the
266///       worktree's top level or nothing.
267/// Why:
268///   Only a last pass that exits 0 installs anything,
269///  and the summary is reported only
270///       after a file changed.
271///
272/// In TS you'd write (pseudocode):
273/// ```ts
274/// async function settledFix(pass, changedPasses, checks, root): Promise<PassResult>;
275/// ```
276pub fn settled_fix<F: RepositoryFacts>(
277    pass: PassResult,
278    changed_passes: usize,
279    checks: &ShippedChecks<F>,
280    root: Option<&Path>,
281) -> PassResult {
282    if pass_exit_code(pass.events.as_slice(), pass.end) != 0 {
283        return pass;
284    }
285    let changes: Vec<InstallChange> = checks.content.install_changes();
286    if changes.is_empty() {
287        return pass;
288    }
289    let (Some(top_level), Some(real_index)) = (root, checks.content.real_index()) else {
290        return fix_failure(
291            pass.arguments.as_slice(),
292            EngineFailureCode::TransactionFailed,
293            NO_WORKTREE_MESSAGE,
294        );
295    };
296    if let Err(message) = install_corrections(top_level, real_index, changes.as_slice()) {
297        return fix_failure(
298            pass.arguments.as_slice(),
299            EngineFailureCode::TransactionFailed,
300            message.as_str(),
301        );
302    }
303    let mut changed_paths: Vec<EventPath> = Vec::new();
304    for change in &changes {
305        changed_paths.push(EventPath::from_git_bytes(change.path.as_slice()));
306    }
307    let mut events: Vec<PolicyEvent> = pass.events;
308    events.push(PolicyEvent::FixSummary {
309        trigger: Trigger::DirectFix,
310        // `as u64` widens the count to the event's number type.
311        passes: changed_passes as u64,
312        changed_paths,
313    });
314    return PassResult {
315        arguments: pass.arguments,
316        events,
317        end: pass.end,
318    };
319}
320
321/// What:
322///  What a fix reports once the loop ended:
323///  `convergence` says how,
324///  `pass` is the
325///       last pass run.
326/// Why:
327///   A settled fix may install;
328///  a blocked fix reports the pass that could not decide;
329///       a cycle and a pass limit report one engine failure each,
330///  as the installed
331///       wrapper's `fixCycleFailure` and `fixPassLimitFailure` do.
332///
333/// In TS you'd write (pseudocode):
334/// ```ts
335/// async function finishFix(convergence, pass, checks, root): Promise<PassResult>;
336/// ```
337pub fn finish_fix<F: RepositoryFacts>(
338    convergence: Convergence,
339    pass: PassResult,
340    checks: &ShippedChecks<F>,
341    root: Option<&Path>,
342) -> PassResult {
343    match convergence {
344        Convergence::Settled { changed_passes } => {
345            return settled_fix(pass, changed_passes, checks, root);
346        }
347        Convergence::Blocked => return pass,
348        Convergence::FixCycle => {
349            return fix_failure(
350                pass.arguments.as_slice(),
351                EngineFailureCode::FixCycle,
352                DIRECT_FIX_CYCLE_MESSAGE,
353            );
354        }
355        Convergence::FixPassLimit => {
356            return fix_failure(
357                pass.arguments.as_slice(),
358                EngineFailureCode::FixPassLimit,
359                FIX_PASS_LIMIT_MESSAGE,
360            );
361        }
362    }
363}
364
365/// What:
366///  Run a whole direct fix and return what it reports.
367///  `root` is the worktree's top
368///       level,
369///  where corrected files are installed.
370/// Why:
371///   See the module comment.
372///
373/// In TS you'd write (pseudocode):
374/// ```ts
375/// async function runDirectFix(request, checks, root): Promise<PassResult>;
376/// ```
377pub fn run_direct_fix<F: RepositoryFacts>(
378    request: &StageRequest,
379    checks: &mut ShippedChecks<F>,
380    root: Option<&Path>,
381) -> PassResult {
382    let mut passes: DirectFixPasses<'_, F> = DirectFixPasses::new(request, checks);
383    let convergence: Convergence = converge(&mut passes);
384    let Some(pass) = passes.last else {
385        unreachable!("the bounded loop runs at least one pass");
386    };
387    return finish_fix(convergence, pass, checks, root);
388}
389
390/// Pass,
391///  settlement and installation-boundary controls stay out of the release executable.
392#[cfg(test)]
393#[path = "direct_fix_tests.rs"]
394mod tests;