use crate::replay_patch::PatchHunk;
#[derive(Clone, Debug, Eq, PartialEq)]
struct BaseChange {
start: usize,
end: usize,
added: Vec<Vec<u8>>,
}
fn base_change(hunk: &PatchHunk) -> BaseChange {
let mut removed: usize = 0;
let mut added: Vec<Vec<u8>> = Vec::new();
for line in &hunk.lines {
if line.op == b'-' {
removed += 1;
}
if line.op == b'+' {
added.push(line.text.clone());
}
}
let start: usize = if removed == 0 {
usize::try_from(hunk.old_start).unwrap_or(0)
} else {
usize::try_from(hunk.old_start - 1).unwrap_or(0)
};
return BaseChange {
start,
end: start + removed,
added,
};
}
fn is_subsequence(lines: &[Vec<u8>], within: &[Vec<u8>]) -> bool {
let mut matched: usize = 0;
for line in within {
if matched < lines.len() && lines[matched] == *line {
matched += 1;
}
}
return matched == lines.len();
}
fn region_contains(prepared: &BaseChange, landed: &[BaseChange], base: &[Vec<u8>]) -> bool {
let (Some(first), Some(last)) = (landed.first(), landed.last()) else {
return false;
};
let mut expected: Vec<Vec<u8>> = Vec::new();
for (index, change) in landed.iter().enumerate() {
expected.extend(change.added.iter().cloned());
if let Some(next) = landed.get(index + 1) {
expected.extend(base.get(change.end..next.start).unwrap_or_default().iter().cloned());
}
}
let anchored_start: bool = prepared.start == first.start;
let anchored_end: bool = prepared.end == last.end;
if expected.is_empty() && anchored_start && anchored_end {
return prepared.added.is_empty();
}
if expected.len() > prepared.added.len() {
return false;
}
if anchored_start && anchored_end {
let ends_match: bool = prepared.added.first() == expected.first()
|| prepared.added.last() == expected.last();
return ends_match && is_subsequence(&expected, &prepared.added);
}
let prefix_matches: bool = anchored_start
&& prepared.added[..expected.len()] == expected[..];
let suffix_start: usize = prepared.added.len() - expected.len();
let suffix_matches: bool = anchored_end && prepared.added[suffix_start..] == expected[..];
return prefix_matches || suffix_matches;
}
pub fn contains_landed_change(
landed: &[PatchHunk],
prepared: &[PatchHunk],
base: &[Vec<u8>],
) -> bool {
let prepared_changes: Vec<BaseChange> = prepared.iter().map(base_change).collect();
let mut covered: Vec<(Option<usize>, BaseChange)> = Vec::new();
for hunk in landed {
let change: BaseChange = base_change(hunk);
let mut cover: Option<usize> = None;
for (index, candidate) in prepared_changes.iter().enumerate() {
if candidate.start <= change.start && change.end <= candidate.end {
cover = Some(index);
break;
}
}
covered.push((cover, change));
}
for (cover, _) in &covered {
if cover.is_none() {
return false;
}
}
for (index, change) in prepared_changes.iter().enumerate() {
let mut inside: Vec<BaseChange> = Vec::new();
for (cover, landed_change) in &covered {
if *cover == Some(index) {
inside.push(landed_change.clone());
}
}
if !(inside.is_empty() || region_contains(change, &inside, base)) {
return false;
}
}
return true;
}
#[cfg(test)]
#[path = "replay_containment_tests.rs"]
mod tests;