| 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 11 matching lines...) Expand all Loading... |
| 22 return out | 22 return out |
| 23 | 23 |
| 24 | 24 |
| 25 class FilePatchBase(object): | 25 class FilePatchBase(object): |
| 26 """Defines a single file being modified. | 26 """Defines a single file being modified. |
| 27 | 27 |
| 28 '/' is always used instead of os.sep for consistency. | 28 '/' is always used instead of os.sep for consistency. |
| 29 """ | 29 """ |
| 30 is_delete = False | 30 is_delete = False |
| 31 is_binary = False | 31 is_binary = False |
| 32 is_new = False |
| 32 | 33 |
| 33 def __init__(self, filename): | 34 def __init__(self, filename): |
| 34 self.filename = None | 35 self.filename = None |
| 35 self._set_filename(filename) | 36 self._set_filename(filename) |
| 36 | 37 |
| 37 def _set_filename(self, filename): | 38 def _set_filename(self, filename): |
| 38 self.filename = filename.replace('\\', '/') | 39 self.filename = filename.replace('\\', '/') |
| 39 # Blacklist a few characters for simplicity. | 40 # Blacklist a few characters for simplicity. |
| 40 for i in ('%', '$', '..', '\'', '"'): | 41 for i in ('%', '$', '..', '\'', '"'): |
| 41 if i in self.filename: | 42 if i in self.filename: |
| 42 self._fail('Can\'t use \'%s\' in filename.' % i) | 43 self._fail('Can\'t use \'%s\' in filename.' % i) |
| 43 for i in ('/', 'CON', 'COM'): | 44 for i in ('/', 'CON', 'COM'): |
| 44 if self.filename.startswith(i): | 45 if self.filename.startswith(i): |
| 45 self._fail('Filename can\'t start with \'%s\'.' % i) | 46 self._fail('Filename can\'t start with \'%s\'.' % i) |
| 46 | 47 |
| 47 def get(self): | 48 def get(self): # pragma: no coverage |
| 48 raise NotImplementedError('Nothing to grab') | 49 raise NotImplementedError('Nothing to grab') |
| 49 | 50 |
| 50 def set_relpath(self, relpath): | 51 def set_relpath(self, relpath): |
| 51 if not relpath: | 52 if not relpath: |
| 52 return | 53 return |
| 53 relpath = relpath.replace('\\', '/') | 54 relpath = relpath.replace('\\', '/') |
| 54 if relpath[0] == '/': | 55 if relpath[0] == '/': |
| 55 self._fail('Relative path starts with %s' % relpath[0]) | 56 self._fail('Relative path starts with %s' % relpath[0]) |
| 56 self._set_filename(posixpath.join(relpath, self.filename)) | 57 self._set_filename(posixpath.join(relpath, self.filename)) |
| 57 | 58 |
| (...skipping 10 matching lines...) Expand all Loading... |
| 68 self.is_binary = is_binary | 69 self.is_binary = is_binary |
| 69 | 70 |
| 70 def get(self): | 71 def get(self): |
| 71 raise NotImplementedError('Nothing to grab') | 72 raise NotImplementedError('Nothing to grab') |
| 72 | 73 |
| 73 | 74 |
| 74 class FilePatchBinary(FilePatchBase): | 75 class FilePatchBinary(FilePatchBase): |
| 75 """Content of a new binary file.""" | 76 """Content of a new binary file.""" |
| 76 is_binary = True | 77 is_binary = True |
| 77 | 78 |
| 78 def __init__(self, filename, data, svn_properties): | 79 def __init__(self, filename, data, svn_properties, is_new): |
| 79 super(FilePatchBinary, self).__init__(filename) | 80 super(FilePatchBinary, self).__init__(filename) |
| 80 self.data = data | 81 self.data = data |
| 81 self.svn_properties = svn_properties or [] | 82 self.svn_properties = svn_properties or [] |
| 83 self.is_new = is_new |
| 82 | 84 |
| 83 def get(self): | 85 def get(self): |
| 84 return self.data | 86 return self.data |
| 85 | 87 |
| 86 | 88 |
| 87 class FilePatchDiff(FilePatchBase): | 89 class FilePatchDiff(FilePatchBase): |
| 88 """Patch for a single file.""" | 90 """Patch for a single file.""" |
| 89 | 91 |
| 90 def __init__(self, filename, diff, svn_properties): | 92 def __init__(self, filename, diff, svn_properties): |
| 91 super(FilePatchDiff, self).__init__(filename) | 93 super(FilePatchDiff, self).__init__(filename) |
| (...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 188 # The rename is about the new file so the old file can be anything. | 190 # The rename is about the new file so the old file can be anything. |
| 189 if new not in (self.filename, 'dev/null'): | 191 if new not in (self.filename, 'dev/null'): |
| 190 self._fail('Unexpected git diff output name %s.' % new) | 192 self._fail('Unexpected git diff output name %s.' % new) |
| 191 if old == 'dev/null' and new == 'dev/null': | 193 if old == 'dev/null' and new == 'dev/null': |
| 192 self._fail('Unexpected /dev/null git diff.') | 194 self._fail('Unexpected /dev/null git diff.') |
| 193 break | 195 break |
| 194 | 196 |
| 195 if not old or not new: | 197 if not old or not new: |
| 196 self._fail('Unexpected git diff; couldn\'t find git header.') | 198 self._fail('Unexpected git diff; couldn\'t find git header.') |
| 197 | 199 |
| 200 last_line = '' |
| 201 |
| 202 while lines: |
| 203 line = lines.pop(0) |
| 204 # TODO(maruel): old should be replace with self.source_file |
| 205 # TODO(maruel): new == self.filename and remove new |
| 206 self._verify_git_header_process_line(lines, line, last_line, old, new) |
| 207 last_line = line |
| 208 |
| 209 # Cheap check to make sure the file name is at least mentioned in the |
| 210 # 'diff' header. That the only remaining invariant. |
| 211 if not self.filename in self.diff_header: |
| 212 self._fail('Diff seems corrupted.') |
| 213 |
| 214 def _verify_git_header_process_line(self, lines, line, last_line, old, new): |
| 215 """Processes a single line of the header. |
| 216 |
| 217 Returns True if it should continue looping. |
| 218 """ |
| 198 # Handle these: | 219 # Handle these: |
| 199 # new file mode \d{6} | 220 # rename from <> |
| 200 # rename from <> | 221 # copy from <> |
| 201 # rename to <> | 222 match = re.match(r'^(rename|copy) from (.+)$', line) |
| 202 # copy from <> | 223 if match: |
| 203 # copy to <> | 224 if old != match.group(2): |
| 204 while lines: | 225 self._fail('Unexpected git diff input name for line %s.' % line) |
| 205 if lines[0].startswith('--- '): | 226 if not lines or not lines[0].startswith('%s to ' % match.group(1)): |
| 206 break | 227 self._fail( |
| 207 line = lines.pop(0) | 228 'Confused %s from/to git diff for line %s.' % |
| 208 match = re.match(r'^(rename|copy) from (.+)$', line) | 229 (match.group(1), line)) |
| 209 if match: | 230 return |
| 210 if old != match.group(2): | |
| 211 self._fail('Unexpected git diff input name for %s.' % match.group(1)) | |
| 212 if not lines: | |
| 213 self._fail('Missing git diff output name for %s.' % match.group(1)) | |
| 214 match = re.match(r'^(rename|copy) to (.+)$', lines.pop(0)) | |
| 215 if not match: | |
| 216 self._fail('Missing git diff output name for %s.' % match.group(1)) | |
| 217 if new != match.group(2): | |
| 218 self._fail('Unexpected git diff output name for %s.' % match.group(1)) | |
| 219 continue | |
| 220 | 231 |
| 221 match = re.match(r'^new file mode (\d{6})$', line) | 232 # Handle these: |
| 222 if match: | 233 # rename to <> |
| 223 mode = match.group(1) | 234 # copy to <> |
| 224 # Only look at owner ACL for executable. | 235 match = re.match(r'^(rename|copy) to (.+)$', line) |
| 225 if bool(int(mode[4]) & 4): | 236 if match: |
| 226 self.svn_properties.append(('svn:executable', '*')) | 237 if new != match.group(2): |
| 238 self._fail('Unexpected git diff output name for line %s.' % line) |
| 239 if not last_line.startswith('%s from ' % match.group(1)): |
| 240 self._fail( |
| 241 'Confused %s from/to git diff for line %s.' % |
| 242 (match.group(1), line)) |
| 243 return |
| 227 | 244 |
| 228 # Handle ---/+++ | 245 # Handle "new file mode \d{6}" |
| 229 while lines: | 246 match = re.match(r'^new file mode (\d{6})$', line) |
| 230 match = re.match(r'^--- (.*)$', lines.pop(0)) | 247 if match: |
| 231 if not match: | 248 mode = match.group(1) |
| 232 continue | 249 # Only look at owner ACL for executable. |
| 250 if bool(int(mode[4]) & 4): |
| 251 self.svn_properties.append(('svn:executable', '*')) |
| 252 |
| 253 # Handle "--- " |
| 254 match = re.match(r'^--- (.*)$', line) |
| 255 if match: |
| 256 if last_line[:3] in ('---', '+++'): |
| 257 self._fail('--- and +++ are reversed') |
| 258 self.is_new = match.group(1) == '/dev/null' |
| 259 # TODO(maruel): Use self.source_file. |
| 233 if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null': | 260 if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null': |
| 234 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) | 261 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) |
| 235 if not lines: | 262 if not lines or not lines[0].startswith('+++'): |
| 236 self._fail('Missing git diff output name.') | 263 self._fail('Missing git diff output name.') |
| 237 match = re.match(r'^\+\+\+ (.*)$', lines.pop(0)) | 264 return |
| 238 if not match: | 265 |
| 266 # Handle "+++ " |
| 267 match = re.match(r'^\+\+\+ (.*)$', line) |
| 268 if match: |
| 269 if not last_line.startswith('---'): |
| 239 self._fail('Unexpected git diff: --- not following +++.') | 270 self._fail('Unexpected git diff: --- not following +++.') |
| 271 # TODO(maruel): new == self.filename. |
| 240 if new != self.mangle(match.group(1)) and '/dev/null' != match.group(1): | 272 if new != self.mangle(match.group(1)) and '/dev/null' != match.group(1): |
| 273 # TODO(maruel): Can +++ be /dev/null? If so, assert self.is_delete == |
| 274 # True. |
| 241 self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1))) | 275 self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1))) |
| 242 assert not lines, '_split_header() is broken' | 276 if lines: |
| 243 break | 277 self._fail('Crap after +++') |
| 278 # We're done. |
| 279 return |
| 244 | 280 |
| 245 def _verify_svn_header(self): | 281 def _verify_svn_header(self): |
| 246 """Sanity checks the header. | 282 """Sanity checks the header. |
| 247 | 283 |
| 248 A svn diff can contain only property changes, in that case there will be no | 284 A svn diff can contain only property changes, in that case there will be no |
| 249 proper header. To make things worse, this property change header is | 285 proper header. To make things worse, this property change header is |
| 250 localized. | 286 localized. |
| 251 """ | 287 """ |
| 252 lines = self.diff_header.splitlines() | 288 lines = self.diff_header.splitlines() |
| 289 last_line = '' |
| 290 |
| 253 while lines: | 291 while lines: |
| 254 match = re.match(r'^--- ([^\t]+).*$', lines.pop(0)) | 292 line = lines.pop(0) |
| 255 if not match: | 293 self._verify_svn_header_process_line(lines, line, last_line) |
| 256 continue | 294 last_line = line |
| 257 # For copy and renames, it's possible that the -- line doesn't match +++, | 295 |
| 258 # so don't check match.group(1) to match self.filename or '/dev/null', it | 296 # Cheap check to make sure the file name is at least mentioned in the |
| 259 # can be anything else. | 297 # 'diff' header. That the only remaining invariant. |
| 298 if not self.filename in self.diff_header: |
| 299 self._fail('Diff seems corrupted.') |
| 300 |
| 301 def _verify_svn_header_process_line(self, lines, line, last_line): |
| 302 """Processes a single line of the header. |
| 303 |
| 304 Returns True if it should continue looping. |
| 305 """ |
| 306 match = re.match(r'^--- ([^\t]+).*$', line) |
| 307 if match: |
| 308 if last_line[:3] in ('---', '+++'): |
| 309 self._fail('--- and +++ are reversed') |
| 310 self.is_new = match.group(1) == '/dev/null' |
| 311 # For copy and renames, it's possible that the -- line doesn't match |
| 312 # +++, so don't check match.group(1) to match self.filename or |
| 313 # '/dev/null', it can be anything else. |
| 260 # TODO(maruel): Handle rename/copy explicitly. | 314 # TODO(maruel): Handle rename/copy explicitly. |
| 261 # if match.group(1) not in (self.filename, '/dev/null'): | 315 # if (self.mangle(match.group(1)) != self.filename and |
| 316 # match.group(1) != '/dev/null'): |
| 262 # self.source_file = match.group(1) | 317 # self.source_file = match.group(1) |
| 263 if not lines: | 318 if not lines or not lines[0].startswith('+++'): |
| 264 self._fail('Nothing after header.') | 319 self._fail('Nothing after header.') |
| 265 match = re.match(r'^\+\+\+ ([^\t]+).*$', lines.pop(0)) | 320 return |
| 266 if not match: | 321 |
| 322 match = re.match(r'^\+\+\+ ([^\t]+).*$', line) |
| 323 if match: |
| 324 if not last_line.startswith('---'): |
| 267 self._fail('Unexpected diff: --- not following +++.') | 325 self._fail('Unexpected diff: --- not following +++.') |
| 268 if match.group(1) not in (self.filename, '/dev/null'): | 326 if (self.mangle(match.group(1)) != self.filename and |
| 327 match.group(1) != '/dev/null'): |
| 328 # TODO(maruel): Can +++ be /dev/null? If so, assert self.is_delete == |
| 329 # True. |
| 269 self._fail('Unexpected diff: %s.' % match.group(1)) | 330 self._fail('Unexpected diff: %s.' % match.group(1)) |
| 270 assert not lines, '_split_header() is broken' | 331 if lines: |
| 271 break | 332 self._fail('Crap after +++') |
| 272 else: | 333 # We're done. |
| 273 # Cheap check to make sure the file name is at least mentioned in the | 334 return |
| 274 # 'diff' header. That the only remaining invariant. | |
| 275 if not self.filename in self.diff_header: | |
| 276 self._fail('Diff seems corrupted.') | |
| 277 | 335 |
| 278 | 336 |
| 279 class PatchSet(object): | 337 class PatchSet(object): |
| 280 """A list of FilePatch* objects.""" | 338 """A list of FilePatch* objects.""" |
| 281 | 339 |
| 282 def __init__(self, patches): | 340 def __init__(self, patches): |
| 283 self.patches = patches | 341 self.patches = patches |
| 284 for p in self.patches: | 342 for p in self.patches: |
| 285 assert isinstance(p, FilePatchBase) | 343 assert isinstance(p, FilePatchBase) |
| 286 | 344 |
| 287 def set_relpath(self, relpath): | 345 def set_relpath(self, relpath): |
| 288 """Used to offset the patch into a subdirectory.""" | 346 """Used to offset the patch into a subdirectory.""" |
| 289 for patch in self.patches: | 347 for patch in self.patches: |
| 290 patch.set_relpath(relpath) | 348 patch.set_relpath(relpath) |
| 291 | 349 |
| 292 def __iter__(self): | 350 def __iter__(self): |
| 293 for patch in self.patches: | 351 for patch in self.patches: |
| 294 yield patch | 352 yield patch |
| 295 | 353 |
| 296 @property | 354 @property |
| 297 def filenames(self): | 355 def filenames(self): |
| 298 return [p.filename for p in self.patches] | 356 return [p.filename for p in self.patches] |
| OLD | NEW |