use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::ObjectId;
use crate::git_metadata::{MetadataOutput, path_from_git_bytes, run_metadata_git, strip_git_line};
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::path::PathBuf;
pub const ATTRIBUTE_CHUNK_SIZE: usize = crate::candidate_prediction::PATHSPEC_CHUNK_SIZE;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AttributeSource {
Index,
Tree(ObjectId),
PerPath(HashMap<Vec<u8>, ObjectId>),
}
#[derive(Clone, Debug)]
pub struct CandidateAttributes {
real_git: PathBuf,
global_prefix: Vec<OsString>,
overlay: Vec<(OsString, OsString)>,
source: AttributeSource,
}
fn attribute_failure(failure: CandidateFailure, detail: &str) -> CandidateError {
return CandidateError::new(
failure,
format!("cli-git could not read the text attribute of candidate files: {detail}").as_str(),
);
}
pub fn parse_text_unset(
output: &[u8],
asked: &[Vec<u8>],
paths: &[&[u8]],
) -> Result<Vec<Vec<u8>>, CandidateError> {
let mut fields: Vec<&[u8]> = output.split(|byte| { return *byte == 0; }).collect();
if fields.pop() != Some(b"".as_slice()) || fields.len() != asked.len() * 3 {
return Err(attribute_failure(
CandidateFailure::ListingMalformed,
"git check-attr did not print one record for each path it was asked about.",
));
}
let mut unset: Vec<Vec<u8>> = Vec::new();
for (position, record) in fields.chunks(3).enumerate() {
if record[0] != asked[position].as_slice() || record[1] != b"text" {
return Err(attribute_failure(
CandidateFailure::ListingMalformed,
"git check-attr printed a record for a different path or attribute than it was asked about.",
));
}
if record[2] == b"unset" {
unset.push(paths[position].to_vec());
}
}
return Ok(unset);
}
impl CandidateAttributes {
pub fn new(
real_git: &std::path::Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
source: AttributeSource,
) -> CandidateAttributes {
return CandidateAttributes {
real_git: real_git.to_path_buf(),
global_prefix: global_prefix.to_vec(),
overlay: overlay.to_vec(),
source,
};
}
fn run(&self, command: &[OsString]) -> Result<Vec<u8>, CandidateError> {
let mut arguments: Vec<OsString> = self.global_prefix.clone();
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(
self.real_git.as_path(),
arguments.as_slice(),
self.overlay.as_slice(),
) {
Ok(captured) => captured,
Err(error) => {
return Err(attribute_failure(
CandidateFailure::GitNotStarted,
format!("git {operation} could not be started: {error}.").as_str(),
));
}
};
if !output.success {
return Err(attribute_failure(
CandidateFailure::GitFailed,
format!(
"git {operation} failed: {}",
String::from_utf8_lossy(output.stderr.as_slice()).trim()
)
.as_str(),
));
}
return Ok(output.stdout);
}
fn top_level(&self) -> Result<Vec<u8>, CandidateError> {
let output: Vec<u8> = self.run(&[
OsString::from("rev-parse"),
OsString::from("--show-toplevel"),
])?;
let top: &[u8] = strip_git_line(output.as_slice());
if top.is_empty() {
return Err(attribute_failure(
CandidateFailure::ListingMalformed,
"git rev-parse --show-toplevel printed no path.",
));
}
return Ok(top.to_vec());
}
fn command(&self, tree: Option<&ObjectId>, names: &[Vec<u8>]) -> Result<Vec<OsString>, CandidateError> {
let mut command: Vec<OsString> = vec![
OsString::from("check-attr"),
OsString::from("-z"),
];
match tree {
Some(object) => command.push(OsString::from(format!("--source={}", object.as_str()))),
None => command.push(OsString::from("--cached")),
}
command.push(OsString::from("text"));
command.push(OsString::from("--"));
for name in names {
let Some(native) = path_from_git_bytes(name.as_slice()) else {
return Err(attribute_failure(
CandidateFailure::ListingMalformed,
"a candidate name is not a path on this platform.",
));
};
command.push(native.into_os_string());
}
return Ok(command);
}
fn groups<'a>(
&self,
paths: &[&'a [u8]],
) -> Result<Vec<(Option<ObjectId>, Vec<&'a [u8]>)>, CandidateError> {
match &self.source {
AttributeSource::Index => return Ok(vec![(None, paths.to_vec())]),
AttributeSource::Tree(tree) => return Ok(vec![(Some(tree.clone()), paths.to_vec())]),
AttributeSource::PerPath(trees) => {
let mut groups: Vec<(Option<ObjectId>, Vec<&'a [u8]>)> = Vec::new();
for path in paths {
let Some(tree) = trees.get(*path) else {
return Err(attribute_failure(
CandidateFailure::StaleCandidate,
"a candidate was not listed from any commit or tree.",
));
};
match groups.iter_mut().find(|group| { return group.0.as_ref() == Some(tree); }) {
Some(group) => group.1.push(*path),
None => groups.push((Some(tree.clone()), vec![path])),
}
}
return Ok(groups);
}
}
}
pub fn text_unset(&self, paths: &[&[u8]]) -> Result<HashSet<Vec<u8>>, CandidateError> {
let mut unset: HashSet<Vec<u8>> = HashSet::new();
if paths.is_empty() {
return Ok(unset);
}
let top: Vec<u8> = self.top_level()?;
for (tree, group) in self.groups(paths)? {
for chunk in group.chunks(ATTRIBUTE_CHUNK_SIZE) {
let asked: Vec<Vec<u8>> = chunk
.iter()
.map(|path| { return [top.as_slice(), b"/", *path].concat(); })
.collect();
let command: Vec<OsString> = self.command(tree.as_ref(), asked.as_slice())?;
let output: Vec<u8> = self.run(command.as_slice())?;
unset.extend(parse_text_unset(output.as_slice(), asked.as_slice(), chunk)?);
}
}
return Ok(unset);
}
}
#[cfg(test)]
#[path = "candidate_attribute_tests.rs"]
mod tests;