git_policy_cli/dependent_version_states.rs
1//! What: Read every workspace manifest at the candidate state and at the base, and decide
2//! which packages' versions were raised.
3//! Why: The plan starts from these facts. The order of failures follows the incumbent's:
4//! every manifest is decoded before any is parsed (its reader decodes while listing,
5//! `dependent-version-bump-policy.ts:82-92`), and each manifest's base text is parsed
6//! before its current text (`dependent-bump-workflow.ts:150-174`).
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // const states = (await reader.manifests()).map(readManifestState);
11//! ```
12
13/// What: `use` brings names from sibling files into this file.
14/// Why: Manifests are selected by path, read through the content seam, and parsed.
15///
16/// In TS you'd write (pseudocode):
17/// ```ts
18/// import { readManifestFacts, strictText } from './dependent_version_manifest.ts';
19/// ```
20use super::dependent_version_content::{
21 PlanError, PolicyIncomplete, TrackedMode, TrackedPath, WorkspaceContent,
22};
23/// Strict decoding and fact extraction of one manifest.
24use super::dependent_version_manifest::{ManifestFacts, read_manifest_facts, strict_text};
25/// Which tracked paths are manifests, and their package directories.
26use super::dependent_version_paths::{is_workspace_manifest_path, package_directory};
27
28/// What: One workspace manifest with what the plan needs from it.
29/// Why: The patch is built from the exact current bytes; the text after an optional
30/// byte-order mark is what was parsed and what the version edit rewrites.
31///
32/// In TS you'd write (pseudocode):
33/// ```ts
34/// type ManifestState = { file: WorkspaceManifestFile; directory: string; current: ManifestDependencyFacts; baseVersion?: string };
35/// ```
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct ManifestState {
38 /// The manifest path.
39 pub path: Vec<u8>,
40 /// Its mode in the candidate state.
41 pub mode: TrackedMode,
42 /// The package directory.
43 pub directory: Vec<u8>,
44 /// The exact candidate bytes.
45 pub bytes: Vec<u8>,
46 /// The decoded candidate text, without a leading byte-order mark.
47 pub text: String,
48 /// Facts parsed from that text.
49 pub facts: ManifestFacts,
50 /// The version at the base; nothing for a new manifest or one without a version there.
51 pub base_version: Option<Vec<u16>>,
52}
53
54/// What: A manifest's bytes before decoding.
55/// Why: Every manifest is read and decoded before any is parsed.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// type RawManifest = { tracked: TrackedPath; current: Uint8Array; base?: Uint8Array };
60/// ```
61struct RawManifest {
62 /// The tracked manifest.
63 tracked: TrackedPath,
64 /// Candidate bytes.
65 current: Vec<u8>,
66 /// Base bytes, when the base has the manifest.
67 base: Option<Vec<u8>>,
68}
69
70/// What: Read and decode every workspace manifest, then parse each, base first.
71/// `&mut dyn WorkspaceContent` lends any provider for writing; `?` returns a failure
72/// to the caller.
73/// Why: A malformed manifest at either state stops the plan, as in the incumbent.
74///
75/// In TS you'd write (pseudocode):
76/// ```ts
77/// function readManifestStates(content, tracked): ManifestState[];
78/// ```
79pub fn read_manifest_states(
80 content: &mut dyn WorkspaceContent,
81 tracked: &[TrackedPath],
82) -> Result<Vec<ManifestState>, PlanError> {
83 let mut raw: Vec<RawManifest> = Vec::new();
84 for entry in tracked {
85 if is_workspace_manifest_path(&entry.path) {
86 raw.push(RawManifest {
87 tracked: entry.clone(),
88 current: content.candidate_bytes(&entry.path)?,
89 base: content.base_bytes(&entry.path)?,
90 });
91 }
92 }
93 for manifest in &raw {
94 strict_text(&manifest.tracked.path, &manifest.current)?;
95 if let Some(base) = &manifest.base {
96 strict_text(&manifest.tracked.path, base)?;
97 }
98 }
99 let mut states: Vec<ManifestState> = Vec::new();
100 for manifest in raw {
101 states.push(manifest_state(manifest)?);
102 }
103 return Ok(states);
104}
105
106/// What: Parse one decoded manifest at both states.
107/// Why: The base contributes only its version; the current text contributes every fact.
108///
109/// In TS you'd write (pseudocode):
110/// ```ts
111/// function readManifestState(file: WorkspaceManifestFile): ManifestState;
112/// ```
113fn manifest_state(manifest: RawManifest) -> Result<ManifestState, PolicyIncomplete> {
114 let path: &[u8] = &manifest.tracked.path;
115 let mut base_version: Option<Vec<u16>> = None;
116 if let Some(base) = &manifest.base {
117 base_version = read_manifest_facts(path, strict_text(path, base)?)?.version;
118 }
119 let text: String = String::from(strict_text(path, &manifest.current)?);
120 let facts: ManifestFacts = read_manifest_facts(path, &text)?;
121 return Ok(ManifestState {
122 path: path.to_vec(),
123 mode: manifest.tracked.mode,
124 directory: package_directory(path).to_vec(),
125 text,
126 facts,
127 base_version,
128 bytes: manifest.current,
129 });
130}
131
132/// What: The names of packages whose version differs from the base, in manifest order.
133/// Why: A manifest new at this state, or without a version at the base, raised nothing;
134/// a version removed at this state counts as raised, as in the incumbent.
135///
136/// In TS you'd write (pseudocode):
137/// ```ts
138/// states.filter(s => s.baseVersion !== undefined && s.current.version !== s.baseVersion).map(s => s.current.name);
139/// ```
140pub fn bumped_names(states: &[ManifestState]) -> Vec<String> {
141 return states
142 .iter()
143 .filter(|state: &&ManifestState| {
144 return state.base_version.is_some() && state.facts.version != state.base_version;
145 })
146 .map(|state: &ManifestState| return state.facts.name.clone())
147 .collect();
148}
149
150/// What: The first pair of manifests, in order, that declare the same package name.
151/// Why: With two packages of one name, a dependency edge cannot say which it means; the
152/// planner refuses rather than choosing one.
153///
154/// In TS you'd write (pseudocode):
155/// ```ts
156/// function duplicateName(states: ManifestState[]): PolicyIncomplete | undefined;
157/// ```
158pub fn duplicate_name(states: &[ManifestState]) -> Option<PolicyIncomplete> {
159 for (index, later) in states.iter().enumerate() {
160 // `&states[..index]` borrows every state before this one.
161 if let Some(earlier) = states[..index]
162 .iter()
163 .find(|state: &&ManifestState| return state.facts.name == later.facts.name)
164 {
165 return Some(PolicyIncomplete::DuplicateName {
166 name: later.facts.name.clone(),
167 first: earlier.path.clone(),
168 second: later.path.clone(),
169 });
170 }
171 }
172 return None;
173}
174
175/// Manifest reading order, raised-version detection and duplicate names.
176#[cfg(test)]
177#[path = "dependent_version_states_tests.rs"]
178mod tests;