use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::{CandidateMode, ObjectId, parse_object_id};
use crate::candidate_reader::{ObjectReader, start_object_reader};
use crate::candidate_record::{CandidateRecord, parse_raw_records};
use crate::candidate_stage::{StageRecord, parse_stage_records};
use crate::candidate_tree::{TreeRecord, parse_tree_records};
use crate::candidate_version::{Candidate, CandidateSource, CandidateVersion, build_version};
use crate::git_metadata::{MetadataOutput, run_metadata_git, strip_git_line};
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::rc::Rc;
pub struct CandidateStore {
real_git: PathBuf,
global_prefix: Vec<OsString>,
overlay: Vec<(OsString, OsString)>,
reader: Option<ObjectReader>,
generation: u64,
versions: Vec<(CandidateSource, Rc<CandidateVersion>)>,
baseline: Option<Option<ObjectId>>,
}
static LISTED: std::sync::Mutex<Option<(ObjectId, std::sync::Arc<Vec<TreeRecord>>)>> = std::sync::Mutex::new(None);
const COMMIT_CHUNK_SIZE: usize = crate::candidate_prediction::PATHSPEC_CHUNK_SIZE;
fn listing_failure(failure: CandidateFailure, operation: &str, detail: &str) -> CandidateError {
return CandidateError::new(
failure,
format!("cli-git could not list candidate files: git {operation} {detail}").as_str(),
);
}
impl CandidateStore {
pub fn new(
real_git: &Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
) -> CandidateStore {
return CandidateStore {
real_git: real_git.to_path_buf(),
global_prefix: global_prefix.to_vec(),
overlay: overlay.to_vec(),
reader: None,
generation: 0,
versions: Vec::new(),
baseline: None,
};
}
#[must_use]
pub fn with_baseline(mut self, baseline: Option<ObjectId>) -> CandidateStore {
self.baseline = Some(baseline);
return self;
}
fn baseline_commit(&mut self) -> Result<Option<ObjectId>, CandidateError> {
if let Some(recorded) = &self.baseline {
return Ok(recorded.clone());
}
return self.reader()?.head_commit();
}
fn reader(&mut self) -> Result<&mut ObjectReader, CandidateError> {
if self.reader.is_none() {
let started: ObjectReader = start_object_reader(
self.real_git.as_path(),
self.global_prefix.as_slice(),
self.overlay.as_slice(),
)?;
self.reader = Some(started);
}
let Some(reader) = self.reader.as_mut() else {
return Err(CandidateError::new(
CandidateFailure::ReaderEnded,
"cli-git could not read a Git object: the object reader was not started.",
));
};
return Ok(reader);
}
fn run_listing(
&self,
operation: &str,
command: &[OsString],
) -> Result<Vec<u8>, CandidateError> {
let mut arguments: Vec<OsString> = self.global_prefix.clone();
arguments.extend_from_slice(command);
let output: MetadataOutput = match run_metadata_git(
self.real_git.as_path(),
arguments.as_slice(),
self.overlay.as_slice(),
) {
Ok(captured) => captured,
Err(error) => {
return Err(listing_failure(
CandidateFailure::GitNotStarted,
operation,
format!("could not be started: {error}.").as_str(),
));
}
};
if !output.success {
return Err(listing_failure(
CandidateFailure::GitFailed,
operation,
format!(
"failed: {}",
String::from_utf8_lossy(output.stderr.as_slice()).trim()
)
.as_str(),
));
}
return Ok(output.stdout);
}
fn empty_tree(&self) -> Result<ObjectId, CandidateError> {
let output: Vec<u8> = self.run_listing(
"hash-object",
&[
OsString::from("hash-object"),
OsString::from("-t"),
OsString::from("tree"),
OsString::from("--stdin"),
],
)?;
let Some(tree) = parse_object_id(strip_git_line(output.as_slice())) else {
return Err(listing_failure(
CandidateFailure::ListingMalformed,
"hash-object",
"did not print the empty tree's object name.",
));
};
return Ok(tree);
}
fn list_staged(
&self,
baseline: Option<&ObjectId>,
pathspecs: &[OsString],
) -> Result<Vec<u8>, CandidateError> {
let tree: ObjectId = match baseline {
Some(commit) => commit.clone(),
None => self.empty_tree()?,
};
let mut command: Vec<OsString> = vec![
OsString::from("diff-index"),
OsString::from("--cached"),
OsString::from("--raw"),
OsString::from("-z"),
OsString::from("--no-renames"),
OsString::from("--no-abbrev"),
OsString::from(tree.as_str()),
OsString::from("--"),
];
command.extend_from_slice(pathspecs);
return self.run_listing("diff-index", command.as_slice());
}
fn list_committed(&self, commit: &ObjectId) -> Result<Vec<u8>, CandidateError> {
return self.run_listing(
"diff-tree",
&[
OsString::from("diff-tree"),
OsString::from("--root"),
OsString::from("--no-commit-id"),
OsString::from("-r"),
OsString::from("-z"),
OsString::from("-m"),
OsString::from("--no-renames"),
OsString::from("--no-abbrev"),
OsString::from(commit.as_str()),
],
);
}
pub fn version(
&mut self,
source: &CandidateSource,
) -> Result<Rc<CandidateVersion>, CandidateError> {
for (known, version) in &self.versions {
if known == source {
return Ok(Rc::clone(version));
}
}
let output: Vec<u8> = match source {
CandidateSource::StagedAgainstHead => {
let head: Option<ObjectId> = self.baseline_commit()?;
self.list_staged(head.as_ref(), &[])?
}
CandidateSource::StagedAgainstCommit(commit) => self.list_staged(Some(commit), &[])?,
CandidateSource::Committed(commit) => self.list_committed(commit)?,
};
let records: Vec<CandidateRecord> = parse_raw_records(output.as_slice())?;
let version: Rc<CandidateVersion> = Rc::new(build_version(self.generation, records));
self.versions.push((source.clone(), Rc::clone(&version)));
return Ok(version);
}
pub fn stage_records(
&mut self,
pathspecs: &[OsString],
) -> Result<Vec<StageRecord>, CandidateError> {
let mut command: Vec<OsString> = vec![
OsString::from("ls-files"),
OsString::from("--stage"),
OsString::from("-z"),
OsString::from("--full-name"),
OsString::from("--"),
];
command.extend_from_slice(pathspecs);
let output: Vec<u8> = self.run_listing("ls-files", command.as_slice())?;
return parse_stage_records(output.as_slice());
}
pub fn head_records(
&mut self,
pathspecs: &[OsString],
) -> Result<Vec<CandidateRecord>, CandidateError> {
let head: Option<ObjectId> = self.baseline_commit()?;
let output: Vec<u8> = self.list_staged(head.as_ref(), pathspecs)?;
return parse_raw_records(output.as_slice());
}
pub fn head_tree_records(&mut self) -> Result<Vec<TreeRecord>, CandidateError> {
let Some(head) = self.baseline_commit()? else {
return Ok(Vec::new());
};
return self.commit_tree_records(&head);
}
pub fn commit_tree_records(
&mut self,
commit: &ObjectId,
) -> Result<Vec<TreeRecord>, CandidateError> {
if let Some((listed, records)) = LISTED.lock().ok().and_then(|held| { return held.clone(); })
&& listed == *commit
{
return Ok(records.as_ref().clone());
}
let records: Vec<TreeRecord> = self.list_commit_tree(commit)?;
if let Ok(mut held) = LISTED.lock() {
*held = Some((commit.clone(), std::sync::Arc::new(records.clone())));
}
return Ok(records);
}
fn list_commit_tree(&mut self, commit: &ObjectId) -> Result<Vec<TreeRecord>, CandidateError> {
let output: Vec<u8> = self.run_listing(
"ls-tree",
&[
OsString::from("ls-tree"),
OsString::from("-r"),
OsString::from("-z"),
OsString::from("--full-tree"),
OsString::from(commit.as_str()),
],
)?;
return parse_tree_records(output.as_slice());
}
pub fn baseline_reference(&mut self) -> Result<Option<ObjectId>, CandidateError> {
return self.baseline_commit();
}
pub fn parents_outside(
&mut self,
commits: &[ObjectId],
) -> Result<Vec<ObjectId>, CandidateError> {
let given: HashSet<&ObjectId> = commits.iter().collect();
let mut seen: HashSet<ObjectId> = HashSet::new();
let mut outside: Vec<ObjectId> = Vec::new();
for chunk in commits.chunks(COMMIT_CHUNK_SIZE) {
let mut command: Vec<OsString> = vec![
OsString::from("rev-list"),
OsString::from("--no-walk=unsorted"),
OsString::from("--parents"),
];
for commit in chunk {
command.push(OsString::from(commit.as_str()));
}
let output: Vec<u8> = self.run_listing("rev-list", command.as_slice())?;
for line in output.split(|byte| { return *byte == b'\n'; }) {
for name in line.split(|byte| { return *byte == b' '; }).skip(1) {
let Some(parent) = parse_object_id(name) else {
return Err(listing_failure(
CandidateFailure::ListingMalformed,
"rev-list",
"printed a parent that is not an object name.",
));
};
if !given.contains(&parent) && seen.insert(parent.clone()) {
outside.push(parent);
}
}
}
}
return Ok(outside);
}
pub fn derived_version(&mut self, records: Vec<CandidateRecord>) -> Rc<CandidateVersion> {
return Rc::new(build_version(self.generation, records));
}
pub fn committed_records(
&mut self,
commit: &ObjectId,
) -> Result<Vec<CandidateRecord>, CandidateError> {
let output: Vec<u8> = self.list_committed(commit)?;
return parse_raw_records(output.as_slice());
}
pub fn tree_records(
&mut self,
tree: &ObjectId,
) -> Result<Vec<CandidateRecord>, CandidateError> {
let listing: Vec<u8> = self.run_listing(
"ls-tree",
&[
OsString::from("ls-tree"),
OsString::from("--full-tree"),
OsString::from("-r"),
OsString::from("-z"),
OsString::from(tree.as_str()),
],
)?;
let raw: Vec<u8> = added_raw_from_ls_tree(listing.as_slice())?;
return parse_raw_records(raw.as_slice());
}
pub fn bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError> {
if candidate.identity.generation != self.generation {
return Err(CandidateError::new(
CandidateFailure::StaleCandidate,
"cli-git refused to read a candidate from a version that was invalidated by a fix or replay; \
list the version again and use its candidates.",
));
}
let Some(object) = candidate.object.as_ref() else {
return Ok(Rc::from(Vec::new()));
};
if candidate.mode == CandidateMode::Gitlink {
return Ok(Rc::from(object.as_str().as_bytes()));
}
return self.reader()?.blob(object);
}
pub fn invalidate(&mut self) {
self.versions.clear();
self.generation += 1;
}
}
#[cfg(test)]
#[path = "candidate_store_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "candidate_store_lifecycle_tests.rs"]
mod lifecycle_tests;
#[cfg(test)]
#[path = "candidate_process_count_tests.rs"]
mod process_count_tests;
#[cfg(test)]
#[path = "candidate_store_baseline_tests.rs"]
mod baseline_tests;
#[cfg(test)]
#[path = "candidate_store_failure_tests.rs"]
mod failure_tests;
fn added_raw_from_ls_tree(output: &[u8]) -> Result<Vec<u8>, CandidateError> {
let mut raw: Vec<u8> = Vec::new();
for record in output
.split(|byte| {
return *byte == 0;
})
.filter(|record| {
return !record.is_empty();
})
{
let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
return Err(CandidateError::new(
crate::candidate_error::CandidateFailure::ListingMalformed,
"cli-git could not list the pushed tree: an entry lacks its path separator.",
));
};
let metadata = String::from_utf8_lossy(&record[..tab]);
let path: &[u8] = &record[tab + 1..];
let fields: Vec<&str> = metadata.split(' ').collect();
if fields.len() != 3 {
return Err(CandidateError::new(
crate::candidate_error::CandidateFailure::ListingMalformed,
"cli-git could not list the pushed tree: an entry lacks its mode, type or object name.",
));
}
if fields[1] != "blob" {
continue;
}
let zeros = "0".repeat(fields[2].len());
raw.extend_from_slice(format!(":000000 {} {zeros} {} A", fields[0], fields[2]).as_bytes());
raw.push(0);
raw.extend_from_slice(path);
raw.push(0);
}
return Ok(raw);
}
#[cfg(test)]
#[path = "candidate_store_tree_tests.rs"]
mod tree_tests;