use std::collections::{BTreeMap, BTreeSet};
use merge::{ConflictMarkers, MergeOutcome, text_hunk_merge_with_markers};
use super::items::{FileSegments, Item, ItemKey, ItemKind, inter_ranges};
const N_SIDES: usize = 3;
type MatchKey = (ItemKey, usize);
pub(crate) fn reconstruct_merged_file(
base: &str,
ours: &str,
theirs: &str,
base_segments: &FileSegments,
ours_segments: &FileSegments,
theirs_segments: &FileSegments,
markers: ConflictMarkers<'_>,
) -> MergeOutcome {
let sides = SideSources::new(base, ours, theirs);
let (mut output, total_conflicts) = merge_region(
sides,
&base_segments.items,
&ours_segments.items,
&theirs_segments.items,
(0, base_segments.source_len),
(0, ours_segments.source_len),
(0, theirs_segments.source_len),
markers,
);
reconcile_trailing_newline(&mut output, sides);
if total_conflicts == 0 {
MergeOutcome::Clean(output)
} else {
MergeOutcome::Conflicts {
merged_bytes_with_markers: output,
conflict_count: total_conflicts,
}
}
}
#[allow(clippy::too_many_arguments)]
fn merge_region(
sides: SideSources<'_>,
base_items: &[Item],
ours_items: &[Item],
theirs_items: &[Item],
base_bounds: (usize, usize),
ours_bounds: (usize, usize),
theirs_bounds: (usize, usize),
markers: ConflictMarkers<'_>,
) -> (Vec<u8>, usize) {
let (base_mks, ours_mks, theirs_mks) =
build_aligned_match_keys(base_items, ours_items, theirs_items, sides);
let base_map: BTreeMap<MatchKey, &Item> = base_mks
.iter()
.zip(base_items.iter())
.map(|(mk, i)| (mk.clone(), i))
.collect();
let ours_map: BTreeMap<MatchKey, &Item> = ours_mks
.iter()
.zip(ours_items.iter())
.map(|(mk, i)| (mk.clone(), i))
.collect();
let theirs_map: BTreeMap<MatchKey, &Item> = theirs_mks
.iter()
.zip(theirs_items.iter())
.map(|(mk, i)| (mk.clone(), i))
.collect();
let all_keys: BTreeSet<&MatchKey> = base_map
.keys()
.chain(ours_map.keys())
.chain(theirs_map.keys())
.collect();
let mut resolved: BTreeMap<MatchKey, (Option<Vec<u8>>, usize)> = BTreeMap::new();
let mut total_conflicts = 0usize;
for key in &all_keys {
if key.0.kind == ItemKind::Use {
continue;
}
let resolution = resolve_node(
sides,
base_map.get(*key).copied(),
ours_map.get(*key).copied(),
theirs_map.get(*key).copied(),
markers,
);
total_conflicts += resolution.1;
resolved.insert((*key).clone(), resolution);
}
let mut use_components: BTreeMap<ItemKey, [Vec<&Item>; 3]> = BTreeMap::new();
for (side, items) in [base_items, ours_items, theirs_items].iter().enumerate() {
for item in *items {
if item.key.kind == ItemKind::Use {
use_components
.entry(item.key.clone())
.or_insert_with(|| [Vec::new(), Vec::new(), Vec::new()])[side]
.push(item);
}
}
}
for (key, [base_uses, ours_uses, theirs_uses]) in &use_components {
let (bytes, conflicts) =
resolve_use_component(sides, base_uses, ours_uses, theirs_uses, markers);
total_conflicts += conflicts;
resolved.insert((key.clone(), 0), (bytes, conflicts));
let slots = base_uses.len().max(ours_uses.len()).max(theirs_uses.len());
for occ in 1..slots {
resolved.insert((key.clone(), occ), (None, 0));
}
}
let item_emit_order = compute_item_emit_order(&base_mks, &ours_mks, &theirs_mks, &all_keys);
let side_idx_maps = [
match_key_index(&base_mks),
match_key_index(&ours_mks),
match_key_index(&theirs_mks),
];
let side_ranges = [
inter_ranges(base_items, base_bounds.0, base_bounds.1),
inter_ranges(ours_items, ours_bounds.0, ours_bounds.1),
inter_ranges(theirs_items, theirs_bounds.0, theirs_bounds.1),
];
let side_sources = [sides.base, sides.ours, sides.theirs];
let mut emitted: [BTreeSet<usize>; N_SIDES] =
[BTreeSet::new(), BTreeSet::new(), BTreeSet::new()];
let mut output: Vec<u8> = Vec::new();
for key in &item_emit_order {
let mut segs: [Option<&str>; N_SIDES] = [None, None, None];
for s in 0..N_SIDES {
if let Some(&r) = side_idx_maps[s].get(key)
&& emitted[s].insert(r)
{
segs[s] = Some(inter_slice(side_sources[s], &side_ranges[s], r));
}
}
let (seg_bytes, seg_conflicts) = merge_segment(segs[0], segs[1], segs[2], markers);
output.extend_from_slice(&seg_bytes);
total_conflicts += seg_conflicts;
if let Some((Some(item_bytes), _)) = resolved.get(key) {
output.extend_from_slice(item_bytes);
}
}
let mut post: [Option<&str>; N_SIDES] = [None, None, None];
for s in 0..N_SIDES {
let last = side_ranges[s].len() - 1;
if emitted[s].insert(last) {
post[s] = Some(inter_slice(side_sources[s], &side_ranges[s], last));
}
}
let (post_bytes, post_conflicts) = merge_segment(post[0], post[1], post[2], markers);
if !post_bytes.is_empty() {
output.extend_from_slice(&post_bytes);
}
total_conflicts += post_conflicts;
(output, total_conflicts)
}
fn build_match_keys(items: &[Item]) -> Vec<MatchKey> {
let mut counters: BTreeMap<ItemKey, usize> = BTreeMap::new();
items
.iter()
.map(|item| {
let n = counters.entry(item.key.clone()).or_insert(0);
let occurrence = *n;
*n += 1;
(item.key.clone(), occurrence)
})
.collect()
}
fn child_key_set(item: &Item) -> BTreeSet<&ItemKey> {
match &item.body {
Some(body) => body.items.iter().map(|c| &c.key).collect(),
None => BTreeSet::new(),
}
}
fn build_aligned_match_keys(
base: &[Item],
ours: &[Item],
theirs: &[Item],
sides: SideSources<'_>,
) -> (Vec<MatchKey>, Vec<MatchKey>, Vec<MatchKey>) {
let mut container_keys: BTreeSet<ItemKey> = BTreeSet::new();
for items in [base, ours, theirs] {
for it in items {
if it.body.is_some() {
container_keys.insert(it.key.clone());
}
}
}
let base_mks = build_match_keys(base);
let align = |side: &[Item], side_src: &str| -> Vec<MatchKey> {
let mut base_by_key: BTreeMap<&ItemKey, Vec<(usize, &Item)>> = BTreeMap::new();
for (i, it) in base.iter().enumerate() {
if container_keys.contains(&it.key) {
base_by_key
.entry(&it.key)
.or_default()
.push((base_mks[i].1, it));
}
}
let mut used: BTreeMap<&ItemKey, BTreeSet<usize>> = BTreeMap::new();
let mut leaf_occ: BTreeMap<&ItemKey, usize> = BTreeMap::new();
let mut fresh: BTreeMap<&ItemKey, usize> = BTreeMap::new();
let mut disc_of: Vec<Option<usize>> = vec![None; side.len()];
for (pos, it) in side.iter().enumerate() {
if !container_keys.contains(&it.key) {
continue;
}
let childset = child_key_set(it);
let used_set = used.entry(&it.key).or_default();
let mut best: Option<(usize, usize)> = None; if let Some(cands) = base_by_key.get(&it.key) {
for (disc, bitem) in cands {
if used_set.contains(disc) {
continue;
}
let overlap = childset.intersection(&child_key_set(bitem)).count();
if overlap > 0 && best.is_none_or(|(o, _)| overlap > o) {
best = Some((overlap, *disc));
}
}
}
if let Some((_, d)) = best {
used_set.insert(d);
disc_of[pos] = Some(d);
}
}
for (pos, it) in side.iter().enumerate() {
if !container_keys.contains(&it.key) || disc_of[pos].is_some() {
continue;
}
let it_header = align_header_bytes(it, side_src);
let used_set = used.entry(&it.key).or_default();
if let Some(cands) = base_by_key.get(&it.key)
&& let Some((d, _)) = cands.iter().find(|(d, b)| {
!used_set.contains(d) && align_header_bytes(b, sides.base) == it_header
})
{
used_set.insert(*d);
disc_of[pos] = Some(*d);
}
}
let mut out = Vec::with_capacity(side.len());
for (pos, it) in side.iter().enumerate() {
if container_keys.contains(&it.key) {
let disc = if let Some(d) = disc_of[pos] {
d
} else {
let used_set = used.entry(&it.key).or_default();
let next_unused = base_by_key.get(&it.key).and_then(|cands| {
cands
.iter()
.map(|(d, _)| *d)
.find(|d| !used_set.contains(d))
});
if let Some(d) = next_unused {
used_set.insert(d);
d
} else {
let base_count = base_by_key.get(&it.key).map_or(0, Vec::len);
let f = fresh.entry(&it.key).or_insert(0);
let d = base_count + *f;
*f += 1;
d
}
};
out.push((it.key.clone(), disc));
} else {
let occ = leaf_occ.entry(&it.key).or_insert(0);
let d = *occ;
*occ += 1;
out.push((it.key.clone(), d));
}
}
out
};
let ours_mks = align(ours, sides.ours);
let theirs_mks = align(theirs, sides.theirs);
(base_mks, ours_mks, theirs_mks)
}
fn match_key_index(mks: &[MatchKey]) -> BTreeMap<MatchKey, usize> {
mks.iter()
.enumerate()
.map(|(i, mk)| (mk.clone(), i))
.collect()
}
fn inter_slice<'a>(source: &'a str, ranges: &[(usize, usize)], idx: usize) -> &'a str {
let (start, end) = ranges[idx];
&source[start..end]
}
fn merge_segment(
base: Option<&str>,
ours: Option<&str>,
theirs: Option<&str>,
markers: ConflictMarkers<'_>,
) -> (Vec<u8>, usize) {
match (base, ours, theirs) {
(None, None, None) => (Vec::new(), 0),
(Some(b), Some(o), Some(t)) => materialize_segment(
text_hunk_merge_with_markers(b.as_bytes(), o.as_bytes(), t.as_bytes(), markers),
b,
),
(Some(b), Some(o), None) => materialize_segment(
text_hunk_merge_with_markers(b.as_bytes(), o.as_bytes(), b.as_bytes(), markers),
b,
),
(Some(b), None, Some(t)) => materialize_segment(
text_hunk_merge_with_markers(b.as_bytes(), b.as_bytes(), t.as_bytes(), markers),
b,
),
(Some(b), None, None) => (b.as_bytes().to_vec(), 0),
(None, Some(o), Some(t)) => {
if o == t {
(o.as_bytes().to_vec(), 0)
} else {
materialize_segment(
text_hunk_merge_with_markers(&[], o.as_bytes(), t.as_bytes(), markers),
"",
)
}
}
(None, Some(o), None) => (o.as_bytes().to_vec(), 0),
(None, None, Some(t)) => (t.as_bytes().to_vec(), 0),
}
}
fn materialize_segment(outcome: MergeOutcome, fallback: &str) -> (Vec<u8>, usize) {
match outcome {
MergeOutcome::Clean(bytes) => (bytes, 0),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => (merged_bytes_with_markers, conflict_count),
MergeOutcome::Binary | MergeOutcome::DeleteVsModify => (fallback.as_bytes().to_vec(), 0),
}
}
#[derive(Clone, Copy)]
struct SideSources<'a> {
base: &'a str,
ours: &'a str,
theirs: &'a str,
eol_policy: EolPolicy,
}
impl<'a> SideSources<'a> {
fn new(base: &'a str, ours: &'a str, theirs: &'a str) -> Self {
let eol_policy = EolPolicy::detect(&[base.as_bytes(), ours.as_bytes(), theirs.as_bytes()]);
SideSources {
base,
ours,
theirs,
eol_policy,
}
}
}
#[derive(Clone, Copy)]
struct EolPolicy {
crlf: usize,
lf: usize,
first_crlf: usize,
first_lf: usize,
}
impl EolPolicy {
fn detect(samples: &[&[u8]]) -> Self {
let mut crlf = 0usize;
let mut lf = 0usize;
let mut first_crlf = 0usize;
let mut first_lf = 0usize;
for (i, s) in samples.iter().enumerate() {
let (c, l) = count_eols(s);
crlf += c;
lf += l;
if i == 0 {
first_crlf = c;
first_lf = l;
}
}
EolPolicy {
crlf,
lf,
first_crlf,
first_lf,
}
}
fn eol(self) -> &'static [u8] {
if self.crlf > self.lf {
return b"\r\n";
}
if self.lf > self.crlf {
return b"\n";
}
if self.first_crlf > self.first_lf {
return b"\r\n";
}
b"\n"
}
}
fn whole_bytes<'a>(item: &Item, src: &'a str) -> &'a [u8] {
&src.as_bytes()[item.start_byte..item.end_byte]
}
fn header_bytes<'a>(item: &Item, src: &'a str) -> &'a [u8] {
let inner_start = item.body.as_ref().expect("container").inner_start;
&src.as_bytes()[item.start_byte..inner_start]
}
fn align_header_bytes<'a>(item: &Item, src: &'a str) -> &'a [u8] {
if item.body.is_some() {
header_bytes(item, src)
} else {
whole_bytes(item, src)
}
}
fn footer_bytes<'a>(item: &Item, src: &'a str) -> &'a [u8] {
let inner_end = item.body.as_ref().expect("container").inner_end;
&src.as_bytes()[inner_end..item.end_byte]
}
fn resolve_node(
sides: SideSources<'_>,
base_item: Option<&Item>,
ours_item: Option<&Item>,
theirs_item: Option<&Item>,
markers: ConflictMarkers<'_>,
) -> (Option<Vec<u8>>, usize) {
let mut present = [base_item, ours_item, theirs_item]
.into_iter()
.flatten()
.peekable();
let all_containers = present.peek().is_some() && present.all(|i| i.body.is_some());
if all_containers {
resolve_container(sides, base_item, ours_item, theirs_item, markers)
} else {
resolve_item(sides, base_item, ours_item, theirs_item, markers)
}
}
fn resolve_container(
sides: SideSources<'_>,
base_item: Option<&Item>,
ours_item: Option<&Item>,
theirs_item: Option<&Item>,
markers: ConflictMarkers<'_>,
) -> (Option<Vec<u8>>, usize) {
match (base_item, ours_item, theirs_item) {
(None, None, None) => (None, 0),
(None, Some(o), None) => (Some(whole_bytes(o, sides.ours).to_vec()), 0),
(None, None, Some(t)) => (Some(whole_bytes(t, sides.theirs).to_vec()), 0),
(None, Some(o), Some(t)) => {
let ow = whole_bytes(o, sides.ours);
let tw = whole_bytes(t, sides.theirs);
if ow == tw {
(Some(ow.to_vec()), 0)
} else {
(Some(emit_addadd_conflict(ow, tw, markers, sides)), 1)
}
}
(Some(_), None, None) => (None, 0),
(Some(b), Some(o), None) => {
let bw = whole_bytes(b, sides.base);
let ow = whole_bytes(o, sides.ours);
if bw == ow {
(None, 0)
} else {
materialize_outcome(text_hunk_merge_with_markers(bw, ow, &[], markers))
}
}
(Some(b), None, Some(t)) => {
let bw = whole_bytes(b, sides.base);
let tw = whole_bytes(t, sides.theirs);
if bw == tw {
(None, 0)
} else {
materialize_outcome(text_hunk_merge_with_markers(bw, &[], tw, markers))
}
}
(Some(b), Some(o), Some(t)) => {
let bw = whole_bytes(b, sides.base);
let ow = whole_bytes(o, sides.ours);
let tw = whole_bytes(t, sides.theirs);
if ow == bw {
(Some(tw.to_vec()), 0)
} else if tw == bw || ow == tw {
(Some(ow.to_vec()), 0)
} else {
merge_container_3way(sides, b, o, t, markers)
}
}
}
}
fn merge_container_3way(
sides: SideSources<'_>,
base: &Item,
ours: &Item,
theirs: &Item,
markers: ConflictMarkers<'_>,
) -> (Option<Vec<u8>>, usize) {
debug_assert!(
base.body.is_some() && ours.body.is_some() && theirs.body.is_some(),
"merge_container_3way entered with a leaf side — structural precondition violated"
);
let (header, hc) = merge3_text(
header_bytes(base, sides.base),
header_bytes(ours, sides.ours),
header_bytes(theirs, sides.theirs),
markers,
);
let bb = base.body.as_ref().expect("container");
let ob = ours.body.as_ref().expect("container");
let tb = theirs.body.as_ref().expect("container");
let (open, oc) = merge3_text(
&sides.base.as_bytes()[bb.inner_start..bb.content_start],
&sides.ours.as_bytes()[ob.inner_start..ob.content_start],
&sides.theirs.as_bytes()[tb.inner_start..tb.content_start],
markers,
);
let (body, bc) = merge_region(
sides,
&bb.items,
&ob.items,
&tb.items,
(bb.content_start, bb.content_end),
(ob.content_start, ob.content_end),
(tb.content_start, tb.content_end),
markers,
);
let (close, cc) = merge3_text(
&sides.base.as_bytes()[bb.content_end..bb.inner_end],
&sides.ours.as_bytes()[ob.content_end..ob.inner_end],
&sides.theirs.as_bytes()[tb.content_end..tb.inner_end],
markers,
);
let (footer, fc) = merge3_text(
footer_bytes(base, sides.base),
footer_bytes(ours, sides.ours),
footer_bytes(theirs, sides.theirs),
markers,
);
let mut out = header;
out.extend_from_slice(&open);
out.extend_from_slice(&body);
out.extend_from_slice(&close);
out.extend_from_slice(&footer);
(Some(out), hc + oc + bc + cc + fc)
}
fn merge3_text(
base: &[u8],
ours: &[u8],
theirs: &[u8],
markers: ConflictMarkers<'_>,
) -> (Vec<u8>, usize) {
if ours == theirs {
return (ours.to_vec(), 0);
}
if ours == base {
return (theirs.to_vec(), 0);
}
if theirs == base {
return (ours.to_vec(), 0);
}
materialize_segment(
text_hunk_merge_with_markers(base, ours, theirs, markers),
std::str::from_utf8(base).unwrap_or(""),
)
}
fn resolve_item(
sides: SideSources<'_>,
base_item: Option<&Item>,
ours_item: Option<&Item>,
theirs_item: Option<&Item>,
markers: ConflictMarkers<'_>,
) -> (Option<Vec<u8>>, usize) {
let base_bytes = base_item.map(|i| &sides.base.as_bytes()[i.start_byte..i.end_byte]);
let ours_bytes = ours_item.map(|i| &sides.ours.as_bytes()[i.start_byte..i.end_byte]);
let theirs_bytes = theirs_item.map(|i| &sides.theirs.as_bytes()[i.start_byte..i.end_byte]);
match (base_bytes, ours_bytes, theirs_bytes) {
(None, None, None) => (None, 0),
(None, Some(o), None) => (Some(o.to_vec()), 0),
(None, None, Some(t)) => (Some(t.to_vec()), 0),
(None, Some(o), Some(t)) => {
if o == t {
(Some(o.to_vec()), 0)
} else {
(Some(emit_addadd_conflict(o, t, markers, sides)), 1)
}
}
(Some(_), None, None) => (None, 0),
(Some(b), Some(o), None) => {
if b == o {
(None, 0)
} else {
let outcome = text_hunk_merge_with_markers(b, o, &[], markers);
materialize_outcome(outcome)
}
}
(Some(b), None, Some(t)) => {
if b == t {
(None, 0)
} else {
let outcome = text_hunk_merge_with_markers(b, &[], t, markers);
materialize_outcome(outcome)
}
}
(Some(b), Some(o), Some(t)) => {
if o == b {
(Some(t.to_vec()), 0)
} else if t == b || o == t {
(Some(o.to_vec()), 0)
} else {
let outcome = text_hunk_merge_with_markers(b, o, t, markers);
materialize_outcome(outcome)
}
}
}
}
fn materialize_outcome(outcome: MergeOutcome) -> (Option<Vec<u8>>, usize) {
match outcome {
MergeOutcome::Clean(bytes) => (Some(bytes), 0),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => (Some(merged_bytes_with_markers), conflict_count),
MergeOutcome::Binary | MergeOutcome::DeleteVsModify => (None, 1),
}
}
fn resolve_use_component(
sides: SideSources<'_>,
base_items: &[&Item],
ours_items: &[&Item],
theirs_items: &[&Item],
markers: ConflictMarkers<'_>,
) -> (Option<Vec<u8>>, usize) {
let eol = sides.eol_policy.eol();
let base_bytes = join_component(base_items, sides.base, eol);
let ours_bytes = join_component(ours_items, sides.ours, eol);
let theirs_bytes = join_component(theirs_items, sides.theirs, eol);
let non_empty = |v: Vec<u8>| if v.is_empty() { None } else { Some(v) };
if ours_bytes == base_bytes {
return (non_empty(theirs_bytes), 0);
}
if theirs_bytes == base_bytes || ours_bytes == theirs_bytes {
return (non_empty(ours_bytes), 0);
}
(
Some(emit_addadd_conflict(
&ours_bytes,
&theirs_bytes,
markers,
sides,
)),
1,
)
}
fn join_component(items: &[&Item], source: &str, eol: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.extend_from_slice(eol);
}
out.extend_from_slice(&source.as_bytes()[item.start_byte..item.end_byte]);
}
out
}
fn compute_item_emit_order(
base_mks: &[MatchKey],
ours_mks: &[MatchKey],
theirs_mks: &[MatchKey],
all_keys: &BTreeSet<&MatchKey>,
) -> Vec<MatchKey> {
let mut order: Vec<MatchKey> = base_mks.to_vec();
for side_mks in [ours_mks, theirs_mks] {
for (idx, key) in side_mks.iter().enumerate() {
if order.contains(key) {
continue;
}
let mut insert_at = 0usize;
for i in (0..idx).rev() {
if let Some(pos) = order.iter().position(|k| *k == side_mks[i]) {
insert_at = pos + 1;
break;
}
}
order.insert(insert_at, key.clone());
}
}
order.into_iter().filter(|k| all_keys.contains(k)).collect()
}
fn ensure_trailing_newline(out: &mut Vec<u8>, eol: &[u8]) {
if !out.is_empty() && *out.last().unwrap() != b'\n' {
out.extend_from_slice(eol);
}
}
fn count_eols(s: &[u8]) -> (usize, usize) {
let mut crlf = 0usize;
let mut lf = 0usize;
let mut prev = 0u8;
for &b in s {
if b == b'\n' {
if prev == b'\r' {
crlf += 1;
} else {
lf += 1;
}
}
prev = b;
}
(crlf, lf)
}
fn reconcile_trailing_newline(out: &mut Vec<u8>, sides: SideSources<'_>) {
if out.is_empty() {
return;
}
let want_newline = majority_ends_with_newline(sides.base, sides.ours, sides.theirs);
let has_newline = *out.last().unwrap() == b'\n';
match (want_newline, has_newline) {
(true, false) => {
out.extend_from_slice(sides.eol_policy.eol());
}
(false, true) => {
out.pop();
if out.last() == Some(&b'\r') {
out.pop();
}
}
_ => {}
}
}
fn majority_ends_with_newline(base: &str, ours: &str, theirs: &str) -> bool {
let mut with = 0u8;
let mut total = 0u8;
for s in [base, ours, theirs] {
if s.is_empty() {
continue;
}
total += 1;
if s.as_bytes().last() == Some(&b'\n') {
with += 1;
}
}
total == 0 || with * 2 > total
}
fn emit_addadd_conflict(
ours: &[u8],
theirs: &[u8],
markers: ConflictMarkers<'_>,
sides: SideSources<'_>,
) -> Vec<u8> {
let items_policy = EolPolicy::detect(&[ours, theirs]);
let eol = if items_policy.crlf + items_policy.lf > 0 {
items_policy.eol()
} else {
sides.eol_policy.eol()
};
let mut out = Vec::with_capacity(ours.len() + theirs.len() + 64);
out.extend_from_slice(b"<<<<<<< ");
out.extend_from_slice(markers.ours.as_bytes());
out.extend_from_slice(eol);
out.extend_from_slice(ours);
ensure_trailing_newline(&mut out, eol);
out.extend_from_slice(b"=======");
out.extend_from_slice(eol);
out.extend_from_slice(theirs);
ensure_trailing_newline(&mut out, eol);
out.extend_from_slice(b">>>>>>> ");
out.extend_from_slice(markers.theirs.as_bytes());
out.extend_from_slice(eol);
out
}