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 |