use std::collections::BTreeMap;
use std::sync::atomic::AtomicBool;
use super::similarity::{Budget, Content, spend};
use super::{Copies, Error, Kind, Limits, Options, Rewrite};
use crate::{EntryMode, ObjectId, ObjectKind, Objects, TreeChange, TreeValue};
impl Objects {
pub fn detect_rewrites(
&self,
old: Option<ObjectId>,
new: Option<ObjectId>,
options: Options,
limits: Limits,
targets: Option<&[&[u8]]>,
cancel: &AtomicBool,
) -> Result<Vec<Rewrite>, Error> {
let mut budget = Budget {
remaining: limits,
cancel,
};
budget.check()?;
if !(1..=100).contains(&options.similarity) {
return Err(Error::Similarity(options.similarity));
}
let changes = self.compare_trees(old, new, limits.trees, cancel)?;
let (sources, destinations) = candidates(&changes, options.copies, &mut budget)?;
let mut blobs = BTreeMap::new();
for candidate in sources.iter().chain(&destinations) {
budget.check()?;
if let std::collections::btree_map::Entry::Vacant(entry) =
blobs.entry(candidate.value.id)
{
entry.insert(read(self, candidate.value.id, &mut budget)?);
}
}
let mut selection = Selection::new(&sources, &destinations, options);
selection.exact(&blobs, &mut budget)?;
if options.similarity < 100 {
selection.edited(&mut blobs, &mut budget)?;
}
budget.check()?;
let mut records = selection.records;
if let Some(targets) = targets {
let mut selected = std::collections::BTreeSet::new();
for target in targets {
budget.work(
target
.len()
.saturating_mul((selected.len() + 1).ilog2() as usize + 1),
)?;
selected.insert(*target);
}
records.retain(|record| selected.contains(record.target.as_slice()));
}
records.sort_by(|a, b| a.target.cmp(&b.target));
budget.check()?;
Ok(records)
}
}
struct Candidate<'a> {
path: &'a [u8],
value: TreeValue,
source_id: ObjectId,
deleted: bool,
}
fn candidates<'a>(
changes: &'a [TreeChange],
copies: Copies,
budget: &mut Budget<'_>,
) -> Result<(Vec<Candidate<'a>>, Vec<Candidate<'a>>), Error> {
let mut sources = Vec::new();
let mut targets = Vec::new();
for change in changes {
budget.work(1)?;
if let Some(old) = change.old.filter(|value| is_file(value.mode))
&& (change.new.is_none()
|| (copies != Copies::Disabled && change.new.is_some_and(|v| is_file(v.mode))))
{
let value = if copies == Copies::ModifiedPostimage {
change.new.unwrap_or(old)
} else {
old
};
sources.push(Candidate {
path: &change.path,
value,
source_id: old.id,
deleted: change.new.is_none(),
});
}
if change.old.is_none()
&& let Some(new) = change.new.filter(|value| is_file(value.mode))
{
targets.push(Candidate {
path: &change.path,
value: new,
source_id: new.id,
deleted: false,
});
}
}
Ok((sources, targets))
}
fn is_file(mode: EntryMode) -> bool {
matches!(mode, EntryMode::Blob | EntryMode::Executable)
}
struct Blob {
bytes: Vec<u8>,
content: Option<Content>,
}
impl Blob {
fn binary(&self) -> bool {
self.bytes[..self.bytes.len().min(8000)].contains(&0)
}
}
fn read(objects: &Objects, id: ObjectId, budget: &mut Budget<'_>) -> Result<Blob, Error> {
let mut limits = budget.remaining.read;
limits.max_object_bytes = limits.max_object_bytes.min(budget.remaining.max_blob_bytes);
let object = objects
.read(id, limits)
.map_err(|source| Error::Read { id, source })?
.ok_or(Error::Missing(id))?;
if object.kind() != ObjectKind::Blob {
return Err(Error::NotBlob {
id,
actual: object.kind(),
});
}
let bytes = object.into_data();
spend(
&mut budget.remaining.max_blob_bytes,
bytes.len(),
"blob bytes",
)?;
budget.check()?;
Ok(Blob {
bytes,
content: None,
})
}
struct Selection<'a> {
sources: &'a [Candidate<'a>],
targets: &'a [Candidate<'a>],
used_sources: Vec<bool>,
used_targets: Vec<bool>,
options: Options,
records: Vec<Rewrite>,
}
impl<'a> Selection<'a> {
fn new(sources: &'a [Candidate<'a>], targets: &'a [Candidate<'a>], options: Options) -> Self {
Self {
sources,
targets,
used_sources: vec![false; sources.len()],
used_targets: vec![false; targets.len()],
options,
records: Vec::new(),
}
}
fn available(&self, source: usize) -> bool {
!self.used_sources[source] || self.options.copies != Copies::Disabled
}
fn exact(
&mut self,
blobs: &BTreeMap<ObjectId, Blob>,
budget: &mut Budget<'_>,
) -> Result<(), Error> {
let mut by_id: BTreeMap<ObjectId, Vec<usize>> = BTreeMap::new();
for (index, source) in self.sources.iter().enumerate() {
budget.work(1)?;
if self.options.track_empty || !blobs[&source.value.id].bytes.is_empty() {
by_id.entry(source.value.id).or_default().push(index);
}
}
for (target, destination) in self.targets.iter().enumerate() {
budget.work(1)?;
let Some(sources) = by_id.get(&destination.value.id) else {
continue;
};
let mut best = None;
for &source in sources {
budget.pair()?;
if !self.available(source) {
continue;
}
let candidate = &self.sources[source];
let rank = (
!candidate.deleted || self.used_sources[source],
basename(candidate.path) != basename(destination.path),
candidate.path,
);
if best.is_none_or(|(_, previous)| rank < previous) {
best = Some((source, rank));
}
}
if let Some((source, _)) = best {
self.select(source, target, 100);
}
}
Ok(())
}
fn edited(
&mut self,
blobs: &mut BTreeMap<ObjectId, Blob>,
budget: &mut Budget<'_>,
) -> Result<(), Error> {
let sources: Vec<_> = (0..self.sources.len())
.filter(|&s| {
let blob = &blobs[&self.sources[s].value.id];
self.available(s)
&& !blob.bytes.is_empty()
&& (self.options.approximate_binary || !blob.binary())
})
.collect();
let targets: Vec<_> = (0..self.targets.len())
.filter(|&t| {
let blob = &blobs[&self.targets[t].value.id];
!self.used_targets[t]
&& !blob.bytes.is_empty()
&& (self.options.approximate_binary || !blob.binary())
})
.collect();
if sources.is_empty() || targets.is_empty() {
return Ok(());
}
if sources.len().max(targets.len()) > self.options.candidate_limit {
return Err(Error::Candidates {
sources: sources.len(),
targets: targets.len(),
limit: self.options.candidate_limit,
});
}
for id in sources
.iter()
.map(|&s| self.sources[s].value.id)
.chain(targets.iter().map(|&t| self.targets[t].value.id))
{
let blob = blobs.get_mut(&id).expect("candidate blob was loaded");
if blob.content.is_none() {
blob.content = Some(Content::new(&blob.bytes, budget)?);
}
}
let mut pairs = Vec::new();
for source in sources {
for &target in &targets {
budget.pair()?;
let old = blobs[&self.sources[source].value.id]
.content
.as_ref()
.unwrap();
let new = blobs[&self.targets[target].value.id]
.content
.as_ref()
.unwrap();
if let Some(score) = old.score(new, self.options.similarity, budget)? {
pairs.push((source, target, score));
}
}
}
budget.check()?;
pairs.sort_by_key(|&(source, target, score)| {
(
std::cmp::Reverse(score),
basename(self.sources[source].path) != basename(self.targets[target].path),
self.sources[source].path,
self.targets[target].path,
)
});
budget.check()?;
for (source, target, score) in pairs {
budget.work(1)?;
if self.available(source) && !self.used_targets[target] {
self.select(source, target, score);
}
}
Ok(())
}
fn select(&mut self, source: usize, target: usize, similarity: u8) {
let old = &self.sources[source];
let new = &self.targets[target];
let kind = if old.deleted && !self.used_sources[source] {
Kind::Rename
} else {
Kind::Copy
};
self.records.push(Rewrite {
source: old.path.to_vec(),
target: new.path.to_vec(),
source_id: old.source_id,
target_id: new.value.id,
similarity,
kind,
});
self.used_sources[source] = true;
self.used_targets[target] = true;
}
}
fn basename(path: &[u8]) -> &[u8] {
path.rsplit(|&b| b == b'/').next().unwrap_or(path)
}