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