# [PATCH 1 of 3 V2] mdiff: add a "blocksinrange" function to filter diff blocks by line range

Denis Laxalde denis.laxalde at logilab.fr
Fri Dec 16 08:30:13 EST 2016

# HG changeset patch
# User Denis Laxalde <denis.laxalde at logilab.fr>
# Date 1476279051 -7200
#      Wed Oct 12 15:30:51 2016 +0200
# Node ID 17eee60ccbafe46f539a75484ebf110377781fb1
# Parent  342d0cb4f446826169a83a6e773f1c767e0c977b
# EXP-Topic linerange-log/revset
mdiff: add a "blocksinrange" function to filter diff blocks by line range

The function filters diff blocks as generated by mdiff.allblock function based
on whether they are contained in a given line range based on the "b-side" of
blocks.

diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py
--- a/mercurial/mdiff.py
+++ b/mercurial/mdiff.py
@@ -113,6 +113,45 @@ def splitblock(base1, lines1, base2, lin
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
diff --git a/tests/test-linerange.py b/tests/test-linerange.py
new file mode 100644
--- /dev/null
+++ b/tests/test-linerange.py
@@ -0,0 +1,117 @@
+from __future__ import absolute_import
+
+import unittest
+from mercurial import error, mdiff
+
+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 = [
+            ((0, 3, 0, 2), '!'),
+            ((3, 7, 2, 6), '='),
+            ((7, 12, 6, 12), '!'),
+            ((15, 15, 12, 12), '='),
+        ]
+
+    def testWithinEqual(self):
+        linerange2 = (3, 5)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (4, 6))
+        self.assertEqual(skipped, [True, False, True, True])
+
+    def testWithinEqualStrictly(self):
+        linerange2 = (2, 6)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (3, 7))
+        self.assertEqual(skipped, [True, False, True, True])
+
+    def testWithinEqualLowerboundOneline(self):
+        linerange2 = (2, 3)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (3, 4))
+        self.assertEqual(skipped, [True, False, True, True])
+
+    def testWithinEqualLowerbound(self):
+        linerange2 = (2, 3)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (3, 4))
+        self.assertEqual(skipped, [True, False, True, True])
+
+    def testWithinEqualUpperbound(self):
+        linerange2 = (3, 6)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (4, 7))
+        self.assertEqual(skipped, [True, False, True, True])
+
+    def testWithinFirstBlockNeq(self):
+        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):
+        for linerange2 in [
+            (6, 7),
+            (7, 8),
+            (7, 7),
+            (6, 12),
+        ]:
+            linerange1, skipped = filteredblocks(self.blocks, linerange2)
+            self.assertEqual(linerange1, (7, 12))
+            self.assertEqual(skipped, [True, True, False, True])
+
+    def testWithinLastLine(self):
+        linerange2 = (11, 12)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (7, 12))
+        self.assertEqual(skipped, [True, True, False, True])
+
+    def testAccrossTwoBlocks(self):
+        linerange2 = (1, 7)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (0, 12))
+        self.assertEqual(skipped, [False, False, False, True])
+
+    def testCrossingSeveralBlocks(self):
+        linerange2 = (1, 8)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (0, 12))
+        self.assertEqual(skipped, [False, False, False, True])
+
+    def testStartInEqBlock(self):
+        linerange2 = (5, 9)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (6, 12))
+        self.assertEqual(skipped, [True, False, False, True])
+
+    def testEndInEqBlock(self):
+        linerange2 = (1, 3)
+        linerange1, skipped = filteredblocks(self.blocks, linerange2)
+        self.assertEqual(linerange1, (0, 4))
+        self.assertEqual(skipped, [False, False, True, True])
+
+    def testOutOfRange(self):
+        for linerange2 in [
+            (0, 34),
+            (15, 12),
+        ]:
+            with self.assertRaises(error.Abort) as cm:
+                mdiff.blocksinrange(self.blocks, linerange2)
+            self.assertIn('line range exceeds file size', str(cm.exception))
+
+if __name__ == '__main__':
+    import silenttestrunner
+    silenttestrunner.main(__name__)