Skip to main content

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