use crate::replay_patch::{PatchHunk, PatchLine};
#[derive(Clone, Debug, Eq, PartialEq)]
struct BaseChange {
start: usize,
end: usize,
added: Vec<Vec<u8>>,
}
fn base_change(hunk: &PatchHunk) -> BaseChange {
let removed: usize = hunk.lines.iter().filter(|line| { return line.op == b'-'; }).count();
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: hunk
.lines
.iter()
.filter(|line: &&PatchLine| { return line.op == b'+'; })
.map(|line| { return line.text.clone(); })
.collect(),
};
}
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 covered: Vec<(Option<usize>, BaseChange)> = landed
.iter()
.map(base_change)
.map(|change| {
let cover: Option<usize> = prepared_changes
.iter()
.position(|candidate| { return candidate.start <= change.start && change.end <= candidate.end; });
return (cover, change);
})
.collect();
if covered.iter().any(|(cover, _)| { return cover.is_none(); }) {
return false;
}
return prepared_changes.iter().enumerate().all(|(index, change)| {
let inside: Vec<BaseChange> = covered
.iter()
.filter(|(cover, _)| { return *cover == Some(index); })
.map(|(_, landed_change)| { return landed_change.clone(); })
.collect();
return inside.is_empty() || region_contains(change, &inside, base);
});
}
#[cfg(test)]
#[path = "replay_containment_tests.rs"]
mod tests;