blob: 8fe8b5c20e081aa5bd675d0b8fc8582204811a67 [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('@@'):
222 match = re.match(r'^@@ -(\d+),(\d+) \+([\d,]+) @@.*$', line)
223 # 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.
226 if not match:
227 self._fail('Hunk header is unparsable')
228 if ',' in match.group(3):
229 start_dst, lines_dst = map(int, match.group(3).split(',', 1))
230 else:
231 start_dst = int(match.group(3))
232 lines_dst = 0
233 new_hunk = Hunk(int(match.group(1)), int(match.group(2)),
234 start_dst, lines_dst)
235 if hunks:
236 if new_hunk.start_src <= hunks[-1].start_src:
237 self._fail('Hunks source lines are not ordered')
238 if new_hunk.start_dst <= hunks[-1].start_dst:
239 self._fail('Hunks destination lines are not ordered')
240 hunks.append(new_hunk)
241 continue
242 hunks[-1].text.append(line)
243
244 if len(hunks) == 1:
245 if hunks[0].start_src == 0 and hunks[0].lines_src == 0:
246 self.is_new = True
247 if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0:
248 self.is_delete = True
249
250 if self.is_new and self.is_delete:
251 self._fail('Hunk header is all 0')
252
253 if not self.is_new and not self.is_delete:
254 for hunk in hunks:
255 variation = (
256 len([1 for i in hunk.text if i.startswith('+')]) -
257 len([1 for i in hunk.text if i.startswith('-')]))
258 if variation != hunk.variation:
259 self._fail(
260 'Hunk header is incorrect: %d vs %d' % (
261 variation, hunk.variation))
262 if not hunk.start_src:
263 self._fail(
264 'Hunk header start line is incorrect: %d' % hunk.start_src)
265 if not hunk.start_dst:
266 self._fail(
267 'Hunk header start line is incorrect: %d' % hunk.start_dst)
268 hunk.start_src -= 1
269 hunk.start_dst -= 1
270 if self.is_new and hunks:
271 hunks[0].start_dst -= 1
272 if self.is_delete and hunks:
273 hunks[0].start_src -= 1
274 return hunks
275
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000276 def mangle(self, string):
277 """Mangle a file path."""
278 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:])
279
280 def _verify_git_header(self):
281 """Sanity checks the header.
282
283 Expects the following format:
284
285 <garbagge>
286 diff --git (|a/)<filename> (|b/)<filename>
287 <similarity>
288 <filemode changes>
289 <index>
290 <copy|rename from>
291 <copy|rename to>
292 --- <filename>
293 +++ <filename>
294
295 Everything is optional except the diff --git line.
296 """
297 lines = self.diff_header.splitlines()
298
299 # Verify the diff --git line.
300 old = None
301 new = None
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000302 while lines:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000303 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0))
304 if not match:
305 continue
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000306 if match.group(1).startswith('a/') and match.group(2).startswith('b/'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000307 self.patchlevel = 1
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000308 old = self.mangle(match.group(1))
309 new = self.mangle(match.group(2))
310
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000311 # The rename is about the new file so the old file can be anything.
312 if new not in (self.filename, 'dev/null'):
313 self._fail('Unexpected git diff output name %s.' % new)
314 if old == 'dev/null' and new == 'dev/null':
315 self._fail('Unexpected /dev/null git diff.')
316 break
317
318 if not old or not new:
319 self._fail('Unexpected git diff; couldn\'t find git header.')
320
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000321 if old not in (self.filename, 'dev/null'):
322 # Copy or rename.
323 self.source_filename = old
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000324 self.is_new = True
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000325
maruel@chromium.org97366be2011-06-03 20:02:46 +0000326 last_line = ''
327
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000328 while lines:
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000329 line = lines.pop(0)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000330 self._verify_git_header_process_line(lines, line, last_line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000331 last_line = line
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000332
maruel@chromium.org97366be2011-06-03 20:02:46 +0000333 # Cheap check to make sure the file name is at least mentioned in the
334 # 'diff' header. That the only remaining invariant.
335 if not self.filename in self.diff_header:
336 self._fail('Diff seems corrupted.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000337
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000338 def _verify_git_header_process_line(self, lines, line, last_line):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000339 """Processes a single line of the header.
340
341 Returns True if it should continue looping.
maruel@chromium.org378a4192011-06-06 13:36:02 +0000342
343 Format is described to
344 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html
maruel@chromium.org97366be2011-06-03 20:02:46 +0000345 """
maruel@chromium.org97366be2011-06-03 20:02:46 +0000346 match = re.match(r'^(rename|copy) from (.+)$', line)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000347 old = self.source_filename or self.filename
maruel@chromium.org97366be2011-06-03 20:02:46 +0000348 if match:
349 if old != match.group(2):
350 self._fail('Unexpected git diff input name for line %s.' % line)
351 if not lines or not lines[0].startswith('%s to ' % match.group(1)):
352 self._fail(
353 'Confused %s from/to git diff for line %s.' %
354 (match.group(1), line))
355 return
356
maruel@chromium.org97366be2011-06-03 20:02:46 +0000357 match = re.match(r'^(rename|copy) to (.+)$', line)
358 if match:
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000359 if self.filename != match.group(2):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000360 self._fail('Unexpected git diff output name for line %s.' % line)
361 if not last_line.startswith('%s from ' % match.group(1)):
362 self._fail(
363 'Confused %s from/to git diff for line %s.' %
364 (match.group(1), line))
365 return
366
maruel@chromium.org40052252011-11-11 20:54:55 +0000367 match = re.match(r'^deleted file mode (\d{6})$', line)
368 if match:
369 # It is necessary to parse it because there may be no hunk, like when the
370 # file was empty.
371 self.is_delete = True
372 return
373
maruel@chromium.org378a4192011-06-06 13:36:02 +0000374 match = re.match(r'^new(| file) mode (\d{6})$', line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000375 if match:
maruel@chromium.org378a4192011-06-06 13:36:02 +0000376 mode = match.group(2)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000377 # Only look at owner ACL for executable.
maruel@chromium.org378a4192011-06-06 13:36:02 +0000378 # TODO(maruel): Add support to remove a property.
maruel@chromium.org86eb9e72011-06-03 20:14:52 +0000379 if bool(int(mode[4]) & 1):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000380 self.svn_properties.append(('svn:executable', '*'))
maruel@chromium.org40052252011-11-11 20:54:55 +0000381 return
maruel@chromium.org97366be2011-06-03 20:02:46 +0000382
maruel@chromium.org97366be2011-06-03 20:02:46 +0000383 match = re.match(r'^--- (.*)$', line)
384 if match:
385 if last_line[:3] in ('---', '+++'):
386 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000387 if match.group(1) == '/dev/null':
388 self.is_new = True
389 elif self.mangle(match.group(1)) != old:
390 # git patches are always well formatted, do not allow random filenames.
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000391 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000392 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000393 self._fail('Missing git diff output name.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000394 return
395
maruel@chromium.org97366be2011-06-03 20:02:46 +0000396 match = re.match(r'^\+\+\+ (.*)$', line)
397 if match:
398 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000399 self._fail('Unexpected git diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000400 if '/dev/null' == match.group(1):
401 self.is_delete = True
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000402 elif self.filename != self.mangle(match.group(1)):
403 self._fail(
404 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000405 if lines:
406 self._fail('Crap after +++')
407 # We're done.
408 return
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000409
410 def _verify_svn_header(self):
411 """Sanity checks the header.
412
413 A svn diff can contain only property changes, in that case there will be no
414 proper header. To make things worse, this property change header is
415 localized.
416 """
417 lines = self.diff_header.splitlines()
maruel@chromium.org97366be2011-06-03 20:02:46 +0000418 last_line = ''
419
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000420 while lines:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000421 line = lines.pop(0)
422 self._verify_svn_header_process_line(lines, line, last_line)
423 last_line = line
424
425 # Cheap check to make sure the file name is at least mentioned in the
426 # 'diff' header. That the only remaining invariant.
427 if not self.filename in self.diff_header:
428 self._fail('Diff seems corrupted.')
429
430 def _verify_svn_header_process_line(self, lines, line, last_line):
431 """Processes a single line of the header.
432
433 Returns True if it should continue looping.
434 """
435 match = re.match(r'^--- ([^\t]+).*$', line)
436 if match:
437 if last_line[:3] in ('---', '+++'):
438 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000439 if match.group(1) == '/dev/null':
440 self.is_new = True
441 elif self.mangle(match.group(1)) != self.filename:
442 # guess the source filename.
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000443 self.source_filename = match.group(1)
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000444 self.is_new = True
maruel@chromium.org97366be2011-06-03 20:02:46 +0000445 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgc4b5e762011-04-20 23:56:08 +0000446 self._fail('Nothing after header.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000447 return
448
449 match = re.match(r'^\+\+\+ ([^\t]+).*$', line)
450 if match:
451 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000452 self._fail('Unexpected diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000453 if match.group(1) == '/dev/null':
454 self.is_delete = True
455 elif self.mangle(match.group(1)) != self.filename:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000456 self._fail('Unexpected diff: %s.' % match.group(1))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000457 if lines:
458 self._fail('Crap after +++')
459 # We're done.
460 return
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000461
462
463class PatchSet(object):
464 """A list of FilePatch* objects."""
465
466 def __init__(self, patches):
maruel@chromium.org5e975632011-09-29 18:07:06 +0000467 for p in patches:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000468 assert isinstance(p, FilePatchBase)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000469
maruel@chromium.org5e975632011-09-29 18:07:06 +0000470 def key(p):
471 """Sort by ordering of application.
472
473 File move are first.
474 Deletes are last.
475 """
476 if p.source_filename:
477 return (p.is_delete, p.source_filename, p.filename)
478 else:
479 # tuple are always greater than string, abuse that fact.
480 return (p.is_delete, (p.filename,), p.filename)
481
482 self.patches = sorted(patches, key=key)
483
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000484 def set_relpath(self, relpath):
485 """Used to offset the patch into a subdirectory."""
486 for patch in self.patches:
487 patch.set_relpath(relpath)
488
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000489 def __iter__(self):
490 for patch in self.patches:
491 yield patch
492
maruel@chromium.org5e975632011-09-29 18:07:06 +0000493 def __getitem__(self, key):
494 return self.patches[key]
495
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000496 @property
497 def filenames(self):
498 return [p.filename for p in self.patches]