git_policy_cli/dependent_version_lifecycle.rs
1//! What: The dependent-version planner's view of a prepared candidate state: every tracked
2//! file of the private index, its bytes with this invocation's corrections laid over
3//! them, and the `HEAD` commit's copy of a file.
4//! Why: The planner reads workspace manifests, the registry configuration and source files
5//! wherever they are, not only the candidates; the installed wrapper's policy reads
6//! them as tracked files of the candidate state (`trackedFiles`, `bytes`, `headBytes`).
7//! Everything goes through the store the candidates were prepared in, so a direct fix's
8//! later pass reads what its earlier passes corrected.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! // const workspace = lifecycleWorkspace(store, corrected, candidatePaths); await planWorkspaceBumps(workspace);
13//! ```
14
15/// The layer's failure type, whose message is reported as the reason.
16use super::candidate_error::CandidateError;
17/// The modes and object names of index and tree entries.
18use super::candidate_object::{CandidateMode, ObjectId};
19/// The records a derived version is built from, and their change kinds.
20use super::candidate_record::{CandidateChange, CandidateRecord};
21/// The store the candidates were prepared in.
22use super::candidate_store::CandidateStore;
23/// The versions whose entries the store reads bytes for.
24use super::candidate_version::{Candidate, CandidateVersion};
25/// The planner's content seam and its failure.
26use super::dependent_version_content::{
27 ContentUnavailable, TrackedMode, TrackedPath, WorkspaceContent,
28};
29/// The corrected files of this invocation.
30use super::direct_fix_install::InstallChange;
31/// What: `BTreeMap<K, V>` is a map kept sorted by key.
32/// Why: The corrected files are kept that way by the content state.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// // Map<string, InstallChange>
37/// ```
38use std::collections::BTreeMap;
39/// `OsString` is owned operating-system text: the listing's pathspec.
40use std::ffi::OsString;
41/// `Rc<T>` is a shared, read-only handle.
42use std::rc::Rc;
43
44/// The reason an unmerged entry stops the plan.
45const UNMERGED_REASON: &str =
46 "is an unmerged (conflicted) index entry; resolve the conflict and stage the result";
47
48/// The reason a candidate the listing lacks stops the plan.
49const UNLISTED_REASON: &str = "is a candidate that the listing of tracked files does not hold, so the listing cannot be trusted";
50
51/// The reason a file that is not tracked cannot be read.
52const UNTRACKED_REASON: &str = "is not a tracked file of the candidate state";
53
54/// What: The candidate state as the planner reads it. `'a` names how long the borrows of
55/// the store, the corrections and the candidate paths last.
56/// Why: Both listings are read once, when first needed, and kept as versions of the
57/// store's current generation, whose bytes the store reads.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// type LifecycleWorkspace = { store: CandidateStore; corrected: Map<string, InstallChange>; candidates: string[] };
62/// ```
63pub struct LifecycleWorkspace<'a> {
64 /// The store bound to the private index.
65 store: &'a mut CandidateStore,
66 /// The corrected files of this invocation, by pathname.
67 corrected: &'a BTreeMap<Vec<u8>, InstallChange>,
68 /// The paths of the lifecycle's candidates, which the tracked listing must hold.
69 candidates: &'a [Vec<u8>],
70 /// The private index's entries, once listed.
71 index: Option<Rc<CandidateVersion>>,
72 /// The `HEAD` commit's files, once listed.
73 head: Option<Rc<CandidateVersion>>,
74}
75
76/// What: A view over a prepared store. `candidates` are the lifecycle's candidate paths
77/// that are not deletions.
78/// Why: Nothing is listed until the planner asks.
79///
80/// In TS you'd write (pseudocode):
81/// ```ts
82/// function lifecycleWorkspace(store, corrected, candidates): LifecycleWorkspace;
83/// ```
84pub fn lifecycle_workspace<'a>(
85 store: &'a mut CandidateStore,
86 corrected: &'a BTreeMap<Vec<u8>, InstallChange>,
87 candidates: &'a [Vec<u8>],
88) -> LifecycleWorkspace<'a> {
89 return LifecycleWorkspace {
90 store,
91 corrected,
92 candidates,
93 index: None,
94 head: None,
95 };
96}
97
98/// What: The planner's failure for a layer failure, naming a path when one is concerned.
99/// Why: The layer's own message already says what to inspect.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// const unavailable = (path, error) => ({ path, reason: error.message });
104/// ```
105fn unavailable(path: Option<&[u8]>, error: &CandidateError) -> ContentUnavailable {
106 return ContentUnavailable {
107 path: path.map(<[u8]>::to_vec),
108 reason: error.message.clone(),
109 };
110}
111
112/// What: The planner's mode for a candidate mode.
113/// Why: The two enumerations name the same four Git modes.
114///
115/// In TS you'd write (pseudocode):
116/// ```ts
117/// const trackedMode = (mode: CandidateMode) => mode === 'gitlink' ? 'submodule' : mode;
118/// ```
119pub fn tracked_mode(mode: CandidateMode) -> TrackedMode {
120 match mode {
121 CandidateMode::Regular => return TrackedMode::Regular,
122 CandidateMode::Executable => return TrackedMode::Executable,
123 CandidateMode::Symlink => return TrackedMode::Symlink,
124 CandidateMode::Gitlink => return TrackedMode::Submodule,
125 }
126}
127
128/// What: A record for a derived version: the entry's path, mode and object.
129/// Why: A derived version only carries what the store needs to read bytes; the change
130/// kind is not read.
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// const entryRecord = (path, mode, object) => ({ path, mode, change: 'modified', object });
135/// ```
136fn entry_record(path: &[u8], mode: CandidateMode, object: &ObjectId) -> CandidateRecord {
137 return CandidateRecord {
138 path: path.to_vec(),
139 mode,
140 change: CandidateChange::Modified,
141 object: Some(object.clone()),
142 };
143}
144
145/// What: `impl LifecycleWorkspace<'_> { ... }` attaches the two listings and the lookups.
146/// Why: The planner, and a direct fix admitting a file it did not select, read both.
147///
148/// In TS you'd write (pseudocode):
149/// ```ts
150/// class LifecycleWorkspace { index() {} head() {} indexEntry(path) {} headEntry(path) {} }
151/// ```
152impl LifecycleWorkspace<'_> {
153 /// What: The private index's stage-0 entries as a version, listed on first use.
154 /// Why: `:/` selects the whole index from any directory. An unmerged entry has no
155 /// single content, and a listing without one of the candidates is not the
156 /// candidate state, so either refuses the plan.
157 ///
158 /// In TS you'd write (pseudocode):
159 /// ```ts
160 /// async index(): Promise<CandidateVersion>;
161 /// ```
162 fn index(&mut self) -> Result<Rc<CandidateVersion>, ContentUnavailable> {
163 if let Some(listed) = &self.index {
164 return Ok(Rc::clone(listed));
165 }
166 let entries = match self.store.stage_records(&[OsString::from(":/")]) {
167 Ok(found) => found,
168 Err(error) => return Err(unavailable(None, &error)),
169 };
170 let mut records: Vec<CandidateRecord> = Vec::new();
171 for entry in &entries {
172 if entry.stage != 0 {
173 return Err(ContentUnavailable {
174 path: Some(entry.path.clone()),
175 reason: String::from(UNMERGED_REASON),
176 });
177 }
178 records.push(entry_record(&entry.path, entry.mode, &entry.object));
179 }
180 let version: Rc<CandidateVersion> = self.store.derived_version(records);
181 for path in self.candidates {
182 if version.candidate_at_path(path).is_none() {
183 return Err(ContentUnavailable {
184 path: Some(path.clone()),
185 reason: String::from(UNLISTED_REASON),
186 });
187 }
188 }
189 self.index = Some(Rc::clone(&version));
190 return Ok(version);
191 }
192
193 /// What: The `HEAD` commit's files as a version, listed on first use; empty before the
194 /// first commit.
195 /// Why: One listing answers every `HEAD` read of the invocation.
196 ///
197 /// In TS you'd write (pseudocode):
198 /// ```ts
199 /// async head(): Promise<CandidateVersion>;
200 /// ```
201 fn head(&mut self) -> Result<Rc<CandidateVersion>, ContentUnavailable> {
202 if let Some(listed) = &self.head {
203 return Ok(Rc::clone(listed));
204 }
205 let entries = match self.store.head_tree_records() {
206 Ok(found) => found,
207 Err(error) => return Err(unavailable(None, &error)),
208 };
209 let mut records: Vec<CandidateRecord> = Vec::new();
210 for entry in &entries {
211 records.push(entry_record(&entry.path, entry.mode, &entry.object));
212 }
213 let version: Rc<CandidateVersion> = self.store.derived_version(records);
214 self.head = Some(Rc::clone(&version));
215 return Ok(version);
216 }
217
218 /// What: The mode and object of a path's private index entry, if it has one.
219 /// Why: A direct fix compares it with `HEAD` before changing a file it did not select.
220 ///
221 /// In TS you'd write (pseudocode):
222 /// ```ts
223 /// async indexEntry(path): Promise<{ mode; object } | undefined>;
224 /// ```
225 pub fn index_entry(
226 &mut self,
227 path: &[u8],
228 ) -> Result<Option<(CandidateMode, ObjectId)>, ContentUnavailable> {
229 let version: Rc<CandidateVersion> = self.index()?;
230 return Ok(entry_of(&version, path));
231 }
232
233 /// What: The mode and object of a path in `HEAD`, if `HEAD` has it.
234 /// Why: See `index_entry`.
235 ///
236 /// In TS you'd write (pseudocode):
237 /// ```ts
238 /// async headEntry(path): Promise<{ mode; object } | undefined>;
239 /// ```
240 pub fn head_entry(
241 &mut self,
242 path: &[u8],
243 ) -> Result<Option<(CandidateMode, ObjectId)>, ContentUnavailable> {
244 let version: Rc<CandidateVersion> = self.head()?;
245 return Ok(entry_of(&version, path));
246 }
247
248 /// What: The bytes of a path's entry in `version` through the store.
249 /// Why: Index and `HEAD` reads share the one object reader.
250 ///
251 /// In TS you'd write (pseudocode):
252 /// ```ts
253 /// async entryBytes(candidate): Promise<Uint8Array>;
254 /// ```
255 fn entry_bytes(&mut self, candidate: &Candidate) -> Result<Vec<u8>, ContentUnavailable> {
256 match self.store.bytes(candidate) {
257 Ok(read) => return Ok(read.to_vec()),
258 Err(error) => return Err(unavailable(Some(candidate.path.as_slice()), &error)),
259 }
260 }
261}
262
263/// What: The mode and object of the entry at `path` in `version`, if any.
264/// Why: Both lookups answer the same pair.
265///
266/// In TS you'd write (pseudocode):
267/// ```ts
268/// const entryOf = (version, path) => version.at(path) && { mode, object };
269/// ```
270fn entry_of(version: &CandidateVersion, path: &[u8]) -> Option<(CandidateMode, ObjectId)> {
271 let candidate: &Candidate = version.candidate_at_path(path)?;
272 let object: &ObjectId = candidate.object.as_ref()?;
273 return Some((candidate.mode, object.clone()));
274}
275
276/// What: `impl WorkspaceContent for LifecycleWorkspace<'_>` is the planner's content seam.
277/// Why: The planner stays free of Git; this is the only place it meets the store.
278///
279/// In TS you'd write (pseudocode):
280/// ```ts
281/// class LifecycleWorkspace implements WorkspaceContent {}
282/// ```
283impl WorkspaceContent for LifecycleWorkspace<'_> {
284 /// What: Every tracked path of the candidate state with its mode, in index order.
285 /// Why: The planner selects manifests, the configuration and sources itself.
286 ///
287 /// In TS you'd write (pseudocode):
288 /// ```ts
289 /// trackedPaths(): Promise<TrackedPath[]>;
290 /// ```
291 fn tracked_paths(&mut self) -> Result<Vec<TrackedPath>, ContentUnavailable> {
292 let version: Rc<CandidateVersion> = self.index()?;
293 let mut paths: Vec<TrackedPath> = Vec::new();
294 for candidate in version.candidates() {
295 paths.push(TrackedPath {
296 path: candidate.path.clone(),
297 mode: tracked_mode(candidate.mode),
298 });
299 }
300 return Ok(paths);
301 }
302
303 /// What: A tracked file's candidate-state bytes, corrected ones first.
304 /// Why: A later pass of a direct fix reads what the earlier passes made of the file.
305 ///
306 /// In TS you'd write (pseudocode):
307 /// ```ts
308 /// candidateBytes(path): Promise<Uint8Array>;
309 /// ```
310 fn candidate_bytes(&mut self, path: &[u8]) -> Result<Vec<u8>, ContentUnavailable> {
311 if let Some(found) = self.corrected.get(path) {
312 return Ok(found.replacement.to_vec());
313 }
314 let version: Rc<CandidateVersion> = self.index()?;
315 let Some(candidate) = version.candidate_at_path(path) else {
316 return Err(ContentUnavailable {
317 path: Some(path.to_vec()),
318 reason: String::from(UNTRACKED_REASON),
319 });
320 };
321 return self.entry_bytes(candidate);
322 }
323
324 /// What: A file's bytes in `HEAD`, or nothing when `HEAD` lacks it.
325 /// Why: A manifest's version is raised when it differs from its `HEAD` version.
326 ///
327 /// In TS you'd write (pseudocode):
328 /// ```ts
329 /// baseBytes(path): Promise<Uint8Array | undefined>;
330 /// ```
331 fn base_bytes(&mut self, path: &[u8]) -> Result<Option<Vec<u8>>, ContentUnavailable> {
332 let version: Rc<CandidateVersion> = self.head()?;
333 let Some(candidate) = version.candidate_at_path(path) else {
334 return Ok(None);
335 };
336 return Ok(Some(self.entry_bytes(candidate)?));
337 }
338}
339
340/// Listing, overlay and `HEAD` controls against real Git stay out of the release executable.
341#[cfg(test)]
342#[path = "dependent_version_lifecycle_tests.rs"]
343mod tests;