--- a/mercurial/mdiff.py Fri Jan 06 16:19:41 2017 +0000
+++ b/mercurial/mdiff.py Tue Jan 03 18:15:58 2017 +0100
@@ -113,6 +113,45 @@
s1 = i1
s2 = i2
+def blocksinrange(blocks, rangeb):
+ """filter `blocks` like (a1, a2, b1, b2) from items outside line range
+ `rangeb` from ``(b1, b2)`` point of view.
+
+ Return `filteredblocks, rangea` where:
+
+ * `filteredblocks` is list of ``block = (a1, a2, b1, b2), stype`` items of
+ `blocks` that are inside `rangeb` from ``(b1, b2)`` point of view; a
+ block ``(b1, b2)`` being inside `rangeb` if
+ ``rangeb[0] < b2 and b1 < rangeb[1]``;
+ * `rangea` is the line range w.r.t. to ``(a1, a2)`` parts of `blocks`.
+ """
+ lbb, ubb = rangeb
+ lba, uba = None, None
+ filteredblocks = []
+ for block in blocks:
+ (a1, a2, b1, b2), stype = block
+ if lbb >= b1 and ubb <= b2 and stype == '=':
+ # rangeb is within a single "=" hunk, restrict back linerange1
+ # by offsetting rangeb
+ lba = lbb - b1 + a1
+ uba = ubb - b1 + a1
+ else:
+ if b1 <= lbb < b2:
+ if stype == '=':
+ lba = a2 - (b2 - lbb)
+ else:
+ lba = a1
+ if b1 < ubb <= b2:
+ if stype == '=':
+ uba = a1 + (ubb - b1)
+ else:
+ uba = a2
+ if lbb < b2 and b1 < ubb:
+ filteredblocks.append(block)
+ if lba is None or uba is None or uba < lba:
+ raise error.Abort(_('line range exceeds file size'))
+ return filteredblocks, (lba, uba)
+
def allblocks(text1, text2, opts=None, lines1=None, lines2=None):
"""Return (block, type) tuples, where block is an mdiff.blocks
line entry. type is '=' for blocks matching exactly one another
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-linerange.py Tue Jan 03 18:15:58 2017 +0100
@@ -0,0 +1,232 @@
+from __future__ import absolute_import
+
+import unittest
+from mercurial import error, mdiff
+
+# for readability, line numbers are 0-origin
+text1 = '''
+ 00 at OLD
+ 01 at OLD
+ 02 at OLD
+02 at NEW, 03 at OLD
+03 at NEW, 04 at OLD
+04 at NEW, 05 at OLD
+05 at NEW, 06 at OLD
+ 07 at OLD
+ 08 at OLD
+ 09 at OLD
+ 10 at OLD
+ 11 at OLD
+'''[1:] # strip initial LF
+
+text2 = '''
+00 at NEW
+01 at NEW
+02 at NEW, 03 at OLD
+03 at NEW, 04 at OLD
+04 at NEW, 05 at OLD
+05 at NEW, 06 at OLD
+06 at NEW
+07 at NEW
+08 at NEW
+09 at NEW
+10 at NEW
+11 at NEW
+'''[1:] # strip initial LF
+
+def filteredblocks(blocks, rangeb):
+ """return `rangea` extracted from `blocks` coming from
+ `mdiff.blocksinrange` along with the mask of blocks within rangeb.
+ """
+ filtered, rangea = mdiff.blocksinrange(blocks, rangeb)
+ skipped = [b not in filtered for b in blocks]
+ return rangea, skipped
+
+class blocksinrangetests(unittest.TestCase):
+
+ def setUp(self):
+ self.blocks = list(mdiff.allblocks(text1, text2))
+ assert self.blocks == [
+ ([0, 3, 0, 2], '!'),
+ ((3, 7, 2, 6), '='),
+ ([7, 12, 6, 12], '!'),
+ ((12, 12, 12, 12), '='),
+ ], self.blocks
+
+ def testWithinEqual(self):
+ """linerange within an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^
+ linerange2 = (3, 5)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (4, 6))
+ self.assertEqual(skipped, [True, False, True, True])
+
+ def testWithinEqualStrictly(self):
+ """linerange matching exactly an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^^^
+ linerange2 = (2, 6)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (3, 7))
+ self.assertEqual(skipped, [True, False, True, True])
+
+ def testWithinEqualLowerbound(self):
+ """linerange at beginning of an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^
+ linerange2 = (2, 4)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (3, 5))
+ self.assertEqual(skipped, [True, False, True, True])
+
+ def testWithinEqualLowerboundOneline(self):
+ """oneline-linerange at beginning of an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^
+ linerange2 = (2, 3)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (3, 4))
+ self.assertEqual(skipped, [True, False, True, True])
+
+ def testWithinEqualUpperbound(self):
+ """linerange at end of an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^^
+ linerange2 = (3, 6)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (4, 7))
+ self.assertEqual(skipped, [True, False, True, True])
+
+ def testWithinEqualUpperboundOneLine(self):
+ """oneline-linerange at end of an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^
+ linerange2 = (5, 6)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (6, 7))
+ self.assertEqual(skipped, [True, False, True, True])
+
+ def testWithinFirstBlockNeq(self):
+ """linerange within the first "!" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^
+ # | (empty)
+ # ^
+ # ^^
+ for linerange2 in [
+ (0, 1),
+ (1, 1),
+ (1, 2),
+ (0, 2),
+ ]:
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (0, 3))
+ self.assertEqual(skipped, [False, True, True, True])
+
+ def testWithinLastBlockNeq(self):
+ """linerange within the last "!" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^
+ # ^
+ # | (empty)
+ # ^^^^^^
+ # ^
+ for linerange2 in [
+ (6, 7),
+ (7, 8),
+ (7, 7),
+ (6, 12),
+ (11, 12),
+ ]:
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (7, 12))
+ self.assertEqual(skipped, [True, True, False, True])
+
+ def testAccrossTwoBlocks(self):
+ """linerange accross two blocks"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^^^
+ linerange2 = (1, 5)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (0, 6))
+ self.assertEqual(skipped, [False, False, True, True])
+
+ def testCrossingSeveralBlocks(self):
+ """linerange accross three blocks"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^^^^^^
+ linerange2 = (1, 8)
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, (0, 12))
+ self.assertEqual(skipped, [False, False, False, True])
+
+ def testStartInEqBlock(self):
+ """linerange starting in an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^^^
+ # ^^^^^^^
+ for linerange2, expectedlinerange1 in [
+ ((5, 9), (6, 12)),
+ ((4, 11), (5, 12)),
+ ]:
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, expectedlinerange1)
+ self.assertEqual(skipped, [True, False, False, True])
+
+ def testEndInEqBlock(self):
+ """linerange ending in an "=" block"""
+ # IDX 0 1
+ # 012345678901
+ # SRC NNOOOONNNNNN (New/Old)
+ # ^^
+ # ^^^^^
+ for linerange2, expectedlinerange1 in [
+ ((1, 3), (0, 4)),
+ ((0, 4), (0, 5)),
+ ]:
+ linerange1, skipped = filteredblocks(self.blocks, linerange2)
+ self.assertEqual(linerange1, expectedlinerange1)
+ self.assertEqual(skipped, [False, False, True, True])
+
+ def testOutOfRange(self):
+ """linerange exceeding file size"""
+ exctype = error.Abort
+ for linerange2 in [
+ (0, 34),
+ (15, 12),
+ ]:
+ # Could be `with self.assertRaises(error.Abort)` but python2.6
+ # does not have assertRaises context manager.
+ try:
+ mdiff.blocksinrange(self.blocks, linerange2)
+ except exctype as exc:
+ self.assertTrue('line range exceeds file size' in str(exc))
+ else:
+ self.fail('%s not raised' % exctype.__name__)
+
+if __name__ == '__main__':
+ import silenttestrunner
+ silenttestrunner.main(__name__)