--- a/mercurial/simplemerge.py Wed Oct 23 17:08:57 2024 -0400
+++ b/mercurial/simplemerge.py Thu Oct 24 17:35:53 2024 +0200
@@ -49,6 +49,30 @@
return None
+def intersect_or_touch(ra, rb):
+ """Given two ranges return the range where they intersect or touch or None.
+
+ >>> intersect_or_touch((0, 10), (0, 6))
+ (0, 6)
+ >>> intersect_or_touch((0, 10), (5, 15))
+ (5, 10)
+ >>> intersect_or_touch((0, 10), (10, 15))
+ (10, 10)
+ >>> intersect_or_touch((0, 9), (10, 15))
+ >>> intersect_or_touch((0, 9), (7, 15))
+ (7, 9)
+ """
+ assert ra[0] <= ra[1]
+ assert rb[0] <= rb[1]
+
+ sa = max(ra[0], rb[0])
+ sb = min(ra[1], rb[1])
+ if sa <= sb:
+ return sa, sb
+ else:
+ return None
+
+
def compare_range(a, astart, aend, b, bstart, bend):
"""Compare a[astart:aend] == b[bstart:bend], without slicing."""
if (aend - astart) != (bend - bstart):
@@ -66,7 +90,16 @@
Given strings BASE, OTHER, THIS, tries to produce a combined text
incorporating the changes from both BASE->OTHER and BASE->THIS."""
- def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
+ def __init__(
+ self,
+ basetext,
+ atext,
+ btext,
+ base=None,
+ a=None,
+ b=None,
+ relaxed_sync=False,
+ ):
self.basetext = basetext
self.atext = atext
self.btext = btext
@@ -76,6 +109,7 @@
a = mdiff.splitnewlines(atext)
if b is None:
b = mdiff.splitnewlines(btext)
+ self.relaxed_sync = relaxed_sync
self.base = base
self.a = a
self.b = b
@@ -220,6 +254,11 @@
len_a = len(amatches)
len_b = len(bmatches)
+ if self.relaxed_sync:
+ intersect_fun = intersect_or_touch
+ else:
+ intersect_fun = intersect
+
sl = []
while ia < len_a and ib < len_b:
@@ -228,7 +267,7 @@
# there is an unconflicted block at i; how long does it
# extend? until whichever one ends earlier.
- i = intersect((abase, abase + alen), (bbase, bbase + blen))
+ i = intersect_fun((abase, abase + alen), (bbase, bbase + blen))
if i:
intbase = i[0]
intend = i[1]
@@ -258,13 +297,41 @@
# advance whichever one ends first in the base text
if (abase + alen) < (bbase + blen):
ia += 1
+ elif not self.relaxed_sync:
+ # if the blocks end at the same time we know they can't overlap
+ # any other block, so no need for the complicated checks below
+ ib += 1
+ elif (abase + alen) > (bbase + blen):
+ ib += 1
else:
- ib += 1
+ # If both end at the same time, either may touching the
+ # follow-up matching block on the other side.
+ # Advance the one whose next block comes sooner.
+ if ia + 1 == len_a:
+ # if we run out of blocks on A side, we may as well advance B
+ # since there's nothing on A side for that to touch
+ ib += 1
+ elif ib + 1 == len_b:
+ ia += 1
+ elif amatches[ia + 1][0] > bmatches[ib + 1][0]:
+ ib += 1
+ elif amatches[ia + 1][0] < bmatches[ib + 1][0]:
+ ia += 1
+ else:
+ # symmetric situation: both sides added lines to the same place
+ # it's less surprising if we treat it as a conflict, so skip
+ # both without a preferred order
+ ia += 1
+ ib += 1
intbase = len(self.base)
abase = len(self.a)
bbase = len(self.b)
- sl.append((intbase, intbase, abase, abase, bbase, bbase))
+ sentinel_hunk = (intbase, intbase, abase, abase, bbase, bbase)
+ # we avoid duplicate sentinel hunk at the end to make the
+ # test output cleaner
+ if not (sl and sl[len(sl) - 1] == sentinel_hunk):
+ sl.append(sentinel_hunk)
return sl
@@ -498,6 +565,7 @@
other,
mode=b'merge',
allow_binary=False,
+ relaxed_sync=False,
):
"""Performs the simplemerge algorithm.
@@ -509,7 +577,9 @@
_verifytext(base)
_verifytext(other)
- m3 = Merge3Text(base.text(), local.text(), other.text())
+ m3 = Merge3Text(
+ base.text(), local.text(), other.text(), relaxed_sync=relaxed_sync
+ )
conflicts = False
if mode == b'union':
lines = _resolve(m3, (1, 2))