Skip to main content

git_policy_cli/
candidate_version.rs

1//! What:
2//!  An immutable candidate version:
3//!  the paths one Git state changes,
4//!  each with its identity,
5//!  mode and object.
6//! Why:
7//!  Every policy of one pass must judge the same files.
8//!  A version is listed once and
9//!      then only read,
10//!  so path metadata is shared instead of re-queried,
11//!  and a later
12//!      fix or replay produces a new version instead of altering this one.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! type CandidateVersion = { readonly candidates: readonly Candidate[] };
17//! ```
18
19/// Import the validated object name and the candidate modes.
20use super::candidate_object::{CandidateMode, ObjectId};
21/// Import the parsed listing records and their change kinds.
22use super::candidate_record::{CandidateChange, CandidateRecord};
23/// What:
24///  `HashMap<K, V>` is a key-to-value table (siblings:
25///  `BTreeMap`,
26///  which keeps keys
27///       sorted,
28///  and `Vec` of pairs).
29/// Why:
30///   Candidates are found by pathname without scanning the whole list.
31///
32/// In TS you'd write (pseudocode):
33/// ```ts
34/// const byPath = new Map<string, number>();
35/// ```
36use std::collections::HashMap;
37
38/// What:
39///  Which Git state a version lists,
40///  and what it is compared with.
41///       `#[derive(...)]` generates cloning,
42///  debug printing and `==`.
43/// Why:
44///   A commit being prepared is judged by what is staged;
45///  a landed commit by what
46///       it changed.
47///  Naming the comparison makes the version reproducible.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// type CandidateSource = { kind: 'staged-against-head' } | { kind: 'staged-against-commit'; commit: ObjectId } | { kind: 'committed'; commit: ObjectId };
52/// ```
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum CandidateSource {
55    /// The index compared with `HEAD`;
56    ///  every index entry when no commit exists yet.
57    StagedAgainstHead,
58    /// The index compared with one named commit,
59    ///  such as a transaction's recorded base.
60    StagedAgainstCommit(ObjectId),
61    /// One commit compared with each of its parents;
62    ///  every entry of a root commit.
63    Committed(ObjectId),
64}
65
66/// What:
67///  The identity of one candidate within one invocation.
68///       `u64` is an unsigned 64-bit counter (siblings `u32`,
69///  `usize`);
70///  `usize` is the
71///       unsigned integer every index uses.
72/// Why:
73///   Identity must not be derived from the pathname,
74///  which the scanner may mask.
75///       The generation tells a candidate of an invalidated version from a current
76///       one;
77///  `u64` cannot run out within a process.
78///  The index is the candidate's
79///       position in its version,
80///  and `usize` is what indexing and the scanner take.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// type CandidateIdentity = { generation: bigint; index: number };
85/// ```
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub struct CandidateIdentity {
88    /// Counts invalidations:
89    ///  candidates of an earlier generation are stale.
90    pub generation: u64,
91    /// Zero-based position in the version's candidate list.
92    pub index: usize,
93}
94
95/// What:
96///  One changed path of a version.
97///       `Vec<u8>` is an owned byte list (sibling `String` would require UTF-8).
98///       `Option<ObjectId>` is "an object name or nothing".
99/// Why:
100///   Git pathnames are arbitrary bytes on Unix and are kept exactly.
101///  A deleted
102///       path has no object,
103///  which the type states.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// type Candidate = { identity: CandidateIdentity; path: Buffer; mode: CandidateMode; change: CandidateChange; object?: ObjectId };
108/// ```
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct Candidate {
111    /// Invocation-local identity,
112    ///  independent of the pathname.
113    pub identity: CandidateIdentity,
114    /// Repository-relative pathname with `/` separators,
115    ///  exactly as Git stores it.
116    pub path: Vec<u8>,
117    /// Mode in the candidate state,
118    ///  or the baseline's mode for a deleted path.
119    pub mode: CandidateMode,
120    /// Change against the baseline.
121    pub change: CandidateChange,
122    /// Object in the candidate state;
123    ///  absent for a deleted path.
124    pub object: Option<ObjectId>,
125}
126
127/// What:
128///  The candidates of one listed state.
129///  Fields are private:
130///  a version is built
131///       once by `build_version` and never changed.
132/// Why:
133///   Immutability is what lets several policies share one listing safely.
134///
135/// In TS you'd write (pseudocode):
136/// ```ts
137/// class CandidateVersion { readonly #candidates: readonly Candidate[]; readonly #byPath: ReadonlyMap<string, number> }
138/// ```
139#[derive(Debug)]
140pub struct CandidateVersion {
141    /// Candidates in listing order.
142    candidates: Vec<Candidate>,
143    /// Position of each pathname's candidate.
144    by_path: HashMap<Vec<u8>, usize>,
145}
146
147/// What:
148///  Build a version from listing records,
149///  keeping the first record of each pathname.
150///       `Vec<CandidateRecord>` is taken by value:
151///  the records' bytes move into the
152///       version without being copied.
153/// Why:
154///   A merge commit lists one comparison per parent,
155///  so a pathname can appear more
156///       than once;
157///  the first appearance decides,
158///  as it did in the TypeScript wrapper.
159///
160/// In TS you'd write (pseudocode):
161/// ```ts
162/// function buildVersion(generation: bigint, records: CandidateRecord[]): CandidateVersion;
163/// ```
164pub fn build_version(generation: u64, records: Vec<CandidateRecord>) -> CandidateVersion {
165    // `Vec::new()` and `HashMap::new()` are empty containers; `mut` lets the loop fill them.
166    let mut candidates: Vec<Candidate> = Vec::new();
167    let mut by_path: HashMap<Vec<u8>, usize> = HashMap::new();
168    for record in records {
169        // `.contains_key(&record.path)` lends the pathname for the lookup.
170        if by_path.contains_key(&record.path) {
171            continue;
172        }
173        let index: usize = candidates.len();
174        // `.clone()` copies the pathname so the table owns its key.
175        by_path.insert(record.path.clone(), index);
176        candidates.push(Candidate {
177            identity: CandidateIdentity { generation, index },
178            path: record.path,
179            mode: record.mode,
180            change: record.change,
181            object: record.object,
182        });
183    }
184    return CandidateVersion {
185        candidates,
186        by_path,
187    };
188}
189
190/// What:
191///  `impl CandidateVersion { ... }` attaches read-only accessors,
192///  like class getters.
193/// Why:
194///   Callers can read a version and cannot change it.
195///
196/// In TS you'd write (pseudocode):
197/// ```ts
198/// class CandidateVersion { get candidates() {} candidateAtPath(path) {} }
199/// ```
200impl CandidateVersion {
201    /// What:
202    ///  Every candidate,
203    ///  in listing order.
204    ///  `&[Candidate]` borrows the list read-only.
205    /// Why:
206    ///   A candidate's `identity.index` is its position in this list.
207    ///
208    /// In TS you'd write (pseudocode):
209    /// ```ts
210    /// get candidates(): readonly Candidate[]
211    /// ```
212    pub fn candidates(&self) -> &[Candidate] {
213        return self.candidates.as_slice();
214    }
215
216    /// What:
217    ///  The candidate at an exact pathname,
218    ///  if this version changes it.
219    ///       `Option<&Candidate>` is "a borrowed candidate or nothing".
220    /// Why:
221    ///   A policy that cares about specific files (a manifest,
222    ///  a rules file) asks by
223    ///       name instead of scanning the list or asking Git again.
224    ///
225    /// In TS you'd write (pseudocode):
226    /// ```ts
227    /// candidateAtPath(path: Buffer): Candidate | undefined
228    /// ```
229    pub fn candidate_at_path(&self, path: &[u8]) -> Option<&Candidate> {
230        // What: `.get(path)` is `Option<&usize>`; a trailing `?` returns `None` when absent;
231        //       `*` reads the position through the borrow.
232        // Why:  The table stores positions, so the candidate itself is fetched from the list.
233        //
234        // In TS you'd write (pseudocode):
235        // ```ts
236        // const index = byPath.get(path); return index === undefined ? undefined : candidates[index];
237        // ```
238        let index: usize = *self.by_path.get(path)?;
239        return self.candidates.get(index);
240    }
241}
242
243/// Version controls stay out of the release executable.
244#[cfg(test)]
245#[path = "candidate_version_tests.rs"]
246mod tests;