Skip to main content

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;