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::{MetadataOutput, path_from_git_bytes, run_metadata_git, strip_git_line};
use crate::git_system_config::with_real_git;
use crate::repo_read_layout::{git_path, read_layout};
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";
pub const PATHSPEC_CHUNK_SIZE: usize = 2_048;
#[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 command_failure(purpose: &str, operation: &str, output: &MetadataOutput) -> CandidateError {
return CandidateError::new(
CandidateFailure::GitFailed,
format!(
"cli-git could not {purpose}: git {operation} failed: {}",
String::from_utf8_lossy(output.stderr.as_slice()).trim()
)
.as_str(),
);
}
fn not_started(purpose: &str, operation: &str, error: &std::io::Error) -> CandidateError {
return CandidateError::new(
CandidateFailure::GitNotStarted,
format!("cli-git could not {purpose}: git {operation} could not be started: {error}.")
.as_str(),
);
}
fn run_git(
real_git: &Path,
prefix: &[OsString],
command: &[OsString],
overlay: &[(OsString, OsString)],
purpose: &str,
) -> Result<MetadataOutput, CandidateError> {
let mut arguments: Vec<OsString> = prefix.to_vec();
arguments.extend_from_slice(command);
let operation: String = match command.first() {
Some(word) => word.to_string_lossy().into_owned(),
None => String::from("(no command)"),
};
let output: MetadataOutput = match run_metadata_git(real_git, arguments.as_slice(), overlay) {
Ok(captured) => captured,
Err(error) => return Err(not_started(purpose, operation.as_str(), &error)),
};
if !output.success {
return Err(command_failure(purpose, operation.as_str(), &output));
}
return Ok(output);
}
fn real_index_path(
real_git: &Path,
prefix: &[OsString],
overlay: &[(OsString, OsString)],
purpose: &str,
) -> Result<PathBuf, CandidateError> {
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"); }) {
return Ok(path);
}
let output: MetadataOutput = run_git(
real_git,
prefix,
&[
OsString::from("rev-parse"),
OsString::from("--path-format=absolute"),
OsString::from("--git-path"),
OsString::from("index"),
],
overlay,
purpose,
)?;
match path_from_git_bytes(strip_git_line(output.stdout.as_slice())) {
Some(path) => return Ok(path),
None => {
return Err(CandidateError::new(
CandidateFailure::ListingMalformed,
format!(
"cli-git could not {purpose}: git rev-parse --git-path index printed no path."
)
.as_str(),
));
}
}
}
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,
"cli-git 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 {
listed_from
.entry(record.path.clone())
.or_insert_with(|| { return 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;