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