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;