Index: patch.py |
diff --git a/patch.py b/patch.py |
index 4a67cadfced14efa28c9e232399593b3dafaae02..74ed7a1409d3bbeb237a71e37456c1ae42953f2e 100644 |
--- a/patch.py |
+++ b/patch.py |
@@ -86,7 +86,10 @@ class FilePatchBase(object): |
out += 'R' |
else: |
out += ' ' |
- return out + ' %s->%s' % (self.source_filename, self.filename) |
+ out += ' ' |
+ if self.source_filename: |
+ out += '%s->' % self.source_filename |
+ return out + str(self.filename) |
class FilePatchDelete(FilePatchBase): |
@@ -112,6 +115,18 @@ class FilePatchBinary(FilePatchBase): |
return self.data |
+class Hunk(object): |
+ """Parsed hunk data container.""" |
+ |
+ def __init__(self, start_src, lines_src, start_dst, lines_dst): |
+ self.start_src = start_src |
+ self.lines_src = lines_src |
+ self.start_dst = start_dst |
+ self.lines_dst = lines_dst |
+ self.variation = self.lines_dst - self.lines_src |
+ self.text = [] |
+ |
+ |
class FilePatchDiff(FilePatchBase): |
"""Patch for a single file.""" |
@@ -127,6 +142,7 @@ class FilePatchDiff(FilePatchBase): |
self._verify_git_header() |
else: |
self._verify_svn_header() |
+ self.hunks = self._split_hunks() |
if self.source_filename and not self.is_new: |
self._fail('If source_filename is set, is_new must be also be set') |
@@ -198,6 +214,65 @@ class FilePatchDiff(FilePatchBase): |
# http://codereview.chromium.org/download/issue6287022_3001_4010.diff |
return any(l.startswith('diff --git') for l in diff_header.splitlines()) |
+ def _split_hunks(self): |
+ """Splits the hunks and does verification.""" |
+ hunks = [] |
+ for line in self.diff_hunks.splitlines(True): |
+ if line.startswith('@@'): |
+ match = re.match(r'^@@ -(\d+),(\d+) \+([\d,]+) @@.*$', line) |
+ # File add will result in "-0,0 +1" but file deletion will result in |
+ # "-1,N +0,0" where N is the number of lines deleted. That's from diff |
+ # and svn diff. git diff doesn't exhibit this behavior. |
+ if not match: |
+ self._fail('Hunk header is unparsable') |
+ if ',' in match.group(3): |
+ start_dst, lines_dst = map(int, match.group(3).split(',', 1)) |
+ else: |
+ start_dst = int(match.group(3)) |
+ lines_dst = 0 |
+ new_hunk = Hunk(int(match.group(1)), int(match.group(2)), |
+ start_dst, lines_dst) |
+ if hunks: |
+ if new_hunk.start_src <= hunks[-1].start_src: |
+ self._fail('Hunks source lines are not ordered') |
+ if new_hunk.start_dst <= hunks[-1].start_dst: |
+ self._fail('Hunks destination lines are not ordered') |
+ hunks.append(new_hunk) |
+ continue |
+ hunks[-1].text.append(line) |
+ |
+ if len(hunks) == 1: |
+ if hunks[0].start_src == 0 and hunks[0].lines_src == 0: |
+ self.is_new = True |
+ if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0: |
+ self.is_delete = True |
+ |
+ if self.is_new and self.is_delete: |
+ self._fail('Hunk header is all 0') |
+ |
+ if not self.is_new and not self.is_delete: |
+ for hunk in hunks: |
+ variation = ( |
+ len([1 for i in hunk.text if i.startswith('+')]) - |
+ len([1 for i in hunk.text if i.startswith('-')])) |
+ if variation != hunk.variation: |
+ self._fail( |
+ 'Hunk header is incorrect: %d vs %d' % ( |
+ variation, hunk.variation)) |
+ if not hunk.start_src: |
+ self._fail( |
+ 'Hunk header start line is incorrect: %d' % hunk.start_src) |
+ if not hunk.start_dst: |
+ self._fail( |
+ 'Hunk header start line is incorrect: %d' % hunk.start_dst) |
+ hunk.start_src -= 1 |
+ hunk.start_dst -= 1 |
+ if self.is_new and hunks: |
+ hunks[0].start_dst -= 1 |
+ if self.is_delete and hunks: |
+ hunks[0].start_src -= 1 |
+ return hunks |
+ |
def mangle(self, string): |
"""Mangle a file path.""" |
return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) |