| OLD | NEW |
| 1 # coding=utf8 | 1 # coding=utf8 |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 """Utility functions to handle patches.""" | 5 """Utility functions to handle patches.""" |
| 6 | 6 |
| 7 import posixpath | 7 import posixpath |
| 8 import os | 8 import os |
| 9 import re | 9 import re |
| 10 | 10 |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 79 else: | 79 else: |
| 80 out += ' ' | 80 out += ' ' |
| 81 if self.is_new: | 81 if self.is_new: |
| 82 out += 'N' | 82 out += 'N' |
| 83 else: | 83 else: |
| 84 out += ' ' | 84 out += ' ' |
| 85 if self.source_filename: | 85 if self.source_filename: |
| 86 out += 'R' | 86 out += 'R' |
| 87 else: | 87 else: |
| 88 out += ' ' | 88 out += ' ' |
| 89 return out + ' %s->%s' % (self.source_filename, self.filename) | 89 out += ' ' |
| 90 if self.source_filename: |
| 91 out += '%s->' % self.source_filename |
| 92 return out + str(self.filename) |
| 90 | 93 |
| 91 | 94 |
| 92 class FilePatchDelete(FilePatchBase): | 95 class FilePatchDelete(FilePatchBase): |
| 93 """Deletes a file.""" | 96 """Deletes a file.""" |
| 94 is_delete = True | 97 is_delete = True |
| 95 | 98 |
| 96 def __init__(self, filename, is_binary): | 99 def __init__(self, filename, is_binary): |
| 97 super(FilePatchDelete, self).__init__(filename) | 100 super(FilePatchDelete, self).__init__(filename) |
| 98 self.is_binary = is_binary | 101 self.is_binary = is_binary |
| 99 | 102 |
| 100 | 103 |
| 101 class FilePatchBinary(FilePatchBase): | 104 class FilePatchBinary(FilePatchBase): |
| 102 """Content of a new binary file.""" | 105 """Content of a new binary file.""" |
| 103 is_binary = True | 106 is_binary = True |
| 104 | 107 |
| 105 def __init__(self, filename, data, svn_properties, is_new): | 108 def __init__(self, filename, data, svn_properties, is_new): |
| 106 super(FilePatchBinary, self).__init__(filename) | 109 super(FilePatchBinary, self).__init__(filename) |
| 107 self.data = data | 110 self.data = data |
| 108 self.svn_properties = svn_properties or [] | 111 self.svn_properties = svn_properties or [] |
| 109 self.is_new = is_new | 112 self.is_new = is_new |
| 110 | 113 |
| 111 def get(self): | 114 def get(self): |
| 112 return self.data | 115 return self.data |
| 113 | 116 |
| 114 | 117 |
| 118 class Hunk(object): |
| 119 """Parsed hunk data container.""" |
| 120 |
| 121 def __init__(self, start_src, lines_src, start_dst, lines_dst): |
| 122 self.start_src = start_src |
| 123 self.lines_src = lines_src |
| 124 self.start_dst = start_dst |
| 125 self.lines_dst = lines_dst |
| 126 self.variation = self.lines_dst - self.lines_src |
| 127 self.text = [] |
| 128 |
| 129 |
| 115 class FilePatchDiff(FilePatchBase): | 130 class FilePatchDiff(FilePatchBase): |
| 116 """Patch for a single file.""" | 131 """Patch for a single file.""" |
| 117 | 132 |
| 118 def __init__(self, filename, diff, svn_properties): | 133 def __init__(self, filename, diff, svn_properties): |
| 119 super(FilePatchDiff, self).__init__(filename) | 134 super(FilePatchDiff, self).__init__(filename) |
| 120 if not diff: | 135 if not diff: |
| 121 self._fail('File doesn\'t have a diff.') | 136 self._fail('File doesn\'t have a diff.') |
| 122 self.diff_header, self.diff_hunks = self._split_header(diff) | 137 self.diff_header, self.diff_hunks = self._split_header(diff) |
| 123 self.svn_properties = svn_properties or [] | 138 self.svn_properties = svn_properties or [] |
| 124 self.is_git_diff = self._is_git_diff_header(self.diff_header) | 139 self.is_git_diff = self._is_git_diff_header(self.diff_header) |
| 125 self.patchlevel = 0 | 140 self.patchlevel = 0 |
| 126 if self.is_git_diff: | 141 if self.is_git_diff: |
| 127 self._verify_git_header() | 142 self._verify_git_header() |
| 128 else: | 143 else: |
| 129 self._verify_svn_header() | 144 self._verify_svn_header() |
| 145 self.hunks = self._split_hunks() |
| 130 if self.source_filename and not self.is_new: | 146 if self.source_filename and not self.is_new: |
| 131 self._fail('If source_filename is set, is_new must be also be set') | 147 self._fail('If source_filename is set, is_new must be also be set') |
| 132 | 148 |
| 133 def get(self, for_git): | 149 def get(self, for_git): |
| 134 if for_git or not self.source_filename: | 150 if for_git or not self.source_filename: |
| 135 return self.diff_header + self.diff_hunks | 151 return self.diff_header + self.diff_hunks |
| 136 else: | 152 else: |
| 137 # patch is stupid. It patches the source_filename instead so get rid of | 153 # patch is stupid. It patches the source_filename instead so get rid of |
| 138 # any source_filename reference if needed. | 154 # any source_filename reference if needed. |
| 139 return ( | 155 return ( |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 191 @staticmethod | 207 @staticmethod |
| 192 def _is_git_diff_header(diff_header): | 208 def _is_git_diff_header(diff_header): |
| 193 """Returns True if the diff for a single files was generated with git.""" | 209 """Returns True if the diff for a single files was generated with git.""" |
| 194 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff | 210 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff |
| 195 # Rename partial change: | 211 # Rename partial change: |
| 196 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff | 212 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff |
| 197 # Rename no change: | 213 # Rename no change: |
| 198 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff | 214 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff |
| 199 return any(l.startswith('diff --git') for l in diff_header.splitlines()) | 215 return any(l.startswith('diff --git') for l in diff_header.splitlines()) |
| 200 | 216 |
| 217 def _split_hunks(self): |
| 218 """Splits the hunks and does verification.""" |
| 219 hunks = [] |
| 220 for line in self.diff_hunks.splitlines(True): |
| 221 if line.startswith('@@'): |
| 222 match = re.match(r'^@@ -(\d+),(\d+) \+([\d,]+) @@.*$', line) |
| 223 # File add will result in "-0,0 +1" but file deletion will result in |
| 224 # "-1,N +0,0" where N is the number of lines deleted. That's from diff |
| 225 # and svn diff. git diff doesn't exhibit this behavior. |
| 226 if not match: |
| 227 self._fail('Hunk header is unparsable') |
| 228 if ',' in match.group(3): |
| 229 start_dst, lines_dst = map(int, match.group(3).split(',', 1)) |
| 230 else: |
| 231 start_dst = int(match.group(3)) |
| 232 lines_dst = 0 |
| 233 new_hunk = Hunk(int(match.group(1)), int(match.group(2)), |
| 234 start_dst, lines_dst) |
| 235 if hunks: |
| 236 if new_hunk.start_src <= hunks[-1].start_src: |
| 237 self._fail('Hunks source lines are not ordered') |
| 238 if new_hunk.start_dst <= hunks[-1].start_dst: |
| 239 self._fail('Hunks destination lines are not ordered') |
| 240 hunks.append(new_hunk) |
| 241 continue |
| 242 hunks[-1].text.append(line) |
| 243 |
| 244 if len(hunks) == 1: |
| 245 if hunks[0].start_src == 0 and hunks[0].lines_src == 0: |
| 246 self.is_new = True |
| 247 if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0: |
| 248 self.is_delete = True |
| 249 |
| 250 if self.is_new and self.is_delete: |
| 251 self._fail('Hunk header is all 0') |
| 252 |
| 253 if not self.is_new and not self.is_delete: |
| 254 for hunk in hunks: |
| 255 variation = ( |
| 256 len([1 for i in hunk.text if i.startswith('+')]) - |
| 257 len([1 for i in hunk.text if i.startswith('-')])) |
| 258 if variation != hunk.variation: |
| 259 self._fail( |
| 260 'Hunk header is incorrect: %d vs %d' % ( |
| 261 variation, hunk.variation)) |
| 262 if not hunk.start_src: |
| 263 self._fail( |
| 264 'Hunk header start line is incorrect: %d' % hunk.start_src) |
| 265 if not hunk.start_dst: |
| 266 self._fail( |
| 267 'Hunk header start line is incorrect: %d' % hunk.start_dst) |
| 268 hunk.start_src -= 1 |
| 269 hunk.start_dst -= 1 |
| 270 if self.is_new and hunks: |
| 271 hunks[0].start_dst -= 1 |
| 272 if self.is_delete and hunks: |
| 273 hunks[0].start_src -= 1 |
| 274 return hunks |
| 275 |
| 201 def mangle(self, string): | 276 def mangle(self, string): |
| 202 """Mangle a file path.""" | 277 """Mangle a file path.""" |
| 203 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) | 278 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) |
| 204 | 279 |
| 205 def _verify_git_header(self): | 280 def _verify_git_header(self): |
| 206 """Sanity checks the header. | 281 """Sanity checks the header. |
| 207 | 282 |
| 208 Expects the following format: | 283 Expects the following format: |
| 209 | 284 |
| 210 <garbagge> | 285 <garbagge> |
| (...skipping 203 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 414 def __iter__(self): | 489 def __iter__(self): |
| 415 for patch in self.patches: | 490 for patch in self.patches: |
| 416 yield patch | 491 yield patch |
| 417 | 492 |
| 418 def __getitem__(self, key): | 493 def __getitem__(self, key): |
| 419 return self.patches[key] | 494 return self.patches[key] |
| 420 | 495 |
| 421 @property | 496 @property |
| 422 def filenames(self): | 497 def filenames(self): |
| 423 return [p.filename for p in self.patches] | 498 return [p.filename for p in self.patches] |
| OLD | NEW |