Plugins/VcsPlugins/vcsGit/GitDiffParser.py

changeset 6020
baf6da1ae288
child 6048
82ad8ec9548c
diff -r 58ecdaf0b789 -r baf6da1ae288 Plugins/VcsPlugins/vcsGit/GitDiffParser.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/VcsPlugins/vcsGit/GitDiffParser.py	Sun Dec 10 17:42:11 2017 +0100
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015 - 2017 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a class to store and parse diff output.
+"""
+
+from __future__ import unicode_literals
+
+import re
+import os
+
+
+class GitDiffParser(object):
+    """
+    Class implementing a class to store and parse diff output.
+    """
+    HunkHeaderRegexp = re.compile(r'^@@ -([0-9,]+) \+([0-9,]+) @@(.*)',
+                                  re.DOTALL)
+    
+    def __init__(self, diff):
+        """
+        Constructor
+        
+        @param diff output of the diff command (list of string)
+        """
+        self.__diff = diff[:]
+        
+        self.__headerLines = []
+        self.__hunks = []
+        self.__parsed = False
+        # diff parsing is done on demand
+    
+    def __initHunk(self):
+        """
+        Private method to initialize a hunk data structure.
+        
+        @return hunk data structure (dictionary)
+        """
+        hunk = {
+            "start": -1,
+            "end": -1,
+            "lines": [],
+            "oldStart": -1,
+            "oldCount": -1,
+            "newStart": -1,
+            "newCount": -1,
+            "heading": "",
+        }
+        return hunk
+    
+    def __parseRange(self, headerRange):
+        """
+        Private method to parse the hunk header range part.
+        
+        @param headerRange hunk header range (string)
+        @return tuple of hunk start and hunk length (integer, integer)
+        """
+        if ',' in headerRange:
+            begin, end = headerRange.split(',', 1)
+            return int(begin), int(end)
+        else:
+            return int(headerRange), 1
+    
+    def __parseDiff(self):
+        """
+        Private method to parse the diff output.
+        
+        @exception AssertionError raised when a malformed hunk header is
+            encountered
+        """
+        if not self.__parsed:
+            # step 1: extract the diff header
+            for line in self.__diff:
+                if not line.startswith("@@ "):
+                    self.__headerLines.append(line)
+                else:
+                    break
+            
+            # step 2: break the rest into diff hunks
+            for lineIdx, line in enumerate(
+                    self.__diff[len(self.__headerLines):]):
+                # disect the hunk header line
+                m = self.HunkHeaderRegexp.match(line)
+                if m:
+                    self.__hunks.append(self.__initHunk())
+                    self.__hunks[-1]["start"] = lineIdx
+                    (self.__hunks[-1]["oldStart"],
+                     self.__hunks[-1]["oldCount"]) = \
+                        self.__parseRange(m.group(1))
+                    (self.__hunks[-1]["newStart"],
+                     self.__hunks[-1]["newCount"]) = \
+                        self.__parseRange(m.group(2))
+                    self.__hunks[-1]["heading"] = m.group(3)
+                elif not self.__hunks:
+                    raise AssertionError("Malformed hunk header: '{0}'"
+                                         .format(line))
+                self.__hunks[-1]["lines"].append(line)
+            # step 3: calculate hunk end lines
+            for hunk in self.__hunks:
+                hunk["end"] = hunk["start"] + len(hunk["lines"]) - 1
+    
+    def __generateRange(self, start, count):
+        """
+        Private method to generate a hunk header range.
+        
+        @param start start line (integer)
+        @param count line count (integer)
+        @return hunk header range (string)
+        """
+        if count == 1:
+            return "{0}".format(start)
+        else:
+            return "{0},{1}".format(start, count)
+    
+    def __generateHunkHeader(self, oldStart, oldCount, newStart, newCount,
+                             heading=os.linesep):
+        """
+        Private method to generate a hunk header line.
+        
+        @param oldStart start line of the old part (integer)
+        @param oldCount line count of the old part (integer)
+        @param newStart start line of the new part (integer)
+        @param newCount line count of the new part (integer)
+        @param heading hunk heading (string)
+        @return hunk header (string)
+        """
+        return "@@ -{0} +{1} @@{2}".format(
+            self.__generateRange(oldStart, oldCount),
+            self.__generateRange(newStart, newCount),
+            heading)
+    
+    def headerLength(self):
+        """
+        Public method to get the header length.
+        
+        @return length of the header (integer)
+        """
+        self.__parseDiff()
+        return len(self.__headerLines)
+    
+    def createHunkPatch(self, lineIndex):
+        """
+        Public method to create a hunk based patch.
+        
+        @param lineIndex line number of the hunk (integer)
+        @return diff lines of the patch (string)
+        """
+        self.__parseDiff()
+        
+        patch = self.__headerLines[:]
+        for hunk in self.__hunks:
+            if hunk["start"] <= lineIndex <= hunk["end"]:
+                patch.extend(hunk["lines"])
+                break
+        
+        return "".join(patch)
+    
+    def createLinesPatch(self, startIndex, endIndex, reverse=False):
+        """
+        Public method to create a selected lines based patch.
+        
+        @param startIndex start line number (integer)
+        @param endIndex end line number (integer)
+        @param reverse flag indicating a reverse patch (boolean)
+        @return diff lines of the patch (string)
+        """
+        self.__parseDiff()
+        
+        ADDITION = "+"
+        DELETION = "-"
+        CONTEXT = " "
+        NONEWLINE = "\\"
+        
+        patch = []
+        startOffset = 0
+        
+        for hunk in self.__hunks:
+            if hunk["end"] < startIndex:
+                # skip hunks before the selected lines
+                continue
+            
+            if hunk["start"] > endIndex:
+                # done, exit the loop
+                break
+            
+            counts = {
+                ADDITION: 0,
+                DELETION: 0,
+                CONTEXT: 0,
+            }
+            previousLineSkipped = False
+            processedLines = []
+            
+            for lineIndex, line in enumerate(hunk["lines"][1:],
+                                             start=hunk["start"] + 1):
+                lineType = line[0]
+                lineContent = line[1:]
+                
+                if not (startIndex <= lineIndex <= endIndex):
+                    if (not reverse and lineType == ADDITION) or \
+                            (reverse and lineType == DELETION):
+                        previousLineSkipped = True
+                        continue
+                    
+                    elif (not reverse and lineType == DELETION) or \
+                            (reverse and lineType == ADDITION):
+                        lineType = CONTEXT
+                
+                if lineType == NONEWLINE and previousLineSkipped:
+                    continue
+                
+                processedLines.append(lineType + lineContent)
+                counts[lineType] += 1
+                previousLineSkipped = False
+            
+            # hunks consisting of pure context lines are excluded
+            if counts[ADDITION] == 0 and counts[DELETION] == 0:
+                continue
+            
+            oldCount = counts[CONTEXT] + counts[DELETION]
+            newCount = counts[CONTEXT] + counts[ADDITION]
+            oldStart = hunk["oldStart"]
+            newStart = oldStart + startOffset
+            if oldCount == 0:
+                newStart += 1
+            if newCount == 0:
+                newStart -= 1
+            
+            startOffset += counts[ADDITION] - counts[DELETION]
+            
+            patch.append(self.__generateHunkHeader(oldStart, oldCount,
+                                                   newStart, newCount,
+                                                   hunk["heading"]))
+            patch.extend(processedLines)
+        
+        if not patch:
+            return ""
+        else:
+            return "".join(self.__headerLines[:] + patch)

eric ide

mercurial