use crate::candidate_pathspec_chunk_size::PATHSPEC_CHUNK_SIZE;
use crate::candidate_prediction_git_runs::{real_index_path, run_git};
use crate::candidate_attribute::{AttributeSource, CandidateAttributes};
use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::ObjectId;
use crate::candidate_private_index::PrivateIndex;
use crate::candidate_record::CandidateRecord;
use crate::candidate_stage::{
ChangedPath, StageRecord, delta_candidates, scope_candidates, staged_delta,
};
use crate::candidate_store::CandidateStore;
use crate::candidate_version::{CandidateSource, CandidateVersion};
use crate::git_metadata::path_from_git_bytes;
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::rc::Rc;
pub const INDEX_FILE_VARIABLE: &str = "GIT_INDEX_FILE";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CandidateRequest {
Add(
Vec<OsString>,
),
Direct(
Vec<OsString>,
),
Staged(
Vec<OsString>,
),
StagedAtBase {
base: Option<ObjectId>,
pathspecs: Vec<OsString>,
},
Pushed(
PushedScope,
),
Landed(
ObjectId,
),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PushedScope {
pub commits: Vec<ObjectId>,
pub trees: Vec<ObjectId>,
}
pub struct PreparedCandidates {
pub store: CandidateStore,
pub version: Rc<CandidateVersion>,
pub real_index: PathBuf,
pub attributes: CandidateAttributes,
_private: PrivateIndex,
}
fn literal_pathspec(path: &[u8]) -> Option<OsString> {
let native: PathBuf = path_from_git_bytes(path)?;
let mut pathspec: OsString = OsString::from(":(top,literal)");
pathspec.push(native.as_os_str());
return Some(pathspec);
}
fn baseline_records(
store: &mut CandidateStore,
changed: &[ChangedPath],
) -> Result<Vec<CandidateRecord>, CandidateError> {
let mut records: Vec<CandidateRecord> = Vec::new();
for chunk in changed.chunks(PATHSPEC_CHUNK_SIZE) {
let mut pathspecs: Vec<OsString> = Vec::with_capacity(chunk.len());
for path in chunk {
let Some(pathspec) = literal_pathspec(path.path.as_slice()) else {
return Err(CandidateError::new(
CandidateFailure::ListingMalformed,
"monochromatic-git-policy-cli could not predict what git add stages: Git listed an entry whose \
name is not a path on this platform.",
));
};
pathspecs.push(pathspec);
}
records.extend(store.head_records(pathspecs.as_slice())?);
}
return Ok(records);
}
fn predicted_add(
store: &mut CandidateStore,
real_git: &Path,
prefix: &[OsString],
overlay: &[(OsString, OsString)],
region: &[OsString],
) -> Result<Vec<CandidateRecord>, CandidateError> {
let whole_index: [OsString; 1] = [OsString::from(":/")];
let before: Vec<StageRecord> = store.stage_records(&whole_index)?;
let mut command: Vec<OsString> = vec![OsString::from("add")];
command.extend_from_slice(region);
run_git(
real_git,
prefix,
command.as_slice(),
overlay,
"predict what git add stages",
)?;
let after: Vec<StageRecord> = store.stage_records(&whole_index)?;
let changed: Vec<ChangedPath> = staged_delta(before.as_slice(), after.as_slice());
let baseline: Vec<CandidateRecord> = baseline_records(store, changed.as_slice())?;
return delta_candidates(changed.as_slice(), baseline.as_slice());
}
fn projected_scope(
store: &mut CandidateStore,
real_git: &Path,
prefix: &[OsString],
overlay: &[(OsString, OsString)],
pathspecs: &[OsString],
) -> Result<Vec<CandidateRecord>, CandidateError> {
let mut command: Vec<OsString> = vec![
OsString::from("add"),
OsString::from("--all"),
OsString::from("--"),
];
command.extend_from_slice(pathspecs);
run_git(
real_git,
prefix,
command.as_slice(),
overlay,
"read the selected worktree files",
)?;
return staged_scope(store, pathspecs);
}
fn staged_scope(
store: &mut CandidateStore,
pathspecs: &[OsString],
) -> Result<Vec<CandidateRecord>, CandidateError> {
let entries: Vec<StageRecord> = store.stage_records(pathspecs)?;
if entries.is_empty() {
return Ok(Vec::new());
}
let baseline: Vec<CandidateRecord> = store.head_records(pathspecs)?;
return scope_candidates(entries.as_slice(), baseline.as_slice());
}
fn note_listing(
listed_from: &mut HashMap<Vec<u8>, ObjectId>,
listed: &[CandidateRecord],
source: &ObjectId,
) {
for record in listed {
if let std::collections::hash_map::Entry::Vacant(slot) = listed_from.entry(record.path.clone()) {
slot.insert(source.clone());
}
}
}
pub fn prepare_candidates(
real_git: &Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
request: &CandidateRequest,
) -> Result<PreparedCandidates, CandidateError> {
let purpose: &str = match request {
CandidateRequest::Add(_) => "predict what git add stages",
CandidateRequest::Direct(_) => "read the selected worktree files",
CandidateRequest::Staged(_) | CandidateRequest::StagedAtBase { .. } => "read the staged index entries",
CandidateRequest::Landed(_) => "read the landed commit changes",
CandidateRequest::Pushed(_) => "read the pushed commit changes",
};
let real_index: PathBuf = real_index_path(real_git, global_prefix, overlay, purpose)?;
let private: PrivateIndex = PrivateIndex::create(real_index.as_path())?;
let mut private_overlay: Vec<(OsString, OsString)> = overlay.to_vec();
private_overlay.push((
OsString::from(INDEX_FILE_VARIABLE),
private.path().as_os_str().to_os_string(),
));
if let CandidateRequest::Pushed(scope) = request {
let mut pushed_store: CandidateStore =
CandidateStore::new(real_git, global_prefix, private_overlay.as_slice());
let mut records: Vec<CandidateRecord> = Vec::new();
let mut listed_from: HashMap<Vec<u8>, ObjectId> = HashMap::new();
for commit in &scope.commits {
let listed: Vec<CandidateRecord> = pushed_store.committed_records(commit)?;
note_listing(&mut listed_from, listed.as_slice(), commit);
records.extend(listed);
}
for tree in &scope.trees {
let listed: Vec<CandidateRecord> = pushed_store.tree_records(tree)?;
note_listing(&mut listed_from, listed.as_slice(), tree);
records.extend(listed);
}
let version: Rc<CandidateVersion> = pushed_store.derived_version(records);
return Ok(PreparedCandidates {
store: pushed_store,
version,
real_index,
attributes: CandidateAttributes::new(
real_git,
global_prefix,
private_overlay.as_slice(),
AttributeSource::PerPath(listed_from),
),
_private: private,
});
}
if let CandidateRequest::Landed(commit) = request {
let mut landed_store: CandidateStore =
CandidateStore::new(real_git, global_prefix, private_overlay.as_slice());
let version: Rc<CandidateVersion> =
landed_store.version(&CandidateSource::Committed(commit.clone()))?;
return Ok(PreparedCandidates {
store: landed_store,
version,
real_index,
attributes: CandidateAttributes::new(
real_git,
global_prefix,
private_overlay.as_slice(),
AttributeSource::Tree(commit.clone()),
),
_private: private,
});
}
let (mut store, records): (CandidateStore, Vec<CandidateRecord>) = match request {
CandidateRequest::Add(region) => {
let mut listing_prefix: Vec<OsString> = global_prefix.to_vec();
listing_prefix.push(OsString::from("--no-literal-pathspecs"));
let mut add_store: CandidateStore = CandidateStore::new(
real_git,
listing_prefix.as_slice(),
private_overlay.as_slice(),
);
let staged: Vec<CandidateRecord> = predicted_add(
&mut add_store,
real_git,
global_prefix,
private_overlay.as_slice(),
region.as_slice(),
)?;
(add_store, staged)
}
CandidateRequest::Landed(_) | CandidateRequest::Pushed(_) => {
unreachable!("landed and pushed requests return before the staged match");
}
CandidateRequest::Direct(pathspecs) => {
let mut scope_prefix: Vec<OsString> = global_prefix.to_vec();
scope_prefix.push(OsString::from("--no-literal-pathspecs"));
let mut scope_store: CandidateStore =
CandidateStore::new(real_git, scope_prefix.as_slice(), private_overlay.as_slice());
let selected: Vec<CandidateRecord> = projected_scope(
&mut scope_store,
real_git,
scope_prefix.as_slice(),
private_overlay.as_slice(),
pathspecs.as_slice(),
)?;
(scope_store, selected)
}
CandidateRequest::Staged(pathspecs) => {
let mut scope_store: CandidateStore =
CandidateStore::new(real_git, global_prefix, private_overlay.as_slice());
let selected: Vec<CandidateRecord> = staged_scope(&mut scope_store, pathspecs.as_slice())?;
(scope_store, selected)
}
CandidateRequest::StagedAtBase { base, pathspecs } => {
let mut scope_store: CandidateStore =
CandidateStore::new(real_git, global_prefix, private_overlay.as_slice()).with_baseline(base.clone());
let selected: Vec<CandidateRecord> = if pathspecs.is_empty() {
Vec::new()
} else {
staged_scope(&mut scope_store, pathspecs.as_slice())?
};
(scope_store, selected)
}
};
let version: Rc<CandidateVersion> = store.derived_version(records);
return Ok(PreparedCandidates {
store,
version,
real_index,
attributes: CandidateAttributes::new(
real_git,
global_prefix,
private_overlay.as_slice(),
AttributeSource::Index,
),
_private: private,
});
}
#[cfg(test)]
#[path = "candidate_prediction_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "candidate_prediction_count_tests.rs"]
mod count_tests;