Skip to main content

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;