Skip to main content

git_policy_cli/candidate/
candidate_known_content.rs

1//! What:
2//!  Which candidates hold content that a parent commit's tree already holds,
3//!  found by exact blob object name.
4//! Why:
5//!  A pure rename or a copy re-lists a file as an addition,
6//!  and a merge lists every file one parent lacks as an addition,
7//!  although the bytes are already in history.
8//!  Scanning them again refuses moves of
9//!      files that were accepted when they entered history,
10//!  while an object name is the hash of the bytes,
11//!  so a
12//!      candidate whose object a parent holds carries no byte that is new.
13//!  The comparison is by object
14//!      name only:
15//!  never by pathname,
16//!  never by similarity,
17//!  so one changed byte makes the content new.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! // const known = await findKnownContent(store, version, ContentBasis.of(request), rulesPath, corrected);
22//! // known.entry(candidate.identity.index)?.parent
23//! ```
24
25/// Import the layer's failure type.
26use crate::candidate_error::CandidateError;
27/// Import the candidate modes and the validated object name.
28use crate::candidate_object::{CandidateMode, ObjectId};
29/// Import what a lifecycle asks for.
30use crate::candidate_prediction::CandidateRequest;
31/// Import the store that lists commits and trees.
32use crate::candidate_store::CandidateStore;
33/// Import the candidate and the immutable version type.
34use crate::candidate_version::{Candidate, CandidateVersion};
35/// Import the change a converged fix installs,
36///  which marks a candidate whose bytes are no longer its object's.
37use crate::direct_fix_install::InstallChange;
38/// Import which candidates a scan reads at all.
39use crate::scanner_selection::is_scannable;
40/// What:
41///  `BTreeMap<K, V>` is a map kept sorted by key (sibling `HashMap` is unordered);
42///  `HashSet<T>` is an
43///       unordered set with constant-time membership tests.
44/// Why:
45///   Known candidates are listed in candidate order,
46///  and each parent's blobs are looked up by name.
47///
48/// In TS you'd write (pseudocode):
49/// ```ts
50/// // Map<number, KnownEntry> iterated in key order; Set<ObjectId>.
51/// ```
52use std::collections::{BTreeMap, HashSet};
53
54/// What:
55///  The commits whose trees count as already holding content for one lifecycle.
56///       `#[derive(...)]` generates cloning,
57///  debug printing and `==`.
58/// Why:
59///   Each lifecycle compares with a different history:
60///  the commit being made with its base,
61///  a landed
62///       commit with each of its parents,
63///  a push with the parents that were published before it.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// type ContentBasis = { kind: 'nothing' } | { kind: 'baseline' } | { kind: 'parents-outside'; commits: ObjectId[] };
68/// ```
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub enum ContentBasis {
71    /// No tree counts:
72    ///  a direct check reads the selected files for what they hold now,
73    ///  and a push with a tip whose published history is unknown has no commit to vouch for anything.
74    Nothing,
75    /// The commit the candidates are compared with:
76    ///  the recorded base of a commit,
77    ///  else the commit `HEAD` names;
78    ///  an unborn branch has none.
79    Baseline,
80    /// Every parent of these commits that is not one of them:
81    ///  a landed commit's parents,
82    ///  or a push's already published parents.
83    ParentsOutside(
84        /// The commits whose parents are listed.
85        Vec<ObjectId>,
86    ),
87}
88
89/// What:
90///  `impl ContentBasis { ... }` attaches the choice for a request.
91/// Why:
92///   The choice follows the request's kind alone,
93///  so no policy decides it separately.
94///
95/// In TS you'd write (pseudocode):
96/// ```ts
97/// class ContentBasis { static of(request: CandidateRequest): ContentBasis }
98/// ```
99impl ContentBasis {
100    /// What:
101    ///  The basis of one request.
102    ///  `&CandidateRequest` borrows what the lifecycle asked for.
103    /// Why:
104    ///   A parent commit of a pushed commit that is itself pushed is new to the remote,
105    ///  so a push counts only the parents outside the pushed commits.
106    ///  A pushed tip tree has no published
107    ///       base to compare with,
108    ///  and its candidates share one version with the commits' candidates,
109    ///  so a push with such a tip skips nothing.
110    ///
111    /// In TS you'd write (pseudocode):
112    /// ```ts
113    /// static of(request: CandidateRequest): ContentBasis
114    /// ```
115    pub fn of(request: &CandidateRequest) -> ContentBasis {
116        match request {
117            CandidateRequest::Direct(_) => return ContentBasis::Nothing,
118            CandidateRequest::Add(_)
119            | CandidateRequest::Staged(_)
120            | CandidateRequest::StagedAtBase { .. } => return ContentBasis::Baseline,
121            CandidateRequest::Landed(commit) => {
122                return ContentBasis::ParentsOutside(vec![commit.clone()]);
123            }
124            CandidateRequest::Pushed(scope) => {
125                if !scope.trees.is_empty() {
126                    return ContentBasis::Nothing;
127                }
128                return ContentBasis::ParentsOutside(scope.commits.clone());
129            }
130        }
131    }
132}
133
134/// What:
135///  One candidate whose content a parent already holds:
136///  the object and the first parent that holds it.
137///       `#[derive(...)]` generates cloning,
138///  debug printing and `==`.
139/// Why:
140///   The skip is a decision,
141///  and a decision is only checkable when its inputs are kept.
142///
143/// In TS you'd write (pseudocode):
144/// ```ts
145/// type KnownEntry = { object: ObjectId; parent: ObjectId };
146/// ```
147#[derive(Clone, Debug, Eq, PartialEq)]
148pub struct KnownEntry {
149    /// The candidate's blob.
150    pub object: ObjectId,
151    /// The commit whose tree holds the blob.
152    pub parent: ObjectId,
153}
154
155/// What:
156///  `impl KnownEntry { ... }` attaches the record of the decision.
157/// Why:
158///   One wording for the decision,
159///  so a log line always carries the same inputs.
160///
161/// In TS you'd write (pseudocode):
162/// ```ts
163/// class KnownEntry { describe(displayPath: string): string }
164/// ```
165impl KnownEntry {
166    /// What:
167    ///  The decision as one line of text.
168    ///  `&str` is the pathname as the scanner masks it.
169    /// Why:
170    ///   The pathname is shown masked because a pathname may hold a secret;
171    ///  the blob and the parent are
172    ///       object names.
173    ///
174    /// In TS you'd write (pseudocode):
175    /// ```ts
176    /// describe(displayPath: string): string
177    /// ```
178    pub fn describe(&self, display_path: &str) -> String {
179        return format!(
180            "skipped the content of {display_path}: blob {} is already in the tree of commit {}; only the pathname was checked.",
181            self.object.as_str(),
182            self.parent.as_str()
183        );
184    }
185}
186
187/// What:
188///  The known candidates of one version,
189///  by the candidate's position in the version.
190///  `#[derive(...)]` generates cloning,
191///  debug printing and `==`;
192///  `Default` is the empty value.
193/// Why:
194///   The scan pass asks by position,
195///  and the log lists them in candidate order.
196///
197/// In TS you'd write (pseudocode):
198/// ```ts
199/// class KnownContent { entry(index: number): KnownEntry | undefined }
200/// ```
201#[derive(Clone, Debug, Default, Eq, PartialEq)]
202pub struct KnownContent {
203    /// The known candidates,
204    ///  sorted by position.
205    entries: BTreeMap<usize, KnownEntry>,
206}
207
208/// What:
209///  `impl KnownContent { ... }` attaches the read-only accessors.
210/// Why:
211///   A scan can read the decision and cannot change it.
212///
213/// In TS you'd write (pseudocode):
214/// ```ts
215/// class KnownContent { entry(index) {} len() {} }
216/// ```
217impl KnownContent {
218    /// What:
219    ///  The entry of the candidate at `index`,
220    ///  if its content is known.
221    /// Why:
222    ///   The scan pass asks once per candidate.
223    ///
224    /// In TS you'd write (pseudocode):
225    /// ```ts
226    /// entry(index: number): KnownEntry | undefined
227    /// ```
228    pub fn entry(&self, index: usize) -> Option<&KnownEntry> {
229        return self.entries.get(&index);
230    }
231
232    /// What:
233    ///  How many candidates are known.
234    /// Why:
235    ///   A pass with none reads exactly the bytes it read before the rule existed.
236    ///
237    /// In TS you'd write (pseudocode):
238    /// ```ts
239    /// get length(): number
240    /// ```
241    pub fn len(&self) -> usize {
242        return self.entries.len();
243    }
244
245    /// What:
246    ///  Whether no candidate is known.
247    /// Why:
248    ///   Clippy asks a type with a `len()` to provide `is_empty()` too.
249    ///
250    /// In TS you'd write (pseudocode):
251    /// ```ts
252    /// get isEmpty(): boolean
253    /// ```
254    pub fn is_empty(&self) -> bool {
255        return self.entries.is_empty();
256    }
257}
258
259/// What:
260///  Whether a candidate's content can be known from a parent's tree at all.
261///       `&[Vec<u8>]` is the rules files' own pathnames.
262/// Why:
263///   Only a scanned blob has bytes that a parent could already hold.
264///  A submodule entry is a commit name,
265///  and a file a fix corrected no longer holds its object's bytes.
266///
267/// In TS you'd write (pseudocode):
268/// ```ts
269/// const comparable = (candidate, rulesPath, corrected) => isScannable(candidate, rulesPath) && candidate.object !== undefined && candidate.mode !== 'gitlink' && !corrected.has(candidate.path);
270/// ```
271fn is_comparable(
272    candidate: &Candidate,
273    rules_paths: &[Vec<u8>],
274    corrected: &BTreeMap<Vec<u8>, InstallChange>,
275) -> bool {
276    return is_scannable(candidate, rules_paths)
277        && candidate.object.is_some()
278        && candidate.mode != CandidateMode::Gitlink
279        && !corrected.contains_key(&candidate.path);
280}
281
282/// What:
283///  Find every comparable candidate whose blob a parent's tree holds.
284///  `&mut CandidateStore` lends the listing;
285///  `&[Vec<u8>]` is the rules files' pathnames;
286///  `&BTreeMap<..>` are the files a fix corrected.
287/// Why:
288///   Parents are listed one after another and listing stops when every candidate is placed,
289///  so a
290///       version that is all known costs one listing.
291///  No candidate to compare and no parent to compare with
292///       each cost no Git process beyond the basis question.
293///  The first parent that holds a blob is the one reported.
294///
295/// In TS you'd write (pseudocode):
296/// ```ts
297/// async function findKnownContent(store, version, basis, rulesPath, corrected): Promise<KnownContent>;
298/// ```
299pub fn find_known_content(
300    store: &mut CandidateStore,
301    version: &CandidateVersion,
302    basis: &ContentBasis,
303    rules_paths: &[Vec<u8>],
304    corrected: &BTreeMap<Vec<u8>, InstallChange>,
305) -> Result<KnownContent, CandidateError> {
306    let mut pending: Vec<&Candidate> = Vec::new();
307    for candidate in version.candidates() {
308        if is_comparable(candidate, rules_paths, corrected) {
309            pending.push(candidate);
310        }
311    }
312    let mut entries: BTreeMap<usize, KnownEntry> = BTreeMap::new();
313    if pending.is_empty() {
314        return Ok(KnownContent { entries });
315    }
316    let parents: Vec<ObjectId> = match basis {
317        ContentBasis::Nothing => Vec::new(),
318        // `.into_iter().collect()` turns "a commit or nothing" into a list of zero or one.
319        ContentBasis::Baseline => store.baseline_reference()?.into_iter().collect(),
320        ContentBasis::ParentsOutside(commits) => store.parents_outside(commits.as_slice())?,
321    };
322    for parent in parents {
323        if pending.is_empty() {
324            break;
325        }
326        let mut blobs: HashSet<ObjectId> = HashSet::new();
327        for record in store.commit_tree_records(&parent)? {
328            if record.mode != CandidateMode::Gitlink {
329                blobs.insert(record.object);
330            }
331        }
332        let mut unplaced: Vec<&Candidate> = Vec::new();
333        for candidate in pending {
334            match candidate.object.as_ref() {
335                Some(object) if blobs.contains(object) => {
336                    entries.insert(
337                        candidate.identity.index,
338                        KnownEntry {
339                            object: object.clone(),
340                            parent: parent.clone(),
341                        },
342                    );
343                }
344                Some(_) | None => unplaced.push(candidate),
345            }
346        }
347        pending = unplaced;
348    }
349    return Ok(KnownContent { entries });
350}
351
352/// Basis choice,
353///  lookup by object name and the stop once every candidate is placed stay out of the release executable.
354#[cfg(test)]
355#[path = "candidate_known_content_tests.rs"]
356mod tests;