Index: patch.py |
diff --git a/patch.py b/patch.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b22e32e55efbd5318d2fd3d922c5cd9e5048519e |
--- /dev/null |
+++ b/patch.py |
@@ -0,0 +1,184 @@ |
+# coding=utf8 |
+# Copyright (c) 2011 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+"""Utility functions to handle patches.""" |
+ |
+import re |
+ |
+ |
+class UnsupportedPatchFormat(Exception): |
+ def __init__(self, filename, status): |
+ super(UnsupportedPatchFormat, self).__init__(filename, status) |
+ self.filename = filename |
+ self.status = status |
+ |
+ def __str__(self): |
+ out = 'Can\'t process patch for file %s.' % self.filename |
+ if self.status: |
+ out += '\n%s' % self.status |
+ return out |
+ |
+ |
+class FilePatchBase(object): |
+ """Defines a single file being modified.""" |
+ is_delete = False |
+ is_binary = False |
+ |
+ def get(self): |
+ raise NotImplementedError('Nothing to grab') |
+ |
+ |
+class FilePatchDelete(FilePatchBase): |
+ """Deletes a file.""" |
+ is_delete = True |
+ |
+ def __init__(self, filename, is_binary): |
+ super(FilePatchDelete, self).__init__() |
+ self.filename = filename |
+ self.is_binary = is_binary |
+ |
+ def get(self): |
+ raise NotImplementedError('Nothing to grab') |
+ |
+ |
+class FilePatchBinary(FilePatchBase): |
+ """Content of a new binary file.""" |
+ is_binary = True |
+ |
+ def __init__(self, filename, data, svn_properties): |
+ super(FilePatchBinary, self).__init__() |
+ self.filename = filename |
+ self.data = data |
+ self.svn_properties = svn_properties or [] |
+ |
+ def get(self): |
+ return self.data |
+ |
+ |
+class FilePatchDiff(FilePatchBase): |
+ """Patch for a single file.""" |
+ |
+ def __init__(self, filename, diff, svn_properties): |
+ super(FilePatchDiff, self).__init__() |
+ self.filename = filename |
+ self.diff = diff |
+ self.svn_properties = svn_properties or [] |
+ self.is_git_diff = self._is_git_diff(diff) |
+ if self.is_git_diff: |
+ self.patchlevel = 1 |
+ self._verify_git_patch(filename, diff) |
+ assert not svn_properties |
+ else: |
+ self.patchlevel = 0 |
+ self._verify_svn_patch(filename, diff) |
+ |
+ def get(self): |
+ return self.diff |
+ |
+ @staticmethod |
+ def _is_git_diff(diff): |
+ """Returns True if the diff for a single files was generated with gt. |
+ |
+ Expects the following format: |
+ |
+ Index: <filename> |
+ diff --git a/<filename> b/<filename> |
+ <filemode changes> |
+ <index> |
+ --- <filename> |
+ +++ <filename> |
+ <hunks> |
+ |
+ Index: is a rietveld specific line. |
+ """ |
+ # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff |
+ # Rename partial change: |
+ # http://codereview.chromium.org/download/issue6250123_3013_6010.diff |
+ # Rename no change: |
+ # http://codereview.chromium.org/download/issue6287022_3001_4010.diff |
+ return any(l.startswith('diff --git') for l in diff.splitlines()[:3]) |
+ |
+ @staticmethod |
+ def _verify_git_patch(filename, diff): |
+ lines = diff.splitlines() |
+ # First fine the git diff header: |
+ while lines: |
+ line = lines.pop(0) |
+ match = re.match(r'^diff \-\-git a\/(.*?) b\/(.*)$', line) |
+ if match: |
+ a = match.group(1) |
+ b = match.group(2) |
+ if a != filename and a != 'dev/null': |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected git diff input name.') |
+ if b != filename and b != 'dev/null': |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected git diff output name.') |
+ if a == 'dev/null' and b == 'dev/null': |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected /dev/null git diff.') |
+ break |
+ else: |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected git diff; couldn\'t find git header.') |
+ |
+ while lines: |
+ line = lines.pop(0) |
+ match = re.match(r'^--- a/(.*)$', line) |
+ if match: |
+ if a != match.group(1): |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected git diff: %s != %s.' % (a, match.group(1))) |
+ match = re.match(r'^\+\+\+ b/(.*)$', lines.pop(0)) |
+ if not match: |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected git diff: --- not following +++.') |
+ if b != match.group(1): |
+ raise UnsupportedPatchFormat( |
+ filename, 'Unexpected git diff: %s != %s.' % (b, match.group(1))) |
+ break |
+ # Don't fail if the patch header is not found, the diff could be a |
+ # file-mode-change-only diff. |
+ |
+ @staticmethod |
+ def _verify_svn_patch(filename, diff): |
+ lines = diff.splitlines() |
+ while lines: |
+ line = lines.pop(0) |
+ match = re.match(r'^--- ([^\t]+).*$', line) |
+ if match: |
+ if match.group(1) not in (filename, '/dev/null'): |
+ raise UnsupportedPatchFormat( |
+ filename, |
+ 'Unexpected diff: %s != %s.' % (filename, match.group(1))) |
+ # Grab next line. |
+ line2 = lines.pop(0) |
+ match = re.match(r'^\+\+\+ ([^\t]+).*$', line2) |
+ if not match: |
+ raise UnsupportedPatchFormat( |
+ filename, |
+ 'Unexpected diff: --- not following +++.:\n%s\n%s' % ( |
+ line, line2)) |
+ if match.group(1) not in (filename, '/dev/null'): |
+ raise UnsupportedPatchFormat( |
+ filename, |
+ 'Unexpected diff: %s != %s.' % (filename, match.group(1))) |
+ break |
+ # Don't fail if the patch header is not found, the diff could be a |
+ # svn-property-change-only diff. |
+ |
+ |
+class PatchSet(object): |
+ """A list of FilePatch* objects.""" |
+ |
+ def __init__(self, patches): |
+ self.patches = patches |
+ |
+ def __iter__(self): |
+ for patch in self.patches: |
+ yield patch |
+ |
+ @property |
+ def filenames(self): |
+ return [p.filename for p in self.patches] |