use crate::standards_review_input::{LandedCommit, MAX_EXCERPT_FILES};
use crate::transaction_git::{GitContext, GitRequest, run_git};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::Path;
pub const MAX_PATCH_BYTES: usize = 1 << 26;
fn is_line_feed_byte(byte: &u8) -> bool {
return *byte == b'\n';
}
fn is_nul_byte(byte: &u8) -> bool {
return *byte == 0;
}
fn read_patch(context: &GitContext, cwd: &Path, sha: &str) -> Result<String, String> {
let request: GitRequest = GitRequest::new(cwd, &["show", "--format=", "--patch", "--no-color", "--no-ext-diff", sha]);
let output = match run_git(context, &request) {
Ok(done) => done,
Err(error) => return Err(format!("starting git show failed: {error}")),
};
if !output.succeeded() {
return Err(format!("git show {sha} failed: {}", output.error_text()));
}
if output.stdout.len() > MAX_PATCH_BYTES {
return Err(format!("the patch of {sha} is over {MAX_PATCH_BYTES} bytes"));
}
return Ok(String::from_utf8_lossy(output.stdout.as_slice()).into_owned());
}
pub fn parse_batch(output: &[u8], names: &[String]) -> Result<BTreeMap<String, String>, String> {
let mut texts: BTreeMap<String, String> = BTreeMap::new();
let mut offset: usize = 0;
for name in names {
let Some(relative) = output[offset..].iter().position(is_line_feed_byte) else {
return Err(format!("git cat-file omitted the header for {name}"));
};
let header: String = String::from_utf8_lossy(&output[offset..offset + relative]).into_owned();
offset += relative + 1;
let fields: Vec<&str> = header.split(' ').collect();
if fields.len() == 2 && fields[1] == "missing" {
continue;
}
let announced_size: Option<usize> = match fields.get(2) {
Some(text) => text.parse::<usize>().ok(),
None => None,
};
let size: usize = match (fields.len(), fields.get(1), announced_size) {
(3, Some(&"blob"), Some(size)) => size,
_ => return Err(format!("git cat-file returned an unexpected header for {name}: {header}")),
};
let end: usize = match offset.checked_add(size) {
Some(end) if end < output.len() && output[end] == b'\n' => end,
_ => return Err(format!("git cat-file returned truncated content for {name}")),
};
texts.insert(name.clone(), String::from_utf8_lossy(&output[offset..end]).into_owned());
offset = end + 1;
}
return Ok(texts);
}
pub fn read_names(context: &GitContext, cwd: &Path, sha: &str) -> Result<Vec<String>, String> {
let request: GitRequest = GitRequest::new(cwd, &["diff-tree", "--no-commit-id", "--name-only", "-r", "-z", sha]);
let output = match run_git(context, &request) {
Ok(done) => done,
Err(error) => return Err(format!("starting git diff-tree failed: {error}")),
};
if !output.succeeded() {
return Err(format!("git diff-tree {sha} failed: {}", output.error_text()));
}
let mut names: Vec<String> = Vec::new();
for name in output.stdout.split(is_nul_byte) {
if !name.is_empty() {
names.push(String::from_utf8_lossy(name).into_owned());
}
}
return Ok(names);
}
pub fn read_message(context: &GitContext, cwd: &Path, sha: &str) -> Result<String, String> {
let request: GitRequest = GitRequest::new(cwd, &["show", "--no-patch", "--format=%B", sha]);
let output = match run_git(context, &request) {
Ok(done) => done,
Err(error) => return Err(format!("starting git show failed: {error}")),
};
if !output.succeeded() {
return Err(format!("git show {sha} failed: {}", output.error_text()));
}
return Ok(String::from_utf8_lossy(output.stdout.as_slice()).into_owned());
}
pub fn read_landed_commit(context: &GitContext, cwd: &Path, sha: &str, subject: &str, code_files: &[String]) -> Result<LandedCommit, String> {
let patch: String = read_patch(context, cwd, sha)?;
let wanted: Vec<String> = code_files.iter().take(MAX_EXCERPT_FILES).cloned().collect();
let mut specs: String = String::new();
for name in &wanted {
specs.push_str(format!("{sha}:{name}\n").as_str());
}
let mut request: GitRequest = GitRequest::new(cwd, &[OsString::from("cat-file"), OsString::from("--batch")]);
request.input = Some(specs.into_bytes());
let output = match run_git(context, &request) {
Ok(done) => done,
Err(error) => return Err(format!("starting git cat-file failed: {error}")),
};
if !output.succeeded() {
return Err(format!("git cat-file failed: {}", output.error_text()));
}
let contents: BTreeMap<String, String> = parse_batch(output.stdout.as_slice(), wanted.as_slice())?;
return Ok(LandedCommit { sha: String::from(sha), subject: String::from(subject), patch, contents });
}
#[cfg(test)]
#[path = "standards_review_source_tests.rs"]
mod tests;