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;