use crate::blob_batch::load_blob_batch;
use crate::replay_containment::contains_landed_change;
use crate::replay_patch::{PatchHunk, parse_indexed_patch, split_keeping_newlines};
use crate::replay_reverse_apply::reverse_applies;
use crate::replay_shadow::run_shadow_checked;
use crate::replay_shared_paths::{SharedPath, TreeEntry};
use crate::transaction_git::{GitContext, GitFailure};
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
const BINARY_PROBE_BYTES: usize = 8_000;
const STRICT_CONTEXT: u32 = 3;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TextSubsumptionError {
Git(GitFailure),
Blob(String),
Format(String),
}
fn is_binary(bytes: &[u8]) -> bool {
return bytes[..bytes.len().min(BINARY_PROBE_BYTES)].contains(&0);
}
fn empty_blob_for(
context: &GitContext,
shadow: &Path,
cwd: &Path,
candidates: &[SharedPath],
) -> Result<String, TextSubsumptionError> {
if !candidates.iter().any(|shared| { return shared.base.is_none(); }) {
return Ok(String::new());
}
let output = run_shadow_checked(
context,
shadow,
cwd,
&["hash-object", "-w", "-t", "blob", "--stdin"],
Some(&[]),
None,
&[],
)
.map_err(TextSubsumptionError::Git)?;
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
fn side_oid(entry: &Option<TreeEntry>, empty_blob: &str) -> String {
return match entry {
Some(present) => present.oid.clone(),
None => String::from(empty_blob),
};
}
fn indexed_tree(
context: &GitContext,
shadow: &Path,
cwd: &Path,
oids: &[String],
) -> Result<String, TextSubsumptionError> {
let mut input: Vec<u8> = Vec::new();
for (index, oid) in oids.iter().enumerate() {
input.extend_from_slice(format!("100644 blob {oid}\t{index}\0").as_bytes());
}
let output = run_shadow_checked(context, shadow, cwd, &["mktree", "-z"], Some(&input), None, &[])
.map_err(TextSubsumptionError::Git)?;
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
fn indexed_hunks(
context: &GitContext,
shadow: &Path,
cwd: &Path,
from: &str,
to: &str,
lines_of_context: u32,
) -> Result<BTreeMap<Vec<u8>, Vec<PatchHunk>>, TextSubsumptionError> {
let unified: String = format!("--unified={lines_of_context}");
let output = run_shadow_checked(
context,
shadow,
cwd,
&[
"diff-tree",
"-p",
"--text",
unified.as_str(),
"--no-color",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--src-prefix=a/",
"--dst-prefix=b/",
from,
to,
],
None,
None,
&[],
)
.map_err(TextSubsumptionError::Git)?;
return parse_indexed_patch(&output.stdout)
.map_err(|error| { return TextSubsumptionError::Format(error.0); });
}
pub fn subsumed_text_paths(
context: &GitContext,
shadow: &Path,
cwd: &Path,
object_directory: &Path,
candidates: &[SharedPath],
) -> Result<Vec<String>, TextSubsumptionError> {
if candidates.is_empty() {
return Ok(Vec::new());
}
let empty_blob: String = empty_blob_for(context, shadow, cwd, candidates)?;
let oids: Vec<String> = candidates
.iter()
.flat_map(|shared| { return [&shared.base, &shared.landed, &shared.prepared]; })
.filter_map(|entry| { return entry.as_ref().map(|present| { return present.oid.clone(); }); })
.collect();
let blobs: HashMap<String, Vec<u8>> = load_blob_batch(context, cwd, &oids, Some(object_directory))
.map_err(TextSubsumptionError::Blob)?;
let blob_of = |entry: &Option<TreeEntry>| -> Option<Vec<u8>> {
return entry.as_ref().map(|present| { return blobs.get(&present.oid).cloned().unwrap_or_default(); });
};
let text: Vec<&SharedPath> = candidates
.iter()
.filter(|shared| {
return [&shared.base, &shared.landed, &shared.prepared].iter().all(|entry| {
let blob = entry.as_ref().map(|present| { return blobs.get(&present.oid); });
return match blob {
None => true,
Some(Some(bytes)) => !is_binary(bytes),
Some(None) => false,
};
});
})
.collect();
if text.is_empty() {
return Ok(Vec::new());
}
let owned: Vec<SharedPath> = text.iter().map(|shared| { return (*shared).clone(); }).collect();
let base_tree: String = indexed_tree(
context,
shadow,
cwd,
&owned.iter().map(|shared| { return side_oid(&shared.base, &empty_blob); }).collect::<Vec<String>>(),
)?;
let landed_tree: String = indexed_tree(
context,
shadow,
cwd,
&owned.iter().map(|shared| { return side_oid(&shared.landed, &empty_blob); }).collect::<Vec<String>>(),
)?;
let prepared_tree: String = indexed_tree(
context,
shadow,
cwd,
&owned.iter().map(|shared| { return side_oid(&shared.prepared, &empty_blob); }).collect::<Vec<String>>(),
)?;
let strict: BTreeMap<Vec<u8>, Vec<PatchHunk>> =
indexed_hunks(context, shadow, cwd, &base_tree, &landed_tree, STRICT_CONTEXT)?;
let landed_zero: BTreeMap<Vec<u8>, Vec<PatchHunk>> =
indexed_hunks(context, shadow, cwd, &base_tree, &landed_tree, 0)?;
let prepared_zero: BTreeMap<Vec<u8>, Vec<PatchHunk>> =
indexed_hunks(context, shadow, cwd, &base_tree, &prepared_tree, 0)?;
let mut subsumed: Vec<String> = Vec::new();
for (index, shared) in owned.iter().enumerate() {
let name: Vec<u8> = index.to_string().into_bytes();
let prepared_bytes: Vec<u8> = blob_of(&shared.prepared).unwrap_or_default();
let base_bytes: Vec<u8> = blob_of(&shared.base).unwrap_or_default();
let strict_hunks: &[PatchHunk] = strict.get(&name).map(Vec::as_slice).unwrap_or_default();
let landed_hunks: &[PatchHunk] = landed_zero.get(&name).map(Vec::as_slice).unwrap_or_default();
let prepared_hunks: &[PatchHunk] = prepared_zero.get(&name).map(Vec::as_slice).unwrap_or_default();
let base_lines: Vec<Vec<u8>> = split_keeping_newlines(&base_bytes);
if reverse_applies(strict_hunks, &prepared_bytes)
|| contains_landed_change(landed_hunks, prepared_hunks, &base_lines)
{
subsumed.push(shared.path.clone());
}
}
return Ok(subsumed);
}
#[cfg(test)]
#[path = "replay_subsumption_text_tests.rs"]
mod tests;