mdiff: add a "blocksinrange" function to filter diff blocks by line range
authorDenis Laxalde <denis.laxalde@logilab.fr>
Tue, 03 Jan 2017 18:15:58 +0100
changeset 30717 3eeb8e138e5c
parent 30716 3de9df6ee5bf
child 30718 ce662ee40d2d
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.
mercurial/mdiff.py
tests/test-linerange.py
--- 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__)