use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resolution {
Ours,
Theirs,
Both,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hunk {
pub start: usize,
pub base: Option<usize>,
pub sep: usize,
pub end: usize,
}
impl Hunk {
pub fn contains(&self, row: usize) -> bool {
row >= self.start && row <= self.end
}
pub fn ours(&self) -> Range<usize> {
(self.start + 1)..self.base.unwrap_or(self.sep)
}
pub fn theirs(&self) -> Range<usize> {
(self.sep + 1)..self.end
}
pub fn base_region(&self) -> Option<Range<usize>> {
self.base.map(|b| (b + 1)..self.sep)
}
pub fn replacement(&self, lines: &[String], res: Resolution) -> Vec<String> {
match res {
Resolution::Ours => lines[self.ours()].to_vec(),
Resolution::Theirs => lines[self.theirs()].to_vec(),
Resolution::Both => {
let mut v = lines[self.ours()].to_vec();
v.extend_from_slice(&lines[self.theirs()]);
v
}
Resolution::None => Vec::new(),
}
}
}
fn is_start(l: &str) -> bool {
l.starts_with("<<<<<<<")
}
fn is_base(l: &str) -> bool {
l.starts_with("|||||||")
}
fn is_sep(l: &str) -> bool {
l.starts_with("=======")
}
fn is_end(l: &str) -> bool {
l.starts_with(">>>>>>>")
}
pub fn hunks(lines: &[String]) -> Vec<Hunk> {
let mut out = Vec::new();
let mut i = 0;
while i < lines.len() {
if is_start(&lines[i])
&& let Some((h, next)) = parse_one(lines, i)
{
out.push(h);
i = next;
continue;
}
i += 1;
}
out
}
fn parse_one(lines: &[String], start: usize) -> Option<(Hunk, usize)> {
let mut base = None;
let mut sep: Option<usize> = None;
let mut j = start + 1;
while j < lines.len() {
let l = &lines[j];
if is_start(l) {
return None;
}
match sep {
None => {
if base.is_none() && is_base(l) {
base = Some(j);
} else if is_sep(l) {
sep = Some(j);
}
}
Some(sep) if is_end(l) => {
return Some((
Hunk {
start,
base,
sep,
end: j,
},
j + 1,
));
}
Some(_) => {}
}
j += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(s: &str) -> Vec<String> {
s.split('\n').map(str::to_string).collect()
}
#[test]
fn parses_a_basic_conflict() {
let l = lines(
"keep\n\
<<<<<<< ours\n\
mine\n\
=======\n\
yours\n\
>>>>>>> theirs\n\
tail",
);
let hs = hunks(&l);
assert_eq!(hs.len(), 1);
let h = hs[0];
assert_eq!((h.start, h.sep, h.end), (1, 3, 5));
assert_eq!(h.base, None);
assert_eq!(&l[h.ours()], &["mine".to_string()]);
assert_eq!(&l[h.theirs()], &["yours".to_string()]);
}
#[test]
fn parses_diff3_base_region() {
let l = lines(
"<<<<<<< ours\n\
mine\n\
||||||| base\n\
orig\n\
=======\n\
yours\n\
>>>>>>> theirs",
);
let h = hunks(&l)[0];
assert_eq!(h.base, Some(2));
assert_eq!(h.sep, 4);
assert_eq!(&l[h.ours()], &["mine".to_string()]);
assert_eq!(
l[h.base_region().unwrap()].to_vec(),
vec!["orig".to_string()]
);
assert_eq!(&l[h.theirs()], &["yours".to_string()]);
}
#[test]
fn finds_multiple_conflicts() {
let l = lines(
"<<<<<<<\na\n=======\nb\n>>>>>>>\n\
mid\n\
<<<<<<<\nc\n=======\nd\n>>>>>>>",
);
let hs = hunks(&l);
assert_eq!(hs.len(), 2);
assert_eq!(hs[0].start, 0);
assert_eq!(hs[1].start, 6);
}
#[test]
fn ignores_unterminated_conflict() {
let l = lines("<<<<<<<\nmine\n=======\nyours\nno end here");
assert!(hunks(&l).is_empty());
}
#[test]
fn ignores_stray_separator_and_end() {
let l = lines("Title\n=======\nbody\n>>>>>>> not a marker");
assert!(hunks(&l).is_empty());
}
#[test]
fn nested_opener_restarts() {
let l = lines("<<<<<<<\nouter\n<<<<<<<\ninner\n=======\nyours\n>>>>>>>");
let hs = hunks(&l);
assert_eq!(hs.len(), 1);
assert_eq!(hs[0].start, 2);
}
#[test]
fn replacement_keeps_requested_side() {
let l = lines("<<<<<<<\nmine\n=======\nyours\n>>>>>>>");
let h = hunks(&l)[0];
assert_eq!(
h.replacement(&l, Resolution::Ours),
vec!["mine".to_string()]
);
assert_eq!(
h.replacement(&l, Resolution::Theirs),
vec!["yours".to_string()]
);
assert_eq!(
h.replacement(&l, Resolution::Both),
vec!["mine".to_string(), "yours".to_string()]
);
assert!(h.replacement(&l, Resolution::None).is_empty());
}
#[test]
fn both_drops_diff3_base() {
let l = lines("<<<<<<<\nmine\n|||||||\norig\n=======\nyours\n>>>>>>>");
let h = hunks(&l)[0];
assert_eq!(
h.replacement(&l, Resolution::Both),
vec!["mine".to_string(), "yours".to_string()]
);
}
}