Chromium Code Reviews| 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 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 146 # Rename partial change: | 148 # Rename partial change: |
| 147 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff | 149 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff |
| 148 # Rename no change: | 150 # Rename no change: |
| 149 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff | 151 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff |
| 150 return any(l.startswith('diff --git') for l in diff_header.splitlines()) | 152 return any(l.startswith('diff --git') for l in diff_header.splitlines()) |
| 151 | 153 |
| 152 def mangle(self, string): | 154 def mangle(self, string): |
| 153 """Mangle a file path.""" | 155 """Mangle a file path.""" |
| 154 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) | 156 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) |
| 155 | 157 |
| 156 def _verify_git_header(self): | 158 def _verify_git_header(self): |
|
Dirk Pranke
2011/06/03 19:17:27
This function is getting awfully long. Can it be b
M-A Ruel
2011/06/03 19:42:31
done
| |
| 157 """Sanity checks the header. | 159 """Sanity checks the header. |
| 158 | 160 |
| 159 Expects the following format: | 161 Expects the following format: |
| 160 | 162 |
| 161 <garbagge> | 163 <garbagge> |
| 162 diff --git (|a/)<filename> (|b/)<filename> | 164 diff --git (|a/)<filename> (|b/)<filename> |
| 163 <similarity> | 165 <similarity> |
| 164 <filemode changes> | 166 <filemode changes> |
| 165 <index> | 167 <index> |
| 166 <copy|rename from> | 168 <copy|rename from> |
| (...skipping 21 matching lines...) Expand all 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 |
| 198 # Handle these: | 200 last_line = '' |
| 199 # new file mode \d{6} | 201 |
| 200 # rename from <> | 202 def process_line(line): |
|
Dirk Pranke
2011/06/03 19:17:27
Does this really need to be a nested function?
M-A Ruel
2011/06/03 19:42:31
not anymore. so much state to keep
| |
| 201 # rename to <> | 203 """Processes a single line of the header. |
| 202 # copy from <> | 204 |
| 203 # copy to <> | 205 Returns True if it should continue looping. |
| 204 while lines: | 206 """ |
| 205 if lines[0].startswith('--- '): | 207 # Handle these: |
| 206 break | 208 # rename from <> |
| 207 line = lines.pop(0) | 209 # copy from <> |
| 208 match = re.match(r'^(rename|copy) from (.+)$', line) | 210 match = re.match(r'^(rename|copy) from (.+)$', line) |
| 209 if match: | 211 if match: |
| 210 if old != match.group(2): | 212 if old != match.group(2): |
| 211 self._fail('Unexpected git diff input name for %s.' % match.group(1)) | 213 self._fail('Unexpected git diff input name for line %s.' % line) |
| 212 if not lines: | 214 if not lines or not lines[0].startswith('%s to ' % match.group(1)): |
| 213 self._fail('Missing git diff output name for %s.' % match.group(1)) | 215 self._fail( |
| 214 match = re.match(r'^(rename|copy) to (.+)$', lines.pop(0)) | 216 'Confused %s from/to git diff for line %s.' % |
| 215 if not match: | 217 (match.group(1), line)) |
| 216 self._fail('Missing git diff output name for %s.' % match.group(1)) | 218 return |
| 219 | |
| 220 # Handle these: | |
| 221 # rename to <> | |
| 222 # copy to <> | |
| 223 match = re.match(r'^(rename|copy) to (.+)$', line) | |
| 224 if match: | |
| 217 if new != match.group(2): | 225 if new != match.group(2): |
| 218 self._fail('Unexpected git diff output name for %s.' % match.group(1)) | 226 self._fail('Unexpected git diff output name for line %s.' % line) |
| 219 continue | 227 if not last_line.startswith('%s from ' % match.group(1)): |
| 228 self._fail( | |
| 229 'Confused %s from/to git diff for line %s.' % | |
| 230 (match.group(1), line)) | |
| 231 return | |
| 220 | 232 |
| 233 # Handle "new file mode \d{6}" | |
| 221 match = re.match(r'^new file mode (\d{6})$', line) | 234 match = re.match(r'^new file mode (\d{6})$', line) |
| 222 if match: | 235 if match: |
| 223 mode = match.group(1) | 236 mode = match.group(1) |
| 224 # Only look at owner ACL for executable. | 237 # Only look at owner ACL for executable. |
| 225 if bool(int(mode[4]) & 4): | 238 if bool(int(mode[4]) & 4): |
| 226 self.svn_properties.append(('svn:executable', '*')) | 239 self.svn_properties.append(('svn:executable', '*')) |
| 227 | 240 |
| 228 # Handle ---/+++ | 241 # Handle "--- " |
| 242 match = re.match(r'^--- (.*)$', line) | |
| 243 if match: | |
| 244 if last_line[:3] in ('---', '+++'): | |
| 245 self._fail('--- and +++ are reversed') | |
| 246 self.is_new = match.group(1) == '/dev/null' | |
| 247 # TODO(maruel): Use self.source_file. | |
| 248 if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null': | |
| 249 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) | |
| 250 if not lines or not lines[0].startswith('+++'): | |
| 251 self._fail('Missing git diff output name.') | |
| 252 return | |
| 253 | |
| 254 # Handle "+++ " | |
| 255 match = re.match(r'^\+\+\+ (.*)$', line) | |
| 256 if match: | |
| 257 if not last_line.startswith('---'): | |
| 258 self._fail('Unexpected git diff: --- not following +++.') | |
| 259 # TODO(maruel): new == self.filename. | |
| 260 if new != self.mangle(match.group(1)) and '/dev/null' != match.group(1): | |
| 261 # TODO(maruel): Can +++ be /dev/null? If so, assert self.is_delete == | |
| 262 # True. | |
| 263 self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1))) | |
| 264 if lines: | |
| 265 self._fail('Crap after +++') | |
| 266 # We're done. | |
| 267 return | |
| 268 | |
| 229 while lines: | 269 while lines: |
| 230 match = re.match(r'^--- (.*)$', lines.pop(0)) | 270 line = lines.pop(0) |
| 231 if not match: | 271 process_line(line) |
| 232 continue | 272 last_line = line |
| 233 if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null': | 273 |
| 234 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) | 274 # Cheap check to make sure the file name is at least mentioned in the |
| 235 if not lines: | 275 # 'diff' header. That the only remaining invariant. |
| 236 self._fail('Missing git diff output name.') | 276 if not self.filename in self.diff_header: |
| 237 match = re.match(r'^\+\+\+ (.*)$', lines.pop(0)) | 277 self._fail('Diff seems corrupted.') |
| 238 if not match: | |
| 239 self._fail('Unexpected git diff: --- not following +++.') | |
| 240 if new != self.mangle(match.group(1)) and '/dev/null' != match.group(1): | |
| 241 self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1))) | |
| 242 assert not lines, '_split_header() is broken' | |
| 243 break | |
| 244 | 278 |
| 245 def _verify_svn_header(self): | 279 def _verify_svn_header(self): |
| 246 """Sanity checks the header. | 280 """Sanity checks the header. |
| 247 | 281 |
| 248 A svn diff can contain only property changes, in that case there will be no | 282 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 | 283 proper header. To make things worse, this property change header is |
| 250 localized. | 284 localized. |
| 251 """ | 285 """ |
| 252 lines = self.diff_header.splitlines() | 286 lines = self.diff_header.splitlines() |
| 287 last_line = '' | |
| 288 | |
| 289 def process_line(line): | |
| 290 """Processes a single line of the header. | |
| 291 | |
| 292 Returns True if it should continue looping. | |
| 293 """ | |
| 294 match = re.match(r'^--- ([^\t]+).*$', line) | |
| 295 if match: | |
| 296 if last_line[:3] in ('---', '+++'): | |
| 297 self._fail('--- and +++ are reversed') | |
| 298 self.is_new = match.group(1) == '/dev/null' | |
| 299 # For copy and renames, it's possible that the -- line doesn't match | |
| 300 # +++, so don't check match.group(1) to match self.filename or | |
| 301 # '/dev/null', it can be anything else. | |
| 302 # TODO(maruel): Handle rename/copy explicitly. | |
| 303 # if (self.mangle(match.group(1)) != self.filename and | |
| 304 # match.group(1) != '/dev/null'): | |
| 305 # self.source_file = match.group(1) | |
| 306 if not lines or not lines[0].startswith('+++'): | |
| 307 self._fail('Nothing after header.') | |
| 308 return | |
| 309 | |
| 310 match = re.match(r'^\+\+\+ ([^\t]+).*$', line) | |
| 311 if match: | |
| 312 if not last_line.startswith('---'): | |
| 313 self._fail('Unexpected diff: --- not following +++.') | |
| 314 if (self.mangle(match.group(1)) != self.filename and | |
| 315 match.group(1) != '/dev/null'): | |
| 316 # TODO(maruel): Can +++ be /dev/null? If so, assert self.is_delete == | |
| 317 # True. | |
| 318 self._fail('Unexpected diff: %s.' % match.group(1)) | |
| 319 if lines: | |
| 320 self._fail('Crap after +++') | |
| 321 # We're done. | |
| 322 return | |
| 323 | |
| 253 while lines: | 324 while lines: |
| 254 match = re.match(r'^--- ([^\t]+).*$', lines.pop(0)) | 325 line = lines.pop(0) |
| 255 if not match: | 326 process_line(line) |
| 256 continue | 327 last_line = line |
| 257 # For copy and renames, it's possible that the -- line doesn't match +++, | |
| 258 # so don't check match.group(1) to match self.filename or '/dev/null', it | |
| 259 # can be anything else. | |
| 260 # TODO(maruel): Handle rename/copy explicitly. | |
| 261 # if match.group(1) not in (self.filename, '/dev/null'): | |
| 262 # self.source_file = match.group(1) | |
| 263 if not lines: | |
| 264 self._fail('Nothing after header.') | |
| 265 match = re.match(r'^\+\+\+ ([^\t]+).*$', lines.pop(0)) | |
| 266 if not match: | |
| 267 self._fail('Unexpected diff: --- not following +++.') | |
| 268 if match.group(1) not in (self.filename, '/dev/null'): | |
| 269 self._fail('Unexpected diff: %s.' % match.group(1)) | |
| 270 assert not lines, '_split_header() is broken' | |
| 271 break | |
| 272 else: | |
| 273 # Cheap check to make sure the file name is at least mentioned in the | |
| 274 # 'diff' header. That the only remaining invariant. | |
| 275 if not self.filename in self.diff_header: | |
| 276 self._fail('Diff seems corrupted.') | |
| 277 | 328 |
| 329 # Cheap check to make sure the file name is at least mentioned in the | |
| 330 # 'diff' header. That the only remaining invariant. | |
| 331 if not self.filename in self.diff_header: | |
| 332 self._fail('Diff seems corrupted.') | |
| 278 | 333 |
| 279 class PatchSet(object): | 334 class PatchSet(object): |
| 280 """A list of FilePatch* objects.""" | 335 """A list of FilePatch* objects.""" |
| 281 | 336 |
| 282 def __init__(self, patches): | 337 def __init__(self, patches): |
| 283 self.patches = patches | 338 self.patches = patches |
| 284 for p in self.patches: | 339 for p in self.patches: |
| 285 assert isinstance(p, FilePatchBase) | 340 assert isinstance(p, FilePatchBase) |
| 286 | 341 |
| 287 def set_relpath(self, relpath): | 342 def set_relpath(self, relpath): |
| 288 """Used to offset the patch into a subdirectory.""" | 343 """Used to offset the patch into a subdirectory.""" |
| 289 for patch in self.patches: | 344 for patch in self.patches: |
| 290 patch.set_relpath(relpath) | 345 patch.set_relpath(relpath) |
| 291 | 346 |
| 292 def __iter__(self): | 347 def __iter__(self): |
| 293 for patch in self.patches: | 348 for patch in self.patches: |
| 294 yield patch | 349 yield patch |
| 295 | 350 |
| 296 @property | 351 @property |
| 297 def filenames(self): | 352 def filenames(self): |
| 298 return [p.filename for p in self.patches] | 353 return [p.filename for p in self.patches] |
| OLD | NEW |