blob: 69ba32305bf32a8acfaaa6b9352f06d34d12574d [file] [log] [blame]
maruel@chromium.orgb3727a32011-04-04 19:31:44 +00001# coding=utf8
maruel@chromium.orgcf602552012-01-10 19:49:31 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgb3727a32011-04-04 19:31:44 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Utility functions to handle patches."""
6
maruel@chromium.orgcd619402011-04-09 00:08:00 +00007import posixpath
8import os
maruel@chromium.orgb3727a32011-04-04 19:31:44 +00009import re
10
11
12class UnsupportedPatchFormat(Exception):
13 def __init__(self, filename, status):
14 super(UnsupportedPatchFormat, self).__init__(filename, status)
15 self.filename = filename
16 self.status = status
17
18 def __str__(self):
19 out = 'Can\'t process patch for file %s.' % self.filename
20 if self.status:
21 out += '\n%s' % self.status
22 return out
23
24
25class FilePatchBase(object):
maruel@chromium.orgcd619402011-04-09 00:08:00 +000026 """Defines a single file being modified.
27
28 '/' is always used instead of os.sep for consistency.
29 """
maruel@chromium.orgb3727a32011-04-04 19:31:44 +000030 is_delete = False
31 is_binary = False
maruel@chromium.org97366be2011-06-03 20:02:46 +000032 is_new = False
maruel@chromium.orgb3727a32011-04-04 19:31:44 +000033
maruel@chromium.orgcd619402011-04-09 00:08:00 +000034 def __init__(self, filename):
maruel@chromium.org5e975632011-09-29 18:07:06 +000035 assert self.__class__ is not FilePatchBase
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000036 self.filename = self._process_filename(filename)
maruel@chromium.orga19047c2011-09-08 12:49:58 +000037 # Set when the file is copied or moved.
38 self.source_filename = None
maruel@chromium.orgcd619402011-04-09 00:08:00 +000039
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000040 @staticmethod
41 def _process_filename(filename):
42 filename = filename.replace('\\', '/')
maruel@chromium.orgcd619402011-04-09 00:08:00 +000043 # Blacklist a few characters for simplicity.
44 for i in ('%', '$', '..', '\'', '"'):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000045 if i in filename:
46 raise UnsupportedPatchFormat(
47 filename, 'Can\'t use \'%s\' in filename.' % i)
maruel@chromium.orgcd619402011-04-09 00:08:00 +000048 for i in ('/', 'CON', 'COM'):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000049 if filename.startswith(i):
50 raise UnsupportedPatchFormat(
51 filename, 'Filename can\'t start with \'%s\'.' % i)
52 return filename
maruel@chromium.orgcd619402011-04-09 00:08:00 +000053
maruel@chromium.orgcd619402011-04-09 00:08:00 +000054 def set_relpath(self, relpath):
55 if not relpath:
56 return
57 relpath = relpath.replace('\\', '/')
58 if relpath[0] == '/':
59 self._fail('Relative path starts with %s' % relpath[0])
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000060 self.filename = self._process_filename(
61 posixpath.join(relpath, self.filename))
maruel@chromium.orga19047c2011-09-08 12:49:58 +000062 if self.source_filename:
63 self.source_filename = self._process_filename(
64 posixpath.join(relpath, self.source_filename))
maruel@chromium.orgcd619402011-04-09 00:08:00 +000065
66 def _fail(self, msg):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000067 """Shortcut function to raise UnsupportedPatchFormat."""
maruel@chromium.orgcd619402011-04-09 00:08:00 +000068 raise UnsupportedPatchFormat(self.filename, msg)
69
maruel@chromium.org5e975632011-09-29 18:07:06 +000070 def __str__(self):
71 # Use a status-like board.
72 out = ''
73 if self.is_binary:
74 out += 'B'
75 else:
76 out += ' '
77 if self.is_delete:
78 out += 'D'
79 else:
80 out += ' '
81 if self.is_new:
82 out += 'N'
83 else:
84 out += ' '
85 if self.source_filename:
86 out += 'R'
87 else:
88 out += ' '
maruel@chromium.orgcf602552012-01-10 19:49:31 +000089 out += ' '
90 if self.source_filename:
91 out += '%s->' % self.source_filename
92 return out + str(self.filename)
maruel@chromium.org5e975632011-09-29 18:07:06 +000093
maruel@chromium.orgb3727a32011-04-04 19:31:44 +000094
95class FilePatchDelete(FilePatchBase):
96 """Deletes a file."""
97 is_delete = True
98
99 def __init__(self, filename, is_binary):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000100 super(FilePatchDelete, self).__init__(filename)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000101 self.is_binary = is_binary
102
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000103
104class FilePatchBinary(FilePatchBase):
105 """Content of a new binary file."""
106 is_binary = True
107
maruel@chromium.org97366be2011-06-03 20:02:46 +0000108 def __init__(self, filename, data, svn_properties, is_new):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000109 super(FilePatchBinary, self).__init__(filename)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000110 self.data = data
111 self.svn_properties = svn_properties or []
maruel@chromium.org97366be2011-06-03 20:02:46 +0000112 self.is_new = is_new
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000113
114 def get(self):
115 return self.data
116
117
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000118class Hunk(object):
119 """Parsed hunk data container."""
120
121 def __init__(self, start_src, lines_src, start_dst, lines_dst):
122 self.start_src = start_src
123 self.lines_src = lines_src
124 self.start_dst = start_dst
125 self.lines_dst = lines_dst
126 self.variation = self.lines_dst - self.lines_src
127 self.text = []
128
129
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000130class FilePatchDiff(FilePatchBase):
131 """Patch for a single file."""
132
133 def __init__(self, filename, diff, svn_properties):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000134 super(FilePatchDiff, self).__init__(filename)
maruel@chromium.org61e0b692011-04-12 21:01:01 +0000135 if not diff:
136 self._fail('File doesn\'t have a diff.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000137 self.diff_header, self.diff_hunks = self._split_header(diff)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000138 self.svn_properties = svn_properties or []
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000139 self.is_git_diff = self._is_git_diff_header(self.diff_header)
140 self.patchlevel = 0
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000141 if self.is_git_diff:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000142 self._verify_git_header()
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000143 else:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000144 self._verify_svn_header()
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000145 self.hunks = self._split_hunks()
maruel@chromium.org5e975632011-09-29 18:07:06 +0000146 if self.source_filename and not self.is_new:
147 self._fail('If source_filename is set, is_new must be also be set')
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000148
maruel@chromium.org5e975632011-09-29 18:07:06 +0000149 def get(self, for_git):
150 if for_git or not self.source_filename:
151 return self.diff_header + self.diff_hunks
152 else:
153 # patch is stupid. It patches the source_filename instead so get rid of
154 # any source_filename reference if needed.
155 return (
156 self.diff_header.replace(self.source_filename, self.filename) +
157 self.diff_hunks)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000158
159 def set_relpath(self, relpath):
160 old_filename = self.filename
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000161 old_source_filename = self.source_filename or self.filename
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000162 super(FilePatchDiff, self).set_relpath(relpath)
163 # Update the header too.
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000164 source_filename = self.source_filename or self.filename
165 lines = self.diff_header.splitlines(True)
166 for i, line in enumerate(lines):
167 if line.startswith('diff --git'):
168 lines[i] = line.replace(
169 'a/' + old_source_filename, source_filename).replace(
170 'b/' + old_filename, self.filename)
171 elif re.match(r'^\w+ from .+$', line) or line.startswith('---'):
172 lines[i] = line.replace(old_source_filename, source_filename)
173 elif re.match(r'^\w+ to .+$', line) or line.startswith('+++'):
174 lines[i] = line.replace(old_filename, self.filename)
175 self.diff_header = ''.join(lines)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000176
177 def _split_header(self, diff):
178 """Splits a diff in two: the header and the hunks."""
179 header = []
180 hunks = diff.splitlines(True)
181 while hunks:
182 header.append(hunks.pop(0))
183 if header[-1].startswith('--- '):
184 break
185 else:
186 # Some diff may not have a ---/+++ set like a git rename with no change or
187 # a svn diff with only property change.
188 pass
189
190 if hunks:
191 if not hunks[0].startswith('+++ '):
192 self._fail('Inconsistent header')
193 header.append(hunks.pop(0))
194 if hunks:
195 if not hunks[0].startswith('@@ '):
196 self._fail('Inconsistent hunk header')
197
198 # Mangle any \\ in the header to /.
199 header_lines = ('Index:', 'diff', 'copy', 'rename', '+++', '---')
200 basename = os.path.basename(self.filename)
201 for i in xrange(len(header)):
202 if (header[i].split(' ', 1)[0] in header_lines or
203 header[i].endswith(basename)):
204 header[i] = header[i].replace('\\', '/')
205 return ''.join(header), ''.join(hunks)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000206
207 @staticmethod
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000208 def _is_git_diff_header(diff_header):
209 """Returns True if the diff for a single files was generated with git."""
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000210 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff
211 # Rename partial change:
212 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff
213 # Rename no change:
214 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000215 return any(l.startswith('diff --git') for l in diff_header.splitlines())
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000216
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000217 def _split_hunks(self):
218 """Splits the hunks and does verification."""
219 hunks = []
220 for line in self.diff_hunks.splitlines(True):
221 if line.startswith('@@'):
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000222 match = re.match(r'^@@ -([\d,]+) \+([\d,]+) @@.*$', line)
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000223 # File add will result in "-0,0 +1" but file deletion will result in
224 # "-1,N +0,0" where N is the number of lines deleted. That's from diff
225 # and svn diff. git diff doesn't exhibit this behavior.
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000226 # svn diff for a single line file rewrite "@@ -1 +1 @@". Fun.
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000227 if not match:
228 self._fail('Hunk header is unparsable')
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000229 if ',' in match.group(1):
230 start_src, lines_src = map(int, match.group(1).split(',', 1))
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000231 else:
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000232 start_src = int(match.group(1))
233 lines_src = 0
234 if ',' in match.group(2):
235 start_dst, lines_dst = map(int, match.group(2).split(',', 1))
236 else:
237 start_dst = int(match.group(2))
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000238 lines_dst = 0
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000239 new_hunk = Hunk(start_src, lines_src, start_dst, lines_dst)
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000240 if hunks:
241 if new_hunk.start_src <= hunks[-1].start_src:
242 self._fail('Hunks source lines are not ordered')
243 if new_hunk.start_dst <= hunks[-1].start_dst:
244 self._fail('Hunks destination lines are not ordered')
245 hunks.append(new_hunk)
246 continue
247 hunks[-1].text.append(line)
248
249 if len(hunks) == 1:
250 if hunks[0].start_src == 0 and hunks[0].lines_src == 0:
251 self.is_new = True
252 if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0:
253 self.is_delete = True
254
255 if self.is_new and self.is_delete:
256 self._fail('Hunk header is all 0')
257
258 if not self.is_new and not self.is_delete:
259 for hunk in hunks:
260 variation = (
261 len([1 for i in hunk.text if i.startswith('+')]) -
262 len([1 for i in hunk.text if i.startswith('-')]))
263 if variation != hunk.variation:
264 self._fail(
265 'Hunk header is incorrect: %d vs %d' % (
266 variation, hunk.variation))
267 if not hunk.start_src:
268 self._fail(
269 'Hunk header start line is incorrect: %d' % hunk.start_src)
270 if not hunk.start_dst:
271 self._fail(
272 'Hunk header start line is incorrect: %d' % hunk.start_dst)
273 hunk.start_src -= 1
274 hunk.start_dst -= 1
275 if self.is_new and hunks:
276 hunks[0].start_dst -= 1
277 if self.is_delete and hunks:
278 hunks[0].start_src -= 1
279 return hunks
280
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000281 def mangle(self, string):
282 """Mangle a file path."""
283 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:])
284
285 def _verify_git_header(self):
286 """Sanity checks the header.
287
288 Expects the following format:
289
290 <garbagge>
291 diff --git (|a/)<filename> (|b/)<filename>
292 <similarity>
293 <filemode changes>
294 <index>
295 <copy|rename from>
296 <copy|rename to>
297 --- <filename>
298 +++ <filename>
299
300 Everything is optional except the diff --git line.
301 """
302 lines = self.diff_header.splitlines()
303
304 # Verify the diff --git line.
305 old = None
306 new = None
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000307 while lines:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000308 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0))
309 if not match:
310 continue
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000311 if match.group(1).startswith('a/') and match.group(2).startswith('b/'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000312 self.patchlevel = 1
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000313 old = self.mangle(match.group(1))
314 new = self.mangle(match.group(2))
315
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000316 # The rename is about the new file so the old file can be anything.
317 if new not in (self.filename, 'dev/null'):
318 self._fail('Unexpected git diff output name %s.' % new)
319 if old == 'dev/null' and new == 'dev/null':
320 self._fail('Unexpected /dev/null git diff.')
321 break
322
323 if not old or not new:
324 self._fail('Unexpected git diff; couldn\'t find git header.')
325
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000326 if old not in (self.filename, 'dev/null'):
327 # Copy or rename.
328 self.source_filename = old
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000329 self.is_new = True
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000330
maruel@chromium.org97366be2011-06-03 20:02:46 +0000331 last_line = ''
332
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000333 while lines:
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000334 line = lines.pop(0)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000335 self._verify_git_header_process_line(lines, line, last_line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000336 last_line = line
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000337
maruel@chromium.org97366be2011-06-03 20:02:46 +0000338 # Cheap check to make sure the file name is at least mentioned in the
339 # 'diff' header. That the only remaining invariant.
340 if not self.filename in self.diff_header:
341 self._fail('Diff seems corrupted.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000342
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000343 def _verify_git_header_process_line(self, lines, line, last_line):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000344 """Processes a single line of the header.
345
346 Returns True if it should continue looping.
maruel@chromium.org378a4192011-06-06 13:36:02 +0000347
348 Format is described to
349 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html
maruel@chromium.org97366be2011-06-03 20:02:46 +0000350 """
maruel@chromium.org97366be2011-06-03 20:02:46 +0000351 match = re.match(r'^(rename|copy) from (.+)$', line)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000352 old = self.source_filename or self.filename
maruel@chromium.org97366be2011-06-03 20:02:46 +0000353 if match:
354 if old != match.group(2):
355 self._fail('Unexpected git diff input name for line %s.' % line)
356 if not lines or not lines[0].startswith('%s to ' % match.group(1)):
357 self._fail(
358 'Confused %s from/to git diff for line %s.' %
359 (match.group(1), line))
360 return
361
maruel@chromium.org97366be2011-06-03 20:02:46 +0000362 match = re.match(r'^(rename|copy) to (.+)$', line)
363 if match:
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000364 if self.filename != match.group(2):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000365 self._fail('Unexpected git diff output name for line %s.' % line)
366 if not last_line.startswith('%s from ' % match.group(1)):
367 self._fail(
368 'Confused %s from/to git diff for line %s.' %
369 (match.group(1), line))
370 return
371
maruel@chromium.org40052252011-11-11 20:54:55 +0000372 match = re.match(r'^deleted file mode (\d{6})$', line)
373 if match:
374 # It is necessary to parse it because there may be no hunk, like when the
375 # file was empty.
376 self.is_delete = True
377 return
378
maruel@chromium.org378a4192011-06-06 13:36:02 +0000379 match = re.match(r'^new(| file) mode (\d{6})$', line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000380 if match:
maruel@chromium.org378a4192011-06-06 13:36:02 +0000381 mode = match.group(2)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000382 # Only look at owner ACL for executable.
maruel@chromium.org378a4192011-06-06 13:36:02 +0000383 # TODO(maruel): Add support to remove a property.
maruel@chromium.org86eb9e72011-06-03 20:14:52 +0000384 if bool(int(mode[4]) & 1):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000385 self.svn_properties.append(('svn:executable', '*'))
maruel@chromium.org40052252011-11-11 20:54:55 +0000386 return
maruel@chromium.org97366be2011-06-03 20:02:46 +0000387
maruel@chromium.org97366be2011-06-03 20:02:46 +0000388 match = re.match(r'^--- (.*)$', line)
389 if match:
390 if last_line[:3] in ('---', '+++'):
391 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000392 if match.group(1) == '/dev/null':
393 self.is_new = True
394 elif self.mangle(match.group(1)) != old:
395 # git patches are always well formatted, do not allow random filenames.
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000396 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000397 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000398 self._fail('Missing git diff output name.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000399 return
400
maruel@chromium.org97366be2011-06-03 20:02:46 +0000401 match = re.match(r'^\+\+\+ (.*)$', line)
402 if match:
403 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000404 self._fail('Unexpected git diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000405 if '/dev/null' == match.group(1):
406 self.is_delete = True
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000407 elif self.filename != self.mangle(match.group(1)):
408 self._fail(
409 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000410 if lines:
411 self._fail('Crap after +++')
412 # We're done.
413 return
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000414
415 def _verify_svn_header(self):
416 """Sanity checks the header.
417
418 A svn diff can contain only property changes, in that case there will be no
419 proper header. To make things worse, this property change header is
420 localized.
421 """
422 lines = self.diff_header.splitlines()
maruel@chromium.org97366be2011-06-03 20:02:46 +0000423 last_line = ''
424
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000425 while lines:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000426 line = lines.pop(0)
427 self._verify_svn_header_process_line(lines, line, last_line)
428 last_line = line
429
430 # Cheap check to make sure the file name is at least mentioned in the
431 # 'diff' header. That the only remaining invariant.
432 if not self.filename in self.diff_header:
433 self._fail('Diff seems corrupted.')
434
435 def _verify_svn_header_process_line(self, lines, line, last_line):
436 """Processes a single line of the header.
437
438 Returns True if it should continue looping.
439 """
440 match = re.match(r'^--- ([^\t]+).*$', line)
441 if match:
442 if last_line[:3] in ('---', '+++'):
443 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000444 if match.group(1) == '/dev/null':
445 self.is_new = True
446 elif self.mangle(match.group(1)) != self.filename:
447 # guess the source filename.
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000448 self.source_filename = match.group(1)
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000449 self.is_new = True
maruel@chromium.org97366be2011-06-03 20:02:46 +0000450 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgc4b5e762011-04-20 23:56:08 +0000451 self._fail('Nothing after header.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000452 return
453
454 match = re.match(r'^\+\+\+ ([^\t]+).*$', line)
455 if match:
456 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000457 self._fail('Unexpected diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000458 if match.group(1) == '/dev/null':
459 self.is_delete = True
460 elif self.mangle(match.group(1)) != self.filename:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000461 self._fail('Unexpected diff: %s.' % match.group(1))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000462 if lines:
463 self._fail('Crap after +++')
464 # We're done.
465 return
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000466
467
468class PatchSet(object):
469 """A list of FilePatch* objects."""
470
471 def __init__(self, patches):
maruel@chromium.org5e975632011-09-29 18:07:06 +0000472 for p in patches:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000473 assert isinstance(p, FilePatchBase)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000474
maruel@chromium.org5e975632011-09-29 18:07:06 +0000475 def key(p):
476 """Sort by ordering of application.
477
478 File move are first.
479 Deletes are last.
480 """
481 if p.source_filename:
482 return (p.is_delete, p.source_filename, p.filename)
483 else:
484 # tuple are always greater than string, abuse that fact.
485 return (p.is_delete, (p.filename,), p.filename)
486
487 self.patches = sorted(patches, key=key)
488
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000489 def set_relpath(self, relpath):
490 """Used to offset the patch into a subdirectory."""
491 for patch in self.patches:
492 patch.set_relpath(relpath)
493
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000494 def __iter__(self):
495 for patch in self.patches:
496 yield patch
497
maruel@chromium.org5e975632011-09-29 18:07:06 +0000498 def __getitem__(self, key):
499 return self.patches[key]
500
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000501 @property
502 def filenames(self):
503 return [p.filename for p in self.patches]