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> {
let mut needs_empty_base: bool = false;
for shared in candidates {
if shared.base.is_none() {
needs_empty_base = true;
break;
}
}
if !needs_empty_base {
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 match parse_indexed_patch(&output.stdout) {
Ok(hunks) => Ok(hunks),
Err(error) => Err(TextSubsumptionError::Format(error.0)),
};
}
fn blob_of(blobs: &HashMap<String, Vec<u8>>, entry: &Option<TreeEntry>) -> Option<Vec<u8>> {
let present: &TreeEntry = entry.as_ref()?;
return Some(blobs.get(&present.oid).cloned().unwrap_or_default());
}
fn base_side(shared: &SharedPath) -> &Option<TreeEntry> {
return &shared.base;
}
fn landed_side(shared: &SharedPath) -> &Option<TreeEntry> {
return &shared.landed;
}
fn prepared_side(shared: &SharedPath) -> &Option<TreeEntry> {
return &shared.prepared;
}
fn side_oids(owned: &[SharedPath], empty_blob: &str, side: fn(&SharedPath) -> &Option<TreeEntry>) -> Vec<String> {
let mut oids: Vec<String> = Vec::new();
for shared in owned {
oids.push(side_oid(side(shared), empty_blob));
}
return oids;
}
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 mut oids: Vec<String> = Vec::new();
for shared in candidates {
for entry in [&shared.base, &shared.landed, &shared.prepared] {
if let Some(present) = entry {
oids.push(present.oid.clone());
}
}
}
let blobs: HashMap<String, Vec<u8>> = load_blob_batch(context, cwd, &oids, Some(object_directory))
.map_err(TextSubsumptionError::Blob)?;
let mut owned: Vec<SharedPath> = Vec::new();
for shared in candidates {
let mut all_text: bool = true;
for entry in [&shared.base, &shared.landed, &shared.prepared] {
let is_text: bool = match entry {
None => true,
Some(present) => match blobs.get(&present.oid) {
Some(bytes) => !is_binary(bytes),
None => false,
},
};
if !is_text {
all_text = false;
break;
}
}
if all_text {
owned.push(shared.clone());
}
}
if owned.is_empty() {
return Ok(Vec::new());
}
let base_tree: String = indexed_tree(
context,
shadow,
cwd,
&side_oids(&owned, &empty_blob, base_side),
)?;
let landed_tree: String = indexed_tree(
context,
shadow,
cwd,
&side_oids(&owned, &empty_blob, landed_side),
)?;
let prepared_tree: String = indexed_tree(
context,
shadow,
cwd,
&side_oids(&owned, &empty_blob, prepared_side),
)?;
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(&blobs, &shared.prepared).unwrap_or_default();
let base_bytes: Vec<u8> = blob_of(&blobs, &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;