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