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;