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 15 matching lines...) Expand all Loading... |
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 is_new = False |
33 | 33 |
34 def __init__(self, filename): | 34 def __init__(self, filename): |
35 self.filename = self._process_filename(filename) | 35 self.filename = self._process_filename(filename) |
| 36 # Set when the file is copied or moved. |
| 37 self.source_filename = None |
36 | 38 |
37 @staticmethod | 39 @staticmethod |
38 def _process_filename(filename): | 40 def _process_filename(filename): |
39 filename = filename.replace('\\', '/') | 41 filename = filename.replace('\\', '/') |
40 # Blacklist a few characters for simplicity. | 42 # Blacklist a few characters for simplicity. |
41 for i in ('%', '$', '..', '\'', '"'): | 43 for i in ('%', '$', '..', '\'', '"'): |
42 if i in filename: | 44 if i in filename: |
43 raise UnsupportedPatchFormat( | 45 raise UnsupportedPatchFormat( |
44 filename, 'Can\'t use \'%s\' in filename.' % i) | 46 filename, 'Can\'t use \'%s\' in filename.' % i) |
45 for i in ('/', 'CON', 'COM'): | 47 for i in ('/', 'CON', 'COM'): |
46 if filename.startswith(i): | 48 if filename.startswith(i): |
47 raise UnsupportedPatchFormat( | 49 raise UnsupportedPatchFormat( |
48 filename, 'Filename can\'t start with \'%s\'.' % i) | 50 filename, 'Filename can\'t start with \'%s\'.' % i) |
49 return filename | 51 return filename |
50 | 52 |
51 def get(self): # pragma: no coverage | 53 def get(self): # pragma: no coverage |
52 raise NotImplementedError('Nothing to grab') | 54 raise NotImplementedError('Nothing to grab') |
53 | 55 |
54 def set_relpath(self, relpath): | 56 def set_relpath(self, relpath): |
55 if not relpath: | 57 if not relpath: |
56 return | 58 return |
57 relpath = relpath.replace('\\', '/') | 59 relpath = relpath.replace('\\', '/') |
58 if relpath[0] == '/': | 60 if relpath[0] == '/': |
59 self._fail('Relative path starts with %s' % relpath[0]) | 61 self._fail('Relative path starts with %s' % relpath[0]) |
60 self.filename = self._process_filename( | 62 self.filename = self._process_filename( |
61 posixpath.join(relpath, self.filename)) | 63 posixpath.join(relpath, self.filename)) |
| 64 if self.source_filename: |
| 65 self.source_filename = self._process_filename( |
| 66 posixpath.join(relpath, self.source_filename)) |
62 | 67 |
63 def _fail(self, msg): | 68 def _fail(self, msg): |
64 """Shortcut function to raise UnsupportedPatchFormat.""" | 69 """Shortcut function to raise UnsupportedPatchFormat.""" |
65 raise UnsupportedPatchFormat(self.filename, msg) | 70 raise UnsupportedPatchFormat(self.filename, msg) |
66 | 71 |
67 | 72 |
68 class FilePatchDelete(FilePatchBase): | 73 class FilePatchDelete(FilePatchBase): |
69 """Deletes a file.""" | 74 """Deletes a file.""" |
70 is_delete = True | 75 is_delete = True |
71 | 76 |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
105 if self.is_git_diff: | 110 if self.is_git_diff: |
106 self._verify_git_header() | 111 self._verify_git_header() |
107 else: | 112 else: |
108 self._verify_svn_header() | 113 self._verify_svn_header() |
109 | 114 |
110 def get(self): | 115 def get(self): |
111 return self.diff_header + self.diff_hunks | 116 return self.diff_header + self.diff_hunks |
112 | 117 |
113 def set_relpath(self, relpath): | 118 def set_relpath(self, relpath): |
114 old_filename = self.filename | 119 old_filename = self.filename |
| 120 old_source_filename = self.source_filename or self.filename |
115 super(FilePatchDiff, self).set_relpath(relpath) | 121 super(FilePatchDiff, self).set_relpath(relpath) |
116 # Update the header too. | 122 # Update the header too. |
117 self.diff_header = self.diff_header.replace(old_filename, self.filename) | 123 source_filename = self.source_filename or self.filename |
| 124 lines = self.diff_header.splitlines(True) |
| 125 for i, line in enumerate(lines): |
| 126 if line.startswith('diff --git'): |
| 127 lines[i] = line.replace( |
| 128 'a/' + old_source_filename, source_filename).replace( |
| 129 'b/' + old_filename, self.filename) |
| 130 elif re.match(r'^\w+ from .+$', line) or line.startswith('---'): |
| 131 lines[i] = line.replace(old_source_filename, source_filename) |
| 132 elif re.match(r'^\w+ to .+$', line) or line.startswith('+++'): |
| 133 lines[i] = line.replace(old_filename, self.filename) |
| 134 self.diff_header = ''.join(lines) |
118 | 135 |
119 def _split_header(self, diff): | 136 def _split_header(self, diff): |
120 """Splits a diff in two: the header and the hunks.""" | 137 """Splits a diff in two: the header and the hunks.""" |
121 header = [] | 138 header = [] |
122 hunks = diff.splitlines(True) | 139 hunks = diff.splitlines(True) |
123 while hunks: | 140 while hunks: |
124 header.append(hunks.pop(0)) | 141 header.append(hunks.pop(0)) |
125 if header[-1].startswith('--- '): | 142 if header[-1].startswith('--- '): |
126 break | 143 break |
127 else: | 144 else: |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
179 """ | 196 """ |
180 lines = self.diff_header.splitlines() | 197 lines = self.diff_header.splitlines() |
181 | 198 |
182 # Verify the diff --git line. | 199 # Verify the diff --git line. |
183 old = None | 200 old = None |
184 new = None | 201 new = None |
185 while lines: | 202 while lines: |
186 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0)) | 203 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0)) |
187 if not match: | 204 if not match: |
188 continue | 205 continue |
189 old = match.group(1).replace('\\', '/') | 206 if match.group(1).startswith('a/') and match.group(2).startswith('b/'): |
190 new = match.group(2).replace('\\', '/') | |
191 if old.startswith('a/') and new.startswith('b/'): | |
192 self.patchlevel = 1 | 207 self.patchlevel = 1 |
193 old = old[2:] | 208 old = self.mangle(match.group(1)) |
194 new = new[2:] | 209 new = self.mangle(match.group(2)) |
| 210 |
195 # The rename is about the new file so the old file can be anything. | 211 # The rename is about the new file so the old file can be anything. |
196 if new not in (self.filename, 'dev/null'): | 212 if new not in (self.filename, 'dev/null'): |
197 self._fail('Unexpected git diff output name %s.' % new) | 213 self._fail('Unexpected git diff output name %s.' % new) |
198 if old == 'dev/null' and new == 'dev/null': | 214 if old == 'dev/null' and new == 'dev/null': |
199 self._fail('Unexpected /dev/null git diff.') | 215 self._fail('Unexpected /dev/null git diff.') |
200 break | 216 break |
201 | 217 |
202 if not old or not new: | 218 if not old or not new: |
203 self._fail('Unexpected git diff; couldn\'t find git header.') | 219 self._fail('Unexpected git diff; couldn\'t find git header.') |
204 | 220 |
| 221 if old not in (self.filename, 'dev/null'): |
| 222 # Copy or rename. |
| 223 self.source_filename = old |
| 224 |
205 last_line = '' | 225 last_line = '' |
206 | 226 |
207 while lines: | 227 while lines: |
208 line = lines.pop(0) | 228 line = lines.pop(0) |
209 # TODO(maruel): old should be replace with self.source_file | 229 self._verify_git_header_process_line(lines, line, last_line) |
210 # TODO(maruel): new == self.filename and remove new | |
211 self._verify_git_header_process_line(lines, line, last_line, old, new) | |
212 last_line = line | 230 last_line = line |
213 | 231 |
214 # Cheap check to make sure the file name is at least mentioned in the | 232 # Cheap check to make sure the file name is at least mentioned in the |
215 # 'diff' header. That the only remaining invariant. | 233 # 'diff' header. That the only remaining invariant. |
216 if not self.filename in self.diff_header: | 234 if not self.filename in self.diff_header: |
217 self._fail('Diff seems corrupted.') | 235 self._fail('Diff seems corrupted.') |
218 | 236 |
219 def _verify_git_header_process_line(self, lines, line, last_line, old, new): | 237 def _verify_git_header_process_line(self, lines, line, last_line): |
220 """Processes a single line of the header. | 238 """Processes a single line of the header. |
221 | 239 |
222 Returns True if it should continue looping. | 240 Returns True if it should continue looping. |
223 | 241 |
224 Format is described to | 242 Format is described to |
225 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html | 243 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html |
226 """ | 244 """ |
227 match = re.match(r'^(rename|copy) from (.+)$', line) | 245 match = re.match(r'^(rename|copy) from (.+)$', line) |
| 246 old = self.source_filename or self.filename |
228 if match: | 247 if match: |
229 if old != match.group(2): | 248 if old != match.group(2): |
230 self._fail('Unexpected git diff input name for line %s.' % line) | 249 self._fail('Unexpected git diff input name for line %s.' % line) |
231 if not lines or not lines[0].startswith('%s to ' % match.group(1)): | 250 if not lines or not lines[0].startswith('%s to ' % match.group(1)): |
232 self._fail( | 251 self._fail( |
233 'Confused %s from/to git diff for line %s.' % | 252 'Confused %s from/to git diff for line %s.' % |
234 (match.group(1), line)) | 253 (match.group(1), line)) |
235 return | 254 return |
236 | 255 |
237 match = re.match(r'^(rename|copy) to (.+)$', line) | 256 match = re.match(r'^(rename|copy) to (.+)$', line) |
238 if match: | 257 if match: |
239 if new != match.group(2): | 258 if self.filename != match.group(2): |
240 self._fail('Unexpected git diff output name for line %s.' % line) | 259 self._fail('Unexpected git diff output name for line %s.' % line) |
241 if not last_line.startswith('%s from ' % match.group(1)): | 260 if not last_line.startswith('%s from ' % match.group(1)): |
242 self._fail( | 261 self._fail( |
243 'Confused %s from/to git diff for line %s.' % | 262 'Confused %s from/to git diff for line %s.' % |
244 (match.group(1), line)) | 263 (match.group(1), line)) |
245 return | 264 return |
246 | 265 |
247 match = re.match(r'^new(| file) mode (\d{6})$', line) | 266 match = re.match(r'^new(| file) mode (\d{6})$', line) |
248 if match: | 267 if match: |
249 mode = match.group(2) | 268 mode = match.group(2) |
250 # Only look at owner ACL for executable. | 269 # Only look at owner ACL for executable. |
251 # TODO(maruel): Add support to remove a property. | 270 # TODO(maruel): Add support to remove a property. |
252 if bool(int(mode[4]) & 1): | 271 if bool(int(mode[4]) & 1): |
253 self.svn_properties.append(('svn:executable', '*')) | 272 self.svn_properties.append(('svn:executable', '*')) |
254 | 273 |
255 match = re.match(r'^--- (.*)$', line) | 274 match = re.match(r'^--- (.*)$', line) |
256 if match: | 275 if match: |
257 if last_line[:3] in ('---', '+++'): | 276 if last_line[:3] in ('---', '+++'): |
258 self._fail('--- and +++ are reversed') | 277 self._fail('--- and +++ are reversed') |
259 self.is_new = match.group(1) == '/dev/null' | 278 self.is_new = match.group(1) == '/dev/null' |
260 # TODO(maruel): Use self.source_file. | 279 # TODO(maruel): Use self.source_file. |
261 if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null': | 280 if self.mangle(match.group(1)) not in (old, 'dev/null'): |
262 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) | 281 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1))) |
263 if not lines or not lines[0].startswith('+++'): | 282 if not lines or not lines[0].startswith('+++'): |
264 self._fail('Missing git diff output name.') | 283 self._fail('Missing git diff output name.') |
265 return | 284 return |
266 | 285 |
267 match = re.match(r'^\+\+\+ (.*)$', line) | 286 match = re.match(r'^\+\+\+ (.*)$', line) |
268 if match: | 287 if match: |
269 if not last_line.startswith('---'): | 288 if not last_line.startswith('---'): |
270 self._fail('Unexpected git diff: --- not following +++.') | 289 self._fail('Unexpected git diff: --- not following +++.') |
271 # TODO(maruel): new == self.filename. | 290 # TODO(maruel): new == self.filename. |
272 if '/dev/null' == match.group(1): | 291 if '/dev/null' == match.group(1): |
273 self.is_delete = True | 292 self.is_delete = True |
274 elif new != self.mangle(match.group(1)): | 293 elif self.filename != self.mangle(match.group(1)): |
275 self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1))) | 294 self._fail( |
| 295 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1))) |
276 if lines: | 296 if lines: |
277 self._fail('Crap after +++') | 297 self._fail('Crap after +++') |
278 # We're done. | 298 # We're done. |
279 return | 299 return |
280 | 300 |
281 def _verify_svn_header(self): | 301 def _verify_svn_header(self): |
282 """Sanity checks the header. | 302 """Sanity checks the header. |
283 | 303 |
284 A svn diff can contain only property changes, in that case there will be no | 304 A svn diff can contain only property changes, in that case there will be no |
285 proper header. To make things worse, this property change header is | 305 proper header. To make things worse, this property change header is |
(...skipping 15 matching lines...) Expand all Loading... |
301 def _verify_svn_header_process_line(self, lines, line, last_line): | 321 def _verify_svn_header_process_line(self, lines, line, last_line): |
302 """Processes a single line of the header. | 322 """Processes a single line of the header. |
303 | 323 |
304 Returns True if it should continue looping. | 324 Returns True if it should continue looping. |
305 """ | 325 """ |
306 match = re.match(r'^--- ([^\t]+).*$', line) | 326 match = re.match(r'^--- ([^\t]+).*$', line) |
307 if match: | 327 if match: |
308 if last_line[:3] in ('---', '+++'): | 328 if last_line[:3] in ('---', '+++'): |
309 self._fail('--- and +++ are reversed') | 329 self._fail('--- and +++ are reversed') |
310 self.is_new = match.group(1) == '/dev/null' | 330 self.is_new = match.group(1) == '/dev/null' |
311 # For copy and renames, it's possible that the -- line doesn't match | 331 if (self.mangle(match.group(1)) != self.filename and |
312 # +++, so don't check match.group(1) to match self.filename or | 332 match.group(1) != '/dev/null'): |
313 # '/dev/null', it can be anything else. | 333 self.source_filename = match.group(1) |
314 # TODO(maruel): Handle rename/copy explicitly. | |
315 # if (self.mangle(match.group(1)) != self.filename and | |
316 # match.group(1) != '/dev/null'): | |
317 # self.source_file = match.group(1) | |
318 if not lines or not lines[0].startswith('+++'): | 334 if not lines or not lines[0].startswith('+++'): |
319 self._fail('Nothing after header.') | 335 self._fail('Nothing after header.') |
320 return | 336 return |
321 | 337 |
322 match = re.match(r'^\+\+\+ ([^\t]+).*$', line) | 338 match = re.match(r'^\+\+\+ ([^\t]+).*$', line) |
323 if match: | 339 if match: |
324 if not last_line.startswith('---'): | 340 if not last_line.startswith('---'): |
325 self._fail('Unexpected diff: --- not following +++.') | 341 self._fail('Unexpected diff: --- not following +++.') |
326 if match.group(1) == '/dev/null': | 342 if match.group(1) == '/dev/null': |
327 self.is_delete = True | 343 self.is_delete = True |
(...skipping 18 matching lines...) Expand all Loading... |
346 for patch in self.patches: | 362 for patch in self.patches: |
347 patch.set_relpath(relpath) | 363 patch.set_relpath(relpath) |
348 | 364 |
349 def __iter__(self): | 365 def __iter__(self): |
350 for patch in self.patches: | 366 for patch in self.patches: |
351 yield patch | 367 yield patch |
352 | 368 |
353 @property | 369 @property |
354 def filenames(self): | 370 def filenames(self): |
355 return [p.filename for p in self.patches] | 371 return [p.filename for p in self.patches] |
OLD | NEW |