Skip to main content

git_policy_cli/
dependent_version_policy.rs

1//! What: The decision of `mono/dependent-version-bump`: when to plan, and the findings a
2//!       plan becomes.
3//! Why: The plan is a pre-commit step. Another forwarded command such as `git add` could
4//!      not apply its patches, so it plans only for `git commit` among forwarded commands,
5//!      and for `git cli-git check` and `git cli-git fix`; and only when a workspace manifest
6//!      is modified, so ordinary commits read nothing. Each stale dependent is one finding
7//!      whose patch rewrites a tracked manifest that may not be a candidate, which adds that
8//!      manifest to the commit (`dependent-version-bump-policy.ts:178-235`).
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! // await findDependentBumps(context);
13//! ```
14
15/// What: `use` brings names from sibling files into this file.
16/// Why:  The decision reads the trigger, candidate paths and the plan.
17///
18/// In TS you'd write (pseudocode):
19/// ```ts
20/// import { planWorkspaceBumps } from './dependent_version_plan.ts';
21/// ```
22use super::dependent_version_content::{PlanError, TrackedMode, WorkspaceContent};
23/// Which candidate paths are workspace manifests.
24use super::dependent_version_paths::is_workspace_manifest_path;
25/// The plan the findings come from.
26use super::dependent_version_plan::{ManifestBump, PlanOutcome, plan_workspace_bumps};
27/// The incumbent's message for a version that cannot be bumped.
28use super::dependent_version_release::unsupported_message;
29/// The lifecycle point the policy runs at.
30use super::policy_trigger::Trigger;
31
32/// What: The finding code of a dependent whose version must move with a raised dependency.
33/// Why:  Callers and transcripts identify the finding by this stable code.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// export const DEPENDENT_VERSION_STALE_CODE = 'dependent-version-stale';
38/// ```
39pub const DEPENDENT_VERSION_STALE_CODE: &str = "dependent-version-stale";
40
41/// What: The finding code of a dependent whose version cannot be bumped automatically.
42/// Why:  It carries no patch; the person bumps that package by hand.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// export const DEPENDENT_VERSION_UNSUPPORTED_CODE = 'dependent-version-unsupported';
47/// ```
48pub const DEPENDENT_VERSION_UNSUPPORTED_CODE: &str = "dependent-version-unsupported";
49
50/// What: How a candidate path changed against the base.
51/// Why:  Only a modified manifest can hold a raised version; a new one has no base version.
52///
53/// In TS you'd write (pseudocode):
54/// ```ts
55/// type CandidateChange = 'added' | 'modified' | 'deleted';
56/// ```
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum CandidateChange {
59    /// New at this state.
60    Added,
61    /// Present at both states with different content or mode.
62    Modified,
63    /// Removed at this state.
64    Deleted,
65}
66
67/// What: One candidate path and how it changed.
68/// Why:  The decision to plan looks at candidate paths only.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// type Candidate = { path: string; change: CandidateChange };
73/// ```
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct Candidate {
76    /// Repository-relative path.
77    pub path: Vec<u8>,
78    /// Its change.
79    pub change: CandidateChange,
80}
81
82/// What: What the lifecycle tells the policy. `&'request [Candidate]` borrows the
83///       candidate list for as long as the request lives.
84/// Why:  `forwards_commit` is whether the forwarded subcommand is `commit`; it matters only
85///       for the pre-forward trigger.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// type DependentRequest = { trigger: PolicyTrigger; forwardsCommit: boolean; candidates: Candidate[] };
90/// ```
91#[derive(Clone, Copy, Debug)]
92pub struct DependentRequest<'request> {
93    /// The lifecycle point.
94    pub trigger: Trigger,
95    /// Whether the forwarded command is `git commit`.
96    pub forwards_commit: bool,
97    /// The candidates of this lifecycle.
98    pub candidates: &'request [Candidate],
99}
100
101/// What: One finding of this policy. `Option<ManifestBump>` is the patch, or nothing.
102/// Why:  The wiring turns `patch` into a full-content patch of the tracked manifest at
103///       `path`.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// type DependentFinding = { code: string; message: string; path?: string; patch?: ManifestBump };
108/// ```
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct DependentFinding {
111    /// `dependent-version-stale` or `dependent-version-unsupported`.
112    pub code: &'static str,
113    /// The explanation for the person who ran the command.
114    pub message: String,
115    /// The manifest the finding is about, when it names one.
116    pub path: Option<Vec<u8>>,
117    /// The bump that settles it, when it can be automatic.
118    pub patch: Option<ManifestBump>,
119}
120
121/// What: Whether this lifecycle should plan at all.
122/// Why:  A forwarded command other than `commit` cannot apply the patches; a change that
123///       modifies no workspace manifest cannot raise a version.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// function shouldPlan(request: DependentRequest): boolean;
128/// ```
129pub fn should_plan(request: &DependentRequest) -> bool {
130    if request.trigger == Trigger::PreForward && !request.forwards_commit {
131        return false;
132    }
133    return request.candidates.iter().any(|candidate: &Candidate| {
134        return candidate.change == CandidateChange::Modified
135            && is_workspace_manifest_path(&candidate.path);
136    });
137}
138
139/// What: The stale finding for one planned bump.
140/// Why:  The message names the dependent, the raised packages and both versions.
141///
142/// In TS you'd write (pseudocode):
143/// ```ts
144/// `${bump.name} reaches a package bumped in this commit (${bumpedNames.join(', ')}); bump it from ${bump.from} to ${bump.to} in the same commit.`
145/// ```
146fn stale_finding(bump: &ManifestBump, bumped_names: &[String]) -> DependentFinding {
147    return DependentFinding {
148        code: DEPENDENT_VERSION_STALE_CODE,
149        message: format!(
150            "{} reaches a package bumped in this commit ({}); bump it from {} to {} in the same commit.",
151            bump.planned.name,
152            bumped_names.join(", "),
153            bump.planned.bump.from,
154            bump.planned.bump.to
155        ),
156        path: Some(bump.path.clone()),
157        patch: Some(bump.clone()),
158    };
159}
160
161/// What: The findings of this policy for one lifecycle point.
162/// Why:  A bump of a manifest that is not an ordinary file cannot be patched and is
163///       dropped, as in the incumbent.
164///
165/// In TS you'd write (pseudocode):
166/// ```ts
167/// async function findDependentBumps(context: PolicyContext): Promise<PolicyFinding[]>;
168/// ```
169pub fn find_dependent_bumps(
170    request: &DependentRequest,
171    content: &mut dyn WorkspaceContent,
172) -> Result<Vec<DependentFinding>, PlanError> {
173    if !should_plan(request) {
174        return Ok(Vec::new());
175    }
176    match plan_workspace_bumps(content)? {
177        PlanOutcome::Unsupported(unsupported) => {
178            return Ok(vec![DependentFinding {
179                code: DEPENDENT_VERSION_UNSUPPORTED_CODE,
180                message: unsupported_message(&unsupported),
181                path: None,
182                patch: None,
183            }]);
184        }
185        PlanOutcome::Planned(plan) => {
186            return Ok(plan
187                .bumps
188                .iter()
189                .filter(|bump: &&ManifestBump| {
190                    return matches!(bump.mode, TrackedMode::Regular | TrackedMode::Executable);
191                })
192                .map(|bump: &ManifestBump| return stale_finding(bump, &plan.bumped_names))
193                .collect());
194        }
195    }
196}
197
198/// The trigger rule, the candidate rule and the findings, including the incumbent's policy
199/// tests.
200#[cfg(test)]
201#[path = "dependent_version_policy_tests.rs"]
202mod tests;