Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(137)

Side by Side Diff: patch.py

Issue 7054048: Increase coverage to 91%; Much stricter about header parsing. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Address review comments Created 9 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | rietveld.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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]
OLDNEW
« no previous file with comments | « no previous file | rietveld.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698