Skip to main content

git_policy_cli/
candidate_prediction.rs

1//! What:
2//!  The two candidate sources that need a private index:
3//!  what `git add` would stage,
4//!       and the worktree files a direct `git cli-git check` or `fix` selects.
5//! Why:
6//!  Both are answered before Git touches the real index.
7//!  `git add` is replayed on a
8//!      private copy of the index,
9//!  and the candidates are the entries the replay changed,
10//!      as the installed wrapper's `add-policy-facts.ts` and `add-staged-delta.ts` define
11//!      them.
12//!  A direct command stages its scope with `git add --all` on a private copy,
13//!      as `direct-check-facts.ts` does,
14//!  and every selected entry is a candidate.
15//!  Either
16//!      way the bytes are blobs Git hashed from the worktree at that moment,
17//!  read through
18//!      the one object reader,
19//!  so the number of Git processes does not grow with the
20//!      number of files.
21//!
22//! In TS you'd write (pseudocode):
23//! ```ts
24//! // await using prepared = await prepareCandidates(git, { kind: 'add', region: ['--', 'a.txt'] });
25//! // for (const candidate of prepared.version.candidates) await prepared.store.bytes(candidate);
26//! ```
27
28/// Import the layer's failure type and its closed list of causes.
29use super::candidate_attribute::{AttributeSource, CandidateAttributes};
30use super::candidate_error::{CandidateError, CandidateFailure};
31/// The landed commit object name a landed-delta request reads.
32use super::candidate_object::ObjectId;
33/// Import the private index copy every prediction stages into.
34use super::candidate_private_index::PrivateIndex;
35/// Import the candidate record a version is built from.
36use super::candidate_record::CandidateRecord;
37/// Import the stage-record comparison and the two ways of turning entries into candidates.
38use super::candidate_stage::{
39    ChangedPath, StageRecord, delta_candidates, scope_candidates, staged_delta,
40};
41/// Import the per-invocation owner of versions and bytes.
42use super::candidate_store::CandidateStore;
43/// Import the immutable version type.
44use super::candidate_version::{CandidateSource, CandidateVersion};
45/// Import the captured-query runner and its byte helpers.
46use super::git_metadata::{MetadataOutput, path_from_git_bytes, run_metadata_git, strip_git_line};
47/// The in-process repository layout, tried before the `rev-parse --git-path` query.
48use super::git_system_config::with_real_git;
49use super::repo_read_layout::{git_path, read_layout};
50/// What:
51///  `OsString` is owned operating-system text of raw OS bytes (sibling `String`
52///       must be UTF-8).
53/// Why:
54///   Arguments,
55///  pathspecs and environment values reach Git byte for byte.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// // string, but byte-preserving.
60/// ```
61use std::collections::HashMap;
62use std::ffi::OsString;
63/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
64use std::path::{Path, PathBuf};
65/// `Rc<T>` is a shared,
66///  read-only handle;
67///  versions are shared by every policy of a pass.
68use std::rc::Rc;
69
70/// The environment variable that points Git at a different index file.
71pub const INDEX_FILE_VARIABLE: &str = "GIT_INDEX_FILE";
72
73/// What:
74///  The most pathspecs one `git diff-index` receives.
75///  `usize` is the type of counts.
76/// Why:
77///   The installed wrapper passes candidate paths 2,048 at a time
78///       (`commit-transaction-candidate-batch.ts`),
79///  far below any platform's argument
80///       limit;
81///  one process per 2,048 changed paths keeps the count bounded.
82///
83/// In TS you'd write (pseudocode):
84/// ```ts
85/// const PATHSPEC_CHUNK_SIZE = 2_048;
86/// ```
87pub const PATHSPEC_CHUNK_SIZE: usize = 2_048;
88
89/// What:
90///  What a lifecycle asks for.
91///  An `enum` is a closed set of named alternatives;
92///  each
93///       carries the arguments it needs.
94///  `#[derive(...)]` generates cloning,
95///  debug
96///       printing and `==`.
97/// Why:
98///   The two sources differ only in what is staged on the private copy and in which
99///       entries become candidates.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// type CandidateRequest = { kind: 'add'; region: string[] } | { kind: 'direct'; pathspecs: string[] };
104/// ```
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub enum CandidateRequest {
107    /// What `git add` with these arguments would stage.
108    Add(
109        /// The arguments after `add`,
110        ///  wrapper controls removed.
111        Vec<OsString>,
112    ),
113    /// The worktree files a direct command selects.
114    Direct(
115        /// The pathspecs of the scope;
116        ///  `--all` is `:/`.
117        Vec<OsString>,
118    ),
119    /// The entries the index already holds for the pathspecs, read without staging the worktree first.
120    ///  The commit path uses it, so corrections already written to the private index are the candidates.
121    Staged(
122        /// The pathspecs of the scope.
123        Vec<OsString>,
124    ),
125    /// The entries the private index holds for the pathspecs, with a recorded base commit (the empty tree when the
126    ///  branch was unborn) as the baseline that marks additions, read without staging the worktree. The commit
127    ///  transaction uses it: its baseline is the base it recorded at invocation, never the `HEAD` another landing may
128    ///  have moved since. No pathspec means no candidate.
129    StagedAtBase {
130        /// The recorded base commit, or nothing for an unborn branch.
131        base: Option<ObjectId>,
132        /// The pathspecs of the scope, each literal and rooted at the top level.
133        pathspecs: Vec<OsString>,
134    },
135    /// The changes a manual push publishes: each newly published commit's own delta and the whole tree
136    ///  of each tip with no known published base.
137    Pushed(
138        /// The commits and trees the scan reads.
139        PushedScope,
140    ),
141    /// The paths one landed commit changed, each against every parent,
142    ///  as the post-commit gate reads them: the committed bytes and no worktree or index state.
143    Landed(
144        /// The landed commit whose tree changes are listed.
145        ObjectId,
146    ),
147}
148
149/// What:
150///  The commits and trees a manual push scans.
151/// Why:
152///   A pushed commit contributes its own delta, and a tip with no known base contributes its tree; both are
153///  listed without staging anything.
154///
155/// In TS you'd write (pseudocode):
156/// ```ts
157/// type PushedScope = { commits: ObjectId[]; trees: ObjectId[] };
158/// ```
159#[derive(Clone, Debug, Eq, PartialEq)]
160pub struct PushedScope {
161    /// Newly published commits, each contributing its own delta.
162    pub commits: Vec<ObjectId>,
163    /// Pushed tips whose whole tree is new.
164    pub trees: Vec<ObjectId>,
165}
166
167/// What:
168///  Prepared candidates:
169///  the store that reads their bytes,
170///  the version that lists
171///       them,
172///  and the private index the store is bound to.
173///  Fields are dropped in the
174///       order written,
175///  so the store's object reader ends before the private directory
176///       is removed.
177/// Why:
178///   The store keeps reading blobs after preparation;
179///  the private index must outlive it.
180///
181/// In TS you'd write (pseudocode):
182/// ```ts
183/// type PreparedCandidates = { store: CandidateStore; version: CandidateVersion; [Symbol.dispose](): void };
184/// ```
185pub struct PreparedCandidates {
186    /// Reads candidate bytes through one long-lived object reader.
187    pub store: CandidateStore,
188    /// The candidates,
189    ///  in the order the installed wrapper reports them.
190    pub version: Rc<CandidateVersion>,
191    /// The real index the private copy was made from,
192    ///  which a direct fix proves unchanged.
193    pub real_index: PathBuf,
194    /// Reads the `text` attribute of the candidates from the state they were listed from.
195    pub attributes: CandidateAttributes,
196    /// Kept only so the private directory lives as long as the store;
197    ///  never read.
198    _private: PrivateIndex,
199}
200
201/// What:
202///  Build the failure of a Git command a prediction ran.
203///       `&MetadataOutput` borrows the captured run;
204///  `None` means Git could not be started.
205/// Why:
206///   A failed replay is reported with Git's own message,
207///  so the person sees why the
208///       command they typed could not be staged.
209///
210/// In TS you'd write (pseudocode):
211/// ```ts
212/// function predictionFailure(purpose: string, operation: string, output?: GitOutput, error?: Error): CandidateError;
213/// ```
214fn command_failure(purpose: &str, operation: &str, output: &MetadataOutput) -> CandidateError {
215    // `String::from_utf8_lossy` decodes Git's message for display, replacing invalid bytes.
216    return CandidateError::new(
217        CandidateFailure::GitFailed,
218        format!(
219            "cli-git could not {purpose}: git {operation} failed: {}",
220            String::from_utf8_lossy(output.stderr.as_slice()).trim()
221        )
222        .as_str(),
223    );
224}
225
226/// What:
227///  Build the failure of a Git command that could not be started.
228/// Why:
229///   Without Git there is nothing to predict,
230///  and the person needs the reason.
231///
232/// In TS you'd write (pseudocode):
233/// ```ts
234/// function notStarted(purpose: string, operation: string, error: Error): CandidateError;
235/// ```
236fn not_started(purpose: &str, operation: &str, error: &std::io::Error) -> CandidateError {
237    return CandidateError::new(
238        CandidateFailure::GitNotStarted,
239        format!("cli-git could not {purpose}: git {operation} could not be started: {error}.")
240            .as_str(),
241    );
242}
243
244/// What:
245///  Run one Git command of a prediction and return its output when it succeeded.
246///       `&[OsString]` borrows the global prefix and the command separately.
247/// Why:
248///   The prefix selects the same repository the caller's command selects;
249///  the
250///       command follows it.
251///
252/// In TS you'd write (pseudocode):
253/// ```ts
254/// async function runGit(git, prefix, command, overlay, purpose): Promise<GitOutput>;
255/// ```
256fn run_git(
257    real_git: &Path,
258    prefix: &[OsString],
259    command: &[OsString],
260    overlay: &[(OsString, OsString)],
261    purpose: &str,
262) -> Result<MetadataOutput, CandidateError> {
263    // `.to_vec()` copies the borrowed prefix; `.extend_from_slice(..)` appends the command.
264    let mut arguments: Vec<OsString> = prefix.to_vec();
265    arguments.extend_from_slice(command);
266    // The operation named in messages is the command's first word, `add` or `rev-parse`.
267    let operation: String = match command.first() {
268        Some(word) => word.to_string_lossy().into_owned(),
269        None => String::from("(no command)"),
270    };
271    // `match` on the start result: `Err(error)` means Git could not be started at all.
272    let output: MetadataOutput = match run_metadata_git(real_git, arguments.as_slice(), overlay) {
273        Ok(captured) => captured,
274        Err(error) => return Err(not_started(purpose, operation.as_str(), &error)),
275    };
276    if !output.success {
277        return Err(command_failure(purpose, operation.as_str(), &output));
278    }
279    return Ok(output);
280}
281
282/// What:
283///  The absolute path of the index the caller's command would use.
284/// Why:
285///   Git answers for `GIT_INDEX_FILE`,
286///  a linked worktree's own index and
287///       `--git-dir`,
288///  so the copy is of exactly the index `git add` would change.
289///
290/// In TS you'd write (pseudocode):
291/// ```ts
292/// async function realIndexPath(git, prefix, overlay): Promise<string>;
293/// ```
294fn real_index_path(
295    real_git: &Path,
296    prefix: &[OsString],
297    overlay: &[(OsString, OsString)],
298    purpose: &str,
299) -> Result<PathBuf, CandidateError> {
300    // A plain layout names the index without a Git process.
301    if let Some(path) = read_layout(prefix, with_real_git(overlay, real_git).as_slice()).as_ref().and_then(|layout| { return git_path(layout, "index"); }) {
302        return Ok(path);
303    }
304    let output: MetadataOutput = run_git(
305        real_git,
306        prefix,
307        &[
308            OsString::from("rev-parse"),
309            OsString::from("--path-format=absolute"),
310            OsString::from("--git-path"),
311            OsString::from("index"),
312        ],
313        overlay,
314        purpose,
315    )?;
316    // `match` unpacks "a path or nothing": empty output names no index.
317    match path_from_git_bytes(strip_git_line(output.stdout.as_slice())) {
318        Some(path) => return Ok(path),
319        None => {
320            return Err(CandidateError::new(
321                CandidateFailure::ListingMalformed,
322                format!(
323                    "cli-git could not {purpose}: git rev-parse --git-path index printed no path."
324                )
325                .as_str(),
326            ));
327        }
328    }
329}
330
331/// What:
332///  One literal,
333///  top-level pathspec naming exactly `path`,
334///  or nothing when the bytes
335///       cannot be a path on this platform.
336/// Why:
337///   `:(top,literal)` makes Git match the repository-relative name byte for byte from
338///       any directory,
339///  whatever wildcard or magic characters it contains.
340///
341/// In TS you'd write (pseudocode):
342/// ```ts
343/// const literalPathspec = (path: Buffer) => Buffer.concat([Buffer.from(':(top,literal)'), path]);
344/// ```
345fn literal_pathspec(path: &[u8]) -> Option<OsString> {
346    // `?` returns `None` when the bytes are not a path here.
347    let native: PathBuf = path_from_git_bytes(path)?;
348    let mut pathspec: OsString = OsString::from(":(top,literal)");
349    pathspec.push(native.as_os_str());
350    return Some(pathspec);
351}
352
353/// What:
354///  The records of `HEAD` for the changed paths,
355///  asked 2,048 paths at a time.
356/// Why:
357///   Which changed paths `HEAD` lacks or still has decides additions and deletions;
358///       nothing outside the changed paths is listed,
359///  so a conflict elsewhere in the
360///       index cannot stop the prediction.
361///  No change asks nothing.
362///
363/// In TS you'd write (pseudocode):
364/// ```ts
365/// async function baselineRecords(store: CandidateStore, changed: ChangedPath[]): Promise<CandidateRecord[]>;
366/// ```
367fn baseline_records(
368    store: &mut CandidateStore,
369    changed: &[ChangedPath],
370) -> Result<Vec<CandidateRecord>, CandidateError> {
371    let mut records: Vec<CandidateRecord> = Vec::new();
372    // `.chunks(n)` yields consecutive slices of at most `n` items; an empty list yields none.
373    for chunk in changed.chunks(PATHSPEC_CHUNK_SIZE) {
374        let mut pathspecs: Vec<OsString> = Vec::with_capacity(chunk.len());
375        for path in chunk {
376            let Some(pathspec) = literal_pathspec(path.path.as_slice()) else {
377                return Err(CandidateError::new(
378                    CandidateFailure::ListingMalformed,
379                    "cli-git could not predict what git add stages: Git listed an entry whose \
380                     name is not a path on this platform.",
381                ));
382            };
383            pathspecs.push(pathspec);
384        }
385        records.extend(store.head_records(pathspecs.as_slice())?);
386    }
387    return Ok(records);
388}
389
390/// What:
391///  The candidates `git add` with `region` would stage,
392///  against `store`'s private index.
393/// Why:
394///   The whole index is listed before and after the replay,
395///  so a path the replay
396///       touched outside the caller's directory is still found.
397///  The replay uses the
398///       caller's own prefix and environment,
399///  so it reads its pathspecs exactly as the
400///       real command will.
401///
402/// In TS you'd write (pseudocode):
403/// ```ts
404/// async function predictAdd(store, git, prefix, overlay, region): Promise<CandidateRecord[]>;
405/// ```
406fn predicted_add(
407    store: &mut CandidateStore,
408    real_git: &Path,
409    prefix: &[OsString],
410    overlay: &[(OsString, OsString)],
411    region: &[OsString],
412) -> Result<Vec<CandidateRecord>, CandidateError> {
413    let whole_index: [OsString; 1] = [OsString::from(":/")];
414    let before: Vec<StageRecord> = store.stage_records(&whole_index)?;
415    let mut command: Vec<OsString> = vec![OsString::from("add")];
416    command.extend_from_slice(region);
417    run_git(
418        real_git,
419        prefix,
420        command.as_slice(),
421        overlay,
422        "predict what git add stages",
423    )?;
424    let after: Vec<StageRecord> = store.stage_records(&whole_index)?;
425    let changed: Vec<ChangedPath> = staged_delta(before.as_slice(), after.as_slice());
426    let baseline: Vec<CandidateRecord> = baseline_records(store, changed.as_slice())?;
427    return delta_candidates(changed.as_slice(), baseline.as_slice());
428}
429
430/// What:
431///  Every entry `pathspecs` select after staging them with `git add --all` on the
432///       private index.
433/// Why:
434///   The private stage hashes the selected worktree files,
435///  so their bytes are what
436///       the worktree holds,
437///  tracked and new files alike,
438///  and the real index is untouched.
439///
440/// In TS you'd write (pseudocode):
441/// ```ts
442/// async function projectScope(store, git, prefix, overlay, pathspecs): Promise<CandidateRecord[]>;
443/// ```
444fn projected_scope(
445    store: &mut CandidateStore,
446    real_git: &Path,
447    prefix: &[OsString],
448    overlay: &[(OsString, OsString)],
449    pathspecs: &[OsString],
450) -> Result<Vec<CandidateRecord>, CandidateError> {
451    let mut command: Vec<OsString> = vec![
452        OsString::from("add"),
453        OsString::from("--all"),
454        OsString::from("--"),
455    ];
456    command.extend_from_slice(pathspecs);
457    run_git(
458        real_git,
459        prefix,
460        command.as_slice(),
461        overlay,
462        "read the selected worktree files",
463    )?;
464    return staged_scope(store, pathspecs);
465}
466
467/// What:
468///  The candidates of the entries the index already holds for these pathspecs, against `HEAD`.
469/// Why:
470///   The direct and the staged requests both end here; only the direct one stages the worktree first.
471///
472/// In TS you'd write (pseudocode):
473/// ```ts
474/// scopeCandidates(await stageRecords(pathspecs), await headRecords(pathspecs));
475/// ```
476fn staged_scope(
477    store: &mut CandidateStore,
478    pathspecs: &[OsString],
479) -> Result<Vec<CandidateRecord>, CandidateError> {
480    let entries: Vec<StageRecord> = store.stage_records(pathspecs)?;
481    if entries.is_empty() {
482        // Nothing selected: no baseline is needed and no object reader starts.
483        return Ok(Vec::new());
484    }
485    let baseline: Vec<CandidateRecord> = store.head_records(pathspecs)?;
486    return scope_candidates(entries.as_slice(), baseline.as_slice());
487}
488
489/// What:
490///  Record `source` as the commit or tree that first lists each pathname of `listed`.
491///  `HashMap::entry(..).or_insert_with(..)` keeps the first source of a pathname.
492/// Why:
493///   A pushed pathname can be listed by several commits;
494///  its candidate comes from the first, so its attributes do too.
495///
496/// In TS you'd write (pseudocode):
497/// ```ts
498/// for (const record of listed) if (!listedFrom.has(record.path)) listedFrom.set(record.path, source);
499/// ```
500fn note_listing(
501    listed_from: &mut HashMap<Vec<u8>, ObjectId>,
502    listed: &[CandidateRecord],
503    source: &ObjectId,
504) {
505    for record in listed {
506        listed_from
507            .entry(record.path.clone())
508            .or_insert_with(|| { return source.clone(); });
509    }
510}
511
512/// What:
513///  Prepare the candidates of one request.
514///  `real_git`,
515///  `global_prefix` and `overlay`
516///       are the Git executable,
517///  the caller's arguments before the subcommand and the
518///       child environment additions,
519///  exactly as every other Git query receives them.
520/// Why:
521///   The real index is copied first,
522///  so nothing below can change it.
523///  For `git add`
524///       the store's own listings add `--no-literal-pathspecs` after the caller's prefix:
525///       they rely on pathspec magic (`:/`,
526///  `:(top,literal)`),
527///  which literal pathspec
528///       mode,
529///  from `GIT_LITERAL_PATHSPECS` or `--literal-pathspecs`,
530///  would turn into
531///       names that match nothing,
532///  leaving every change unchecked.
533///  The replay keeps the
534///       caller's mode.
535///
536/// In TS you'd write (pseudocode):
537/// ```ts
538/// async function prepareCandidates(git, prefix, overlay, request): Promise<PreparedCandidates>;
539/// ```
540pub fn prepare_candidates(
541    real_git: &Path,
542    global_prefix: &[OsString],
543    overlay: &[(OsString, OsString)],
544    request: &CandidateRequest,
545) -> Result<PreparedCandidates, CandidateError> {
546    let purpose: &str = match request {
547        CandidateRequest::Add(_) => "predict what git add stages",
548        CandidateRequest::Direct(_) => "read the selected worktree files",
549        CandidateRequest::Staged(_) | CandidateRequest::StagedAtBase { .. } => "read the staged index entries",
550        CandidateRequest::Landed(_) => "read the landed commit changes",
551        CandidateRequest::Pushed(_) => "read the pushed commit changes",
552    };
553    let real_index: PathBuf = real_index_path(real_git, global_prefix, overlay, purpose)?;
554    let private: PrivateIndex = PrivateIndex::create(real_index.as_path())?;
555    // The caller's environment, with every command of the prediction on the private index.
556    let mut private_overlay: Vec<(OsString, OsString)> = overlay.to_vec();
557    private_overlay.push((
558        OsString::from(INDEX_FILE_VARIABLE),
559        private.path().as_os_str().to_os_string(),
560    ));
561    // A pushed scope lists committed records directly; nothing is staged or predicted for it.
562    if let CandidateRequest::Pushed(scope) = request {
563        let mut pushed_store: CandidateStore =
564            CandidateStore::new(real_git, global_prefix, private_overlay.as_slice());
565        let mut records: Vec<CandidateRecord> = Vec::new();
566        // The commit or tree that first lists a pathname answers for its attributes, as it answers for the
567        // pathname's candidate.
568        let mut listed_from: HashMap<Vec<u8>, ObjectId> = HashMap::new();
569        for commit in &scope.commits {
570            let listed: Vec<CandidateRecord> = pushed_store.committed_records(commit)?;
571            note_listing(&mut listed_from, listed.as_slice(), commit);
572            records.extend(listed);
573        }
574        for tree in &scope.trees {
575            let listed: Vec<CandidateRecord> = pushed_store.tree_records(tree)?;
576            note_listing(&mut listed_from, listed.as_slice(), tree);
577            records.extend(listed);
578        }
579        let version: Rc<CandidateVersion> = pushed_store.derived_version(records);
580        return Ok(PreparedCandidates {
581            store: pushed_store,
582            version,
583            real_index,
584            attributes: CandidateAttributes::new(
585                real_git,
586                global_prefix,
587                private_overlay.as_slice(),
588                AttributeSource::PerPath(listed_from),
589            ),
590            _private: private,
591        });
592    }
593    // A landed commit lists its committed records directly; nothing is staged or predicted for it.
594    if let CandidateRequest::Landed(commit) = request {
595        let mut landed_store: CandidateStore =
596            CandidateStore::new(real_git, global_prefix, private_overlay.as_slice());
597        let version: Rc<CandidateVersion> =
598            landed_store.version(&CandidateSource::Committed(commit.clone()))?;
599        return Ok(PreparedCandidates {
600            store: landed_store,
601            version,
602            real_index,
603            attributes: CandidateAttributes::new(
604                real_git,
605                global_prefix,
606                private_overlay.as_slice(),
607                AttributeSource::Tree(commit.clone()),
608            ),
609            _private: private,
610        });
611    }
612    // `match` picks the source; each arm builds its store and its candidate records.
613    // `mut store` because building the version below reads the store's generation.
614    let (mut store, records): (CandidateStore, Vec<CandidateRecord>) = match request {
615        CandidateRequest::Add(region) => {
616            let mut listing_prefix: Vec<OsString> = global_prefix.to_vec();
617            listing_prefix.push(OsString::from("--no-literal-pathspecs"));
618            let mut add_store: CandidateStore = CandidateStore::new(
619                real_git,
620                listing_prefix.as_slice(),
621                private_overlay.as_slice(),
622            );
623            let staged: Vec<CandidateRecord> = predicted_add(
624                &mut add_store,
625                real_git,
626                global_prefix,
627                private_overlay.as_slice(),
628                region.as_slice(),
629            )?;
630            (add_store, staged)
631        }
632        CandidateRequest::Landed(_) | CandidateRequest::Pushed(_) => {
633            unreachable!("landed and pushed requests return before the staged match");
634        }
635        CandidateRequest::Direct(pathspecs) => {
636            // The same magic-enabled prefix as the add arm: the staging and the listings must read
637            // `:/` and the selected pathspecs alike, so literal mode cannot split them.
638            let mut scope_prefix: Vec<OsString> = global_prefix.to_vec();
639            scope_prefix.push(OsString::from("--no-literal-pathspecs"));
640            let mut scope_store: CandidateStore =
641                CandidateStore::new(real_git, scope_prefix.as_slice(), private_overlay.as_slice());
642            let selected: Vec<CandidateRecord> = projected_scope(
643                &mut scope_store,
644                real_git,
645                scope_prefix.as_slice(),
646                private_overlay.as_slice(),
647                pathspecs.as_slice(),
648            )?;
649            (scope_store, selected)
650        }
651        CandidateRequest::Staged(pathspecs) => {
652            let mut scope_store: CandidateStore =
653                CandidateStore::new(real_git, global_prefix, private_overlay.as_slice());
654            let selected: Vec<CandidateRecord> = staged_scope(&mut scope_store, pathspecs.as_slice())?;
655            (scope_store, selected)
656        }
657        CandidateRequest::StagedAtBase { base, pathspecs } => {
658            let mut scope_store: CandidateStore =
659                CandidateStore::new(real_git, global_prefix, private_overlay.as_slice()).with_baseline(base.clone());
660            // An empty scope would list the whole index, so it is answered without asking Git.
661            let selected: Vec<CandidateRecord> = if pathspecs.is_empty() {
662                Vec::new()
663            } else {
664                staged_scope(&mut scope_store, pathspecs.as_slice())?
665            };
666            (scope_store, selected)
667        }
668    };
669    let version: Rc<CandidateVersion> = store.derived_version(records);
670    return Ok(PreparedCandidates {
671        store,
672        version,
673        real_index,
674        // Staged and projected candidates are judged by the private index they were listed from.
675        attributes: CandidateAttributes::new(
676            real_git,
677            global_prefix,
678            private_overlay.as_slice(),
679            AttributeSource::Index,
680        ),
681        _private: private,
682    });
683}
684
685/// Prediction and projection controls against real Git stay out of the release executable.
686#[cfg(test)]
687#[path = "candidate_prediction_tests.rs"]
688mod tests;
689
690/// The process-count bound of both sources and its per-file positive control.
691#[cfg(test)]
692#[path = "candidate_prediction_count_tests.rs"]
693mod count_tests;