Skip to main content

git_policy_cli/
dependent_version_content.rs

1//! What: The repository content the dependent-version planner reads, behind one small
2//!       interface, and the two ways planning can fail to finish.
3//! Why: The planner is pure with respect to the repository: it never starts Git and never
4//!      reads the filesystem. The policy's wiring answers from the wrapper's candidate
5//!      layer, the release path from the worktree, and tests from memory. Failures are
6//!      reported by cause: content that could not be read is `content-unavailable`;
7//!      content that was read but that the planner cannot use is `policy-incomplete`.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! // interface WorkspaceContent { trackedPaths(): TrackedPath[]; candidateBytes(path): Uint8Array; baseBytes(path): Uint8Array | undefined }
12//! ```
13
14/// What: The Git file mode of a tracked path. An `enum` is a closed set of named
15///       alternatives; `#[derive(...)]` generates copying, debug printing and `==`.
16/// Why:  Only ordinary files can receive a full-content patch, so the policy drops a
17///       bump whose manifest is a symbolic link or a submodule, as the incumbent does
18///       (`dependent-version-bump-policy.ts:209`).
19///
20/// In TS you'd write (pseudocode):
21/// ```ts
22/// type TrackedMode = 'regular' | 'executable' | 'symlink' | 'submodule';
23/// ```
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum TrackedMode {
26    /// Mode `100644`.
27    Regular,
28    /// Mode `100755`.
29    Executable,
30    /// Mode `120000`; its bytes are the link target.
31    Symlink,
32    /// Mode `160000`, a gitlink.
33    Submodule,
34}
35
36/// What: One tracked path in the candidate state. `Vec<u8>` is an owned list of bytes;
37///       Git paths are bytes, not necessarily UTF-8.
38/// Why:  The planner selects manifests, the registry configuration and source files from
39///       this list itself, so the provider needs no knowledge of what the planner reads.
40///
41/// In TS you'd write (pseudocode):
42/// ```ts
43/// type TrackedPath = { path: Uint8Array; mode: TrackedMode };
44/// ```
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct TrackedPath {
47    /// Repository-relative path, `/`-separated.
48    pub path: Vec<u8>,
49    /// Its mode in the candidate state.
50    pub mode: TrackedMode,
51}
52
53/// What: Something the provider could not read. `Option<Vec<u8>>` is "a path, or
54///       nothing" (nothing when the tracked-path listing itself failed); `String` is
55///       owned UTF-8 text.
56/// Why:  The engine reports this as `content-unavailable`: the policy is intact, its
57///       input is missing.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// type ContentUnavailable = { path?: Uint8Array; reason: string };
62/// ```
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct ContentUnavailable {
65    /// The path whose bytes could not be read, or nothing for the listing.
66    pub path: Option<Vec<u8>>,
67    /// The provider's explanation.
68    pub reason: String,
69}
70
71/// What: The repository facts the planner reads. A `trait` is a named set of methods a
72///       type promises to provide, like a TS `interface`; `&mut self` lends the provider
73///       for writing, because it may remember what it read; `&[u8]` borrows a path.
74/// Why:  The planner asks only for what it needs: every manifest at both states, the
75///       registry configuration once a version was raised, and source files of the
76///       dependents whose bundled imports decide an edge.
77///
78/// In TS you'd write (pseudocode):
79/// ```ts
80/// interface WorkspaceContent { trackedPaths(): TrackedPath[]; candidateBytes(path: Uint8Array): Uint8Array; baseBytes(path: Uint8Array): Uint8Array | undefined }
81/// ```
82pub trait WorkspaceContent {
83    /// Every tracked path in the candidate state, in the provider's order (Git's index
84    /// order in the wrapper).
85    fn tracked_paths(&mut self) -> Result<Vec<TrackedPath>, ContentUnavailable>;
86    /// The bytes of a tracked path in the candidate state.
87    fn candidate_bytes(&mut self, path: &[u8]) -> Result<Vec<u8>, ContentUnavailable>;
88    /// The bytes of a path at the comparison base (`HEAD` for the policy), or nothing when
89    /// the base lacks it.
90    fn base_bytes(&mut self, path: &[u8]) -> Result<Option<Vec<u8>>, ContentUnavailable>;
91}
92
93/// What: Why the planner's own machinery could not finish although its input was read.
94/// Why:  The engine reports each of these as `policy-incomplete`. `ManifestShape` carries
95///       the incumbent's `ManifestShapeError` text exactly, so both implementations can
96///       be compared message for message.
97///
98/// In TS you'd write (pseudocode):
99/// ```ts
100/// type PolicyIncomplete = { kind: 'not-utf8'; path } | { kind: 'manifest-syntax'; path; detail } | { kind: 'manifest-shape'; path; problem } | { kind: 'duplicate-name'; name; first; second };
101/// ```
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub enum PolicyIncomplete {
104    /// A manifest or the registry configuration is not valid UTF-8.
105    NotUtf8 {
106        /// The file that failed to decode.
107        path: Vec<u8>,
108    },
109    /// A manifest is not JSON.
110    ManifestSyntax {
111        /// The manifest.
112        path: Vec<u8>,
113        /// The parser's explanation.
114        detail: String,
115    },
116    /// A manifest is JSON but lacks a fact the plan needs, or its version cannot be
117    /// rewritten in place.
118    ManifestShape {
119        /// The manifest.
120        path: Vec<u8>,
121        /// The incumbent's message, which names the manifest.
122        problem: String,
123    },
124    /// Two workspace manifests declare the same package name, so the dependency graph is
125    /// ambiguous.
126    DuplicateName {
127        /// The shared name.
128        name: String,
129        /// The earlier manifest in tracked order.
130        first: Vec<u8>,
131        /// The later manifest.
132        second: Vec<u8>,
133    },
134}
135
136/// What: Any failure to finish planning. `From` conversions below let `?` lift either
137///       cause into this type.
138/// Why:  The wiring maps the two variants to the two engine failure codes.
139///
140/// In TS you'd write (pseudocode):
141/// ```ts
142/// type PlanError = { kind: 'content-unavailable'; error: ContentUnavailable } | { kind: 'policy-incomplete'; error: PolicyIncomplete };
143/// ```
144#[derive(Clone, Debug, Eq, PartialEq)]
145pub enum PlanError {
146    /// Reported as `content-unavailable`.
147    ContentUnavailable(
148        /// What could not be read.
149        ContentUnavailable,
150    ),
151    /// Reported as `policy-incomplete`.
152    PolicyIncomplete(
153        /// What the planner could not use.
154        PolicyIncomplete,
155    ),
156}
157
158/// What: Lift an unreadable-content failure into a planning failure. `impl Trait for Type`
159///       gives a type the methods a trait requires.
160/// Why:  Lets the planner write `content.candidate_bytes(path)?`.
161///
162/// In TS you'd write (pseudocode):
163/// ```ts
164/// const failure: PlanError = { kind: 'content-unavailable', error };
165/// ```
166impl From<ContentUnavailable> for PlanError {
167    /// Wrap the unreadable-content failure.
168    fn from(error: ContentUnavailable) -> PlanError {
169        return PlanError::ContentUnavailable(error);
170    }
171}
172
173/// What: Lift a machinery failure into a planning failure.
174/// Why:  Lets the planner write `read_manifest_facts(...)?`.
175///
176/// In TS you'd write (pseudocode):
177/// ```ts
178/// const failure: PlanError = { kind: 'policy-incomplete', error };
179/// ```
180impl From<PolicyIncomplete> for PlanError {
181    /// Wrap the machinery failure.
182    fn from(error: PolicyIncomplete) -> PlanError {
183        return PlanError::PolicyIncomplete(error);
184    }
185}
186
187/// What: Show a repository path as text. `String::from_utf8_lossy` replaces bytes that are
188///       not UTF-8 with U+FFFD; `.into_owned()` makes the result an owned `String`.
189/// Why:  Messages and finding paths are text; Git paths are bytes.
190///
191/// In TS you'd write (pseudocode):
192/// ```ts
193/// const shown = new TextDecoder().decode(path);
194/// ```
195pub fn display_path(path: &[u8]) -> String {
196    return String::from_utf8_lossy(path).into_owned();
197}
198
199/// The escape hatch named in every remedy.
200const HATCH: &str = "--no-enforce-mono/dependent-version-bump";
201
202/// What: The complete diagnostic for a planning failure, naming the input and every way
203///       forward.
204/// Why:  The person who ran the command must be able to act without reading the source.
205///
206/// In TS you'd write (pseudocode):
207/// ```ts
208/// function planErrorMessage(error: PlanError): string;
209/// ```
210pub fn plan_error_message(error: &PlanError) -> String {
211    match error {
212        PlanError::ContentUnavailable(unavailable) => {
213            // `.as_deref()` borrows the optional path; `map_or` picks a default for none.
214            let subject: String = unavailable.path.as_deref().map_or_else(
215                || return String::from("the list of tracked files"),
216                |path: &[u8]| return display_path(path),
217            );
218            return format!(
219                "mono/dependent-version-bump could not read {subject}: {}. \
220                 Nothing was decided; retry once the content is readable, \
221                 or pass {HATCH} to skip this policy for one command.",
222                unavailable.reason
223            );
224        }
225        PlanError::PolicyIncomplete(incomplete) => {
226            return format!(
227                "mono/dependent-version-bump could not finish: {}. \
228                 Correct the file and retry, or pass {HATCH} to skip this policy for one command.",
229                incomplete_reason(incomplete)
230            );
231        }
232    }
233}
234
235/// What: The cause part of a `policy-incomplete` diagnostic.
236/// Why:  Each cause names the file and what is wrong with it.
237///
238/// In TS you'd write (pseudocode):
239/// ```ts
240/// function incompleteReason(error: PolicyIncomplete): string;
241/// ```
242fn incomplete_reason(incomplete: &PolicyIncomplete) -> String {
243    match incomplete {
244        PolicyIncomplete::NotUtf8 { path } => {
245            return format!("{} is not valid UTF-8", display_path(path));
246        }
247        PolicyIncomplete::ManifestSyntax { path, detail } => {
248            return format!("{} is not JSON ({detail})", display_path(path));
249        }
250        PolicyIncomplete::ManifestShape { problem, .. } => return problem.clone(),
251        PolicyIncomplete::DuplicateName {
252            name,
253            first,
254            second,
255        } => {
256            return format!(
257                "{} and {} both declare the package name {name:?}, so which one a dependency \
258                 names is ambiguous",
259                display_path(first),
260                display_path(second)
261            );
262        }
263    }
264}
265
266/// Error conversions and diagnostics.
267#[cfg(test)]
268#[path = "dependent_version_content_tests.rs"]
269mod tests;