blob: 1bc608c9f9ef03fbd25752b51bc91eb75bff211a [file] [log] [blame]
[email protected]b3727a32011-04-04 19:31:441# coding=utf8
[email protected]cf602552012-01-10 19:49:312# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]b3727a32011-04-04 19:31:443# 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
[email protected]cd619402011-04-09 00:08:007import posixpath
8import os
[email protected]b3727a32011-04-04 19:31:449import 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):
[email protected]cd619402011-04-09 00:08:0026 """Defines a single file being modified.
27
28 '/' is always used instead of os.sep for consistency.
29 """
[email protected]b3727a32011-04-04 19:31:4430 is_delete = False
31 is_binary = False
[email protected]97366be2011-06-03 20:02:4632 is_new = False
[email protected]b3727a32011-04-04 19:31:4433
[email protected]cd619402011-04-09 00:08:0034 def __init__(self, filename):
[email protected]5e975632011-09-29 18:07:0635 assert self.__class__ is not FilePatchBase
[email protected]be113f12011-09-01 15:05:3436 self.filename = self._process_filename(filename)
[email protected]a19047c2011-09-08 12:49:5837 # Set when the file is copied or moved.
38 self.source_filename = None
[email protected]cd619402011-04-09 00:08:0039
[email protected]8fab6b62012-02-16 21:50:3540 @property
41 def filename_utf8(self):
42 return self.filename.encode('utf-8')
43
44 @property
45 def source_filename_utf8(self):
46 if self.source_filename is not None:
47 return self.source_filename.encode('utf-8')
48
[email protected]be113f12011-09-01 15:05:3449 @staticmethod
50 def _process_filename(filename):
51 filename = filename.replace('\\', '/')
[email protected]cd619402011-04-09 00:08:0052 # Blacklist a few characters for simplicity.
[email protected]ca858012015-03-27 15:21:5353 for i in ('$', '..', '\'', '"', '<', '>', ':', '|', '?', '*'):
[email protected]be113f12011-09-01 15:05:3454 if i in filename:
55 raise UnsupportedPatchFormat(
56 filename, 'Can\'t use \'%s\' in filename.' % i)
[email protected]8770f482015-05-27 18:26:4657 if filename.startswith('/'):
58 raise UnsupportedPatchFormat(
59 filename, 'Filename can\'t start with \'/\'.')
60 if filename == 'CON':
61 raise UnsupportedPatchFormat(
62 filename, 'Filename can\'t be \'CON\'.')
63 if re.match('COM\d', filename):
64 raise UnsupportedPatchFormat(
65 filename, 'Filename can\'t be \'%s\'.' % filename)
[email protected]be113f12011-09-01 15:05:3466 return filename
[email protected]cd619402011-04-09 00:08:0067
[email protected]cd619402011-04-09 00:08:0068 def set_relpath(self, relpath):
69 if not relpath:
70 return
71 relpath = relpath.replace('\\', '/')
72 if relpath[0] == '/':
73 self._fail('Relative path starts with %s' % relpath[0])
[email protected]be113f12011-09-01 15:05:3474 self.filename = self._process_filename(
75 posixpath.join(relpath, self.filename))
[email protected]a19047c2011-09-08 12:49:5876 if self.source_filename:
77 self.source_filename = self._process_filename(
78 posixpath.join(relpath, self.source_filename))
[email protected]cd619402011-04-09 00:08:0079
80 def _fail(self, msg):
[email protected]be113f12011-09-01 15:05:3481 """Shortcut function to raise UnsupportedPatchFormat."""
[email protected]cd619402011-04-09 00:08:0082 raise UnsupportedPatchFormat(self.filename, msg)
83
[email protected]5e975632011-09-29 18:07:0684 def __str__(self):
85 # Use a status-like board.
86 out = ''
87 if self.is_binary:
88 out += 'B'
89 else:
90 out += ' '
91 if self.is_delete:
92 out += 'D'
93 else:
94 out += ' '
95 if self.is_new:
96 out += 'N'
97 else:
98 out += ' '
99 if self.source_filename:
100 out += 'R'
101 else:
102 out += ' '
[email protected]cf602552012-01-10 19:49:31103 out += ' '
104 if self.source_filename:
[email protected]8fab6b62012-02-16 21:50:35105 out += '%s->' % self.source_filename_utf8
106 return out + self.filename_utf8
[email protected]5e975632011-09-29 18:07:06107
[email protected]4dd9f722012-10-01 16:23:03108 def dump(self):
109 """Dumps itself in a verbose way to help diagnosing."""
110 return str(self)
111
[email protected]b3727a32011-04-04 19:31:44112
113class FilePatchDelete(FilePatchBase):
114 """Deletes a file."""
115 is_delete = True
116
117 def __init__(self, filename, is_binary):
[email protected]cd619402011-04-09 00:08:00118 super(FilePatchDelete, self).__init__(filename)
[email protected]b3727a32011-04-04 19:31:44119 self.is_binary = is_binary
120
[email protected]b3727a32011-04-04 19:31:44121
122class FilePatchBinary(FilePatchBase):
123 """Content of a new binary file."""
124 is_binary = True
125
[email protected]97366be2011-06-03 20:02:46126 def __init__(self, filename, data, svn_properties, is_new):
[email protected]cd619402011-04-09 00:08:00127 super(FilePatchBinary, self).__init__(filename)
[email protected]b3727a32011-04-04 19:31:44128 self.data = data
129 self.svn_properties = svn_properties or []
[email protected]97366be2011-06-03 20:02:46130 self.is_new = is_new
[email protected]b3727a32011-04-04 19:31:44131
132 def get(self):
133 return self.data
134
[email protected]4dd9f722012-10-01 16:23:03135 def __str__(self):
136 return str(super(FilePatchBinary, self)) + ' %d bytes' % len(self.data)
137
[email protected]b3727a32011-04-04 19:31:44138
[email protected]cf602552012-01-10 19:49:31139class Hunk(object):
140 """Parsed hunk data container."""
141
142 def __init__(self, start_src, lines_src, start_dst, lines_dst):
143 self.start_src = start_src
144 self.lines_src = lines_src
145 self.start_dst = start_dst
146 self.lines_dst = lines_dst
147 self.variation = self.lines_dst - self.lines_src
148 self.text = []
149
[email protected]17fa4be2012-08-29 17:18:12150 def __repr__(self):
151 return '%s<(%d, %d) to (%d, %d)>' % (
152 self.__class__.__name__,
153 self.start_src, self.lines_src, self.start_dst, self.lines_dst)
154
[email protected]cf602552012-01-10 19:49:31155
[email protected]b3727a32011-04-04 19:31:44156class FilePatchDiff(FilePatchBase):
157 """Patch for a single file."""
158
159 def __init__(self, filename, diff, svn_properties):
[email protected]cd619402011-04-09 00:08:00160 super(FilePatchDiff, self).__init__(filename)
[email protected]61e0b692011-04-12 21:01:01161 if not diff:
162 self._fail('File doesn\'t have a diff.')
[email protected]cd619402011-04-09 00:08:00163 self.diff_header, self.diff_hunks = self._split_header(diff)
[email protected]b3727a32011-04-04 19:31:44164 self.svn_properties = svn_properties or []
[email protected]cd619402011-04-09 00:08:00165 self.is_git_diff = self._is_git_diff_header(self.diff_header)
Edward Lesmesf8792072017-09-13 08:05:12166 self.patchlevel = 0
[email protected]b3727a32011-04-04 19:31:44167 if self.is_git_diff:
[email protected]cd619402011-04-09 00:08:00168 self._verify_git_header()
[email protected]b3727a32011-04-04 19:31:44169 else:
[email protected]cd619402011-04-09 00:08:00170 self._verify_svn_header()
[email protected]cf602552012-01-10 19:49:31171 self.hunks = self._split_hunks()
[email protected]5e975632011-09-29 18:07:06172 if self.source_filename and not self.is_new:
173 self._fail('If source_filename is set, is_new must be also be set')
[email protected]b3727a32011-04-04 19:31:44174
[email protected]5e975632011-09-29 18:07:06175 def get(self, for_git):
176 if for_git or not self.source_filename:
177 return self.diff_header + self.diff_hunks
178 else:
179 # patch is stupid. It patches the source_filename instead so get rid of
180 # any source_filename reference if needed.
181 return (
[email protected]8fab6b62012-02-16 21:50:35182 self.diff_header.replace(
183 self.source_filename_utf8, self.filename_utf8) +
[email protected]5e975632011-09-29 18:07:06184 self.diff_hunks)
[email protected]cd619402011-04-09 00:08:00185
186 def set_relpath(self, relpath):
[email protected]8fab6b62012-02-16 21:50:35187 old_filename = self.filename_utf8
188 old_source_filename = self.source_filename_utf8 or self.filename_utf8
[email protected]cd619402011-04-09 00:08:00189 super(FilePatchDiff, self).set_relpath(relpath)
190 # Update the header too.
[email protected]8fab6b62012-02-16 21:50:35191 filename = self.filename_utf8
192 source_filename = self.source_filename_utf8 or self.filename_utf8
[email protected]a19047c2011-09-08 12:49:58193 lines = self.diff_header.splitlines(True)
194 for i, line in enumerate(lines):
195 if line.startswith('diff --git'):
196 lines[i] = line.replace(
197 'a/' + old_source_filename, source_filename).replace(
[email protected]8fab6b62012-02-16 21:50:35198 'b/' + old_filename, filename)
[email protected]a19047c2011-09-08 12:49:58199 elif re.match(r'^\w+ from .+$', line) or line.startswith('---'):
200 lines[i] = line.replace(old_source_filename, source_filename)
201 elif re.match(r'^\w+ to .+$', line) or line.startswith('+++'):
[email protected]8fab6b62012-02-16 21:50:35202 lines[i] = line.replace(old_filename, filename)
[email protected]a19047c2011-09-08 12:49:58203 self.diff_header = ''.join(lines)
[email protected]cd619402011-04-09 00:08:00204
205 def _split_header(self, diff):
206 """Splits a diff in two: the header and the hunks."""
207 header = []
208 hunks = diff.splitlines(True)
209 while hunks:
210 header.append(hunks.pop(0))
211 if header[-1].startswith('--- '):
212 break
213 else:
214 # Some diff may not have a ---/+++ set like a git rename with no change or
215 # a svn diff with only property change.
216 pass
217
218 if hunks:
219 if not hunks[0].startswith('+++ '):
220 self._fail('Inconsistent header')
221 header.append(hunks.pop(0))
222 if hunks:
223 if not hunks[0].startswith('@@ '):
224 self._fail('Inconsistent hunk header')
225
226 # Mangle any \\ in the header to /.
227 header_lines = ('Index:', 'diff', 'copy', 'rename', '+++', '---')
[email protected]8fab6b62012-02-16 21:50:35228 basename = os.path.basename(self.filename_utf8)
[email protected]cd619402011-04-09 00:08:00229 for i in xrange(len(header)):
230 if (header[i].split(' ', 1)[0] in header_lines or
231 header[i].endswith(basename)):
232 header[i] = header[i].replace('\\', '/')
233 return ''.join(header), ''.join(hunks)
[email protected]b3727a32011-04-04 19:31:44234
235 @staticmethod
[email protected]cd619402011-04-09 00:08:00236 def _is_git_diff_header(diff_header):
237 """Returns True if the diff for a single files was generated with git."""
[email protected]b3727a32011-04-04 19:31:44238 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff
239 # Rename partial change:
240 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff
241 # Rename no change:
242 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff
[email protected]cd619402011-04-09 00:08:00243 return any(l.startswith('diff --git') for l in diff_header.splitlines())
[email protected]b3727a32011-04-04 19:31:44244
[email protected]cf602552012-01-10 19:49:31245 def _split_hunks(self):
246 """Splits the hunks and does verification."""
247 hunks = []
248 for line in self.diff_hunks.splitlines(True):
249 if line.startswith('@@'):
[email protected]db1fd782012-01-11 01:51:29250 match = re.match(r'^@@ -([\d,]+) \+([\d,]+) @@.*$', line)
[email protected]cf602552012-01-10 19:49:31251 # File add will result in "-0,0 +1" but file deletion will result in
252 # "-1,N +0,0" where N is the number of lines deleted. That's from diff
253 # and svn diff. git diff doesn't exhibit this behavior.
[email protected]db1fd782012-01-11 01:51:29254 # svn diff for a single line file rewrite "@@ -1 +1 @@". Fun.
[email protected]17fa4be2012-08-29 17:18:12255 # "@@ -1 +1,N @@" is also valid where N is the length of the new file.
[email protected]cf602552012-01-10 19:49:31256 if not match:
257 self._fail('Hunk header is unparsable')
[email protected]17fa4be2012-08-29 17:18:12258 count = match.group(1).count(',')
259 if not count:
260 start_src = int(match.group(1))
261 lines_src = 1
262 elif count == 1:
[email protected]db1fd782012-01-11 01:51:29263 start_src, lines_src = map(int, match.group(1).split(',', 1))
[email protected]cf602552012-01-10 19:49:31264 else:
[email protected]17fa4be2012-08-29 17:18:12265 self._fail('Hunk header is malformed')
266
267 count = match.group(2).count(',')
268 if not count:
269 start_dst = int(match.group(2))
270 lines_dst = 1
271 elif count == 1:
[email protected]db1fd782012-01-11 01:51:29272 start_dst, lines_dst = map(int, match.group(2).split(',', 1))
273 else:
[email protected]17fa4be2012-08-29 17:18:12274 self._fail('Hunk header is malformed')
[email protected]db1fd782012-01-11 01:51:29275 new_hunk = Hunk(start_src, lines_src, start_dst, lines_dst)
[email protected]cf602552012-01-10 19:49:31276 if hunks:
277 if new_hunk.start_src <= hunks[-1].start_src:
278 self._fail('Hunks source lines are not ordered')
279 if new_hunk.start_dst <= hunks[-1].start_dst:
280 self._fail('Hunks destination lines are not ordered')
281 hunks.append(new_hunk)
282 continue
283 hunks[-1].text.append(line)
284
285 if len(hunks) == 1:
286 if hunks[0].start_src == 0 and hunks[0].lines_src == 0:
287 self.is_new = True
288 if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0:
289 self.is_delete = True
290
291 if self.is_new and self.is_delete:
292 self._fail('Hunk header is all 0')
293
294 if not self.is_new and not self.is_delete:
295 for hunk in hunks:
296 variation = (
297 len([1 for i in hunk.text if i.startswith('+')]) -
298 len([1 for i in hunk.text if i.startswith('-')]))
299 if variation != hunk.variation:
300 self._fail(
[email protected]17fa4be2012-08-29 17:18:12301 'Hunk header is incorrect: %d vs %d; %r' % (
302 variation, hunk.variation, hunk))
[email protected]cf602552012-01-10 19:49:31303 if not hunk.start_src:
304 self._fail(
305 'Hunk header start line is incorrect: %d' % hunk.start_src)
306 if not hunk.start_dst:
307 self._fail(
308 'Hunk header start line is incorrect: %d' % hunk.start_dst)
309 hunk.start_src -= 1
310 hunk.start_dst -= 1
311 if self.is_new and hunks:
312 hunks[0].start_dst -= 1
313 if self.is_delete and hunks:
314 hunks[0].start_src -= 1
315 return hunks
316
Edward Lesmesf8792072017-09-13 08:05:12317 def mangle(self, string):
318 """Mangle a file path."""
319 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:])
320
[email protected]cd619402011-04-09 00:08:00321 def _verify_git_header(self):
322 """Sanity checks the header.
323
324 Expects the following format:
325
[email protected]ff526192013-06-10 19:30:26326 <garbage>
[email protected]cd619402011-04-09 00:08:00327 diff --git (|a/)<filename> (|b/)<filename>
328 <similarity>
329 <filemode changes>
330 <index>
331 <copy|rename from>
332 <copy|rename to>
333 --- <filename>
334 +++ <filename>
335
336 Everything is optional except the diff --git line.
337 """
338 lines = self.diff_header.splitlines()
339
340 # Verify the diff --git line.
341 old = None
342 new = None
[email protected]b3727a32011-04-04 19:31:44343 while lines:
[email protected]cd619402011-04-09 00:08:00344 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0))
345 if not match:
346 continue
[email protected]a19047c2011-09-08 12:49:58347 if match.group(1).startswith('a/') and match.group(2).startswith('b/'):
[email protected]cd619402011-04-09 00:08:00348 self.patchlevel = 1
Edward Lesmesf8792072017-09-13 08:05:12349 old = self.mangle(match.group(1))
350 new = self.mangle(match.group(2))
[email protected]a19047c2011-09-08 12:49:58351
[email protected]cd619402011-04-09 00:08:00352 # The rename is about the new file so the old file can be anything.
[email protected]8fab6b62012-02-16 21:50:35353 if new not in (self.filename_utf8, 'dev/null'):
[email protected]cd619402011-04-09 00:08:00354 self._fail('Unexpected git diff output name %s.' % new)
355 if old == 'dev/null' and new == 'dev/null':
356 self._fail('Unexpected /dev/null git diff.')
357 break
358
359 if not old or not new:
360 self._fail('Unexpected git diff; couldn\'t find git header.')
361
[email protected]8fab6b62012-02-16 21:50:35362 if old not in (self.filename_utf8, 'dev/null'):
[email protected]a19047c2011-09-08 12:49:58363 # Copy or rename.
[email protected]8fab6b62012-02-16 21:50:35364 self.source_filename = old.decode('utf-8')
[email protected]8baaea72011-09-08 12:55:29365 self.is_new = True
[email protected]a19047c2011-09-08 12:49:58366
[email protected]97366be2011-06-03 20:02:46367 last_line = ''
368
[email protected]cd619402011-04-09 00:08:00369 while lines:
[email protected]b6ffdaf2011-06-03 19:23:16370 line = lines.pop(0)
[email protected]a19047c2011-09-08 12:49:58371 self._verify_git_header_process_line(lines, line, last_line)
[email protected]97366be2011-06-03 20:02:46372 last_line = line
[email protected]b6ffdaf2011-06-03 19:23:16373
[email protected]97366be2011-06-03 20:02:46374 # Cheap check to make sure the file name is at least mentioned in the
375 # 'diff' header. That the only remaining invariant.
[email protected]8fab6b62012-02-16 21:50:35376 if not self.filename_utf8 in self.diff_header:
[email protected]97366be2011-06-03 20:02:46377 self._fail('Diff seems corrupted.')
[email protected]cd619402011-04-09 00:08:00378
[email protected]a19047c2011-09-08 12:49:58379 def _verify_git_header_process_line(self, lines, line, last_line):
[email protected]97366be2011-06-03 20:02:46380 """Processes a single line of the header.
381
382 Returns True if it should continue looping.
[email protected]378a4192011-06-06 13:36:02383
384 Format is described to
385 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html
[email protected]97366be2011-06-03 20:02:46386 """
[email protected]97366be2011-06-03 20:02:46387 match = re.match(r'^(rename|copy) from (.+)$', line)
[email protected]8fab6b62012-02-16 21:50:35388 old = self.source_filename_utf8 or self.filename_utf8
[email protected]97366be2011-06-03 20:02:46389 if match:
390 if old != match.group(2):
391 self._fail('Unexpected git diff input name for line %s.' % line)
392 if not lines or not lines[0].startswith('%s to ' % match.group(1)):
393 self._fail(
394 'Confused %s from/to git diff for line %s.' %
395 (match.group(1), line))
396 return
397
[email protected]97366be2011-06-03 20:02:46398 match = re.match(r'^(rename|copy) to (.+)$', line)
399 if match:
[email protected]8fab6b62012-02-16 21:50:35400 if self.filename_utf8 != match.group(2):
[email protected]97366be2011-06-03 20:02:46401 self._fail('Unexpected git diff output name for line %s.' % line)
402 if not last_line.startswith('%s from ' % match.group(1)):
403 self._fail(
404 'Confused %s from/to git diff for line %s.' %
405 (match.group(1), line))
406 return
407
[email protected]40052252011-11-11 20:54:55408 match = re.match(r'^deleted file mode (\d{6})$', line)
409 if match:
410 # It is necessary to parse it because there may be no hunk, like when the
411 # file was empty.
412 self.is_delete = True
413 return
414
[email protected]378a4192011-06-06 13:36:02415 match = re.match(r'^new(| file) mode (\d{6})$', line)
[email protected]97366be2011-06-03 20:02:46416 if match:
[email protected]378a4192011-06-06 13:36:02417 mode = match.group(2)
[email protected]97366be2011-06-03 20:02:46418 # Only look at owner ACL for executable.
[email protected]86eb9e72011-06-03 20:14:52419 if bool(int(mode[4]) & 1):
[email protected]e1a03762012-09-24 15:28:52420 self.svn_properties.append(('svn:executable', '.'))
[email protected]dffc73c2012-09-21 19:09:16421 elif not self.source_filename and self.is_new:
422 # It's a new file, not from a rename/copy, then there's no property to
423 # delete.
[email protected]d7ca6162012-08-29 17:22:22424 self.svn_properties.append(('svn:executable', None))
[email protected]40052252011-11-11 20:54:55425 return
[email protected]97366be2011-06-03 20:02:46426
[email protected]97366be2011-06-03 20:02:46427 match = re.match(r'^--- (.*)$', line)
428 if match:
429 if last_line[:3] in ('---', '+++'):
430 self._fail('--- and +++ are reversed')
[email protected]8baaea72011-09-08 12:55:29431 if match.group(1) == '/dev/null':
432 self.is_new = True
Edward Lesmesf8792072017-09-13 08:05:12433 elif self.mangle(match.group(1)) != old:
[email protected]8baaea72011-09-08 12:55:29434 # git patches are always well formatted, do not allow random filenames.
[email protected]cd619402011-04-09 00:08:00435 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
[email protected]97366be2011-06-03 20:02:46436 if not lines or not lines[0].startswith('+++'):
[email protected]cd619402011-04-09 00:08:00437 self._fail('Missing git diff output name.')
[email protected]97366be2011-06-03 20:02:46438 return
439
[email protected]97366be2011-06-03 20:02:46440 match = re.match(r'^\+\+\+ (.*)$', line)
441 if match:
442 if not last_line.startswith('---'):
[email protected]cd619402011-04-09 00:08:00443 self._fail('Unexpected git diff: --- not following +++.')
[email protected]be605652011-09-02 20:28:07444 if '/dev/null' == match.group(1):
445 self.is_delete = True
Edward Lesmesf8792072017-09-13 08:05:12446 elif self.filename_utf8 != self.mangle(match.group(1)):
[email protected]a19047c2011-09-08 12:49:58447 self._fail(
448 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1)))
[email protected]97366be2011-06-03 20:02:46449 if lines:
450 self._fail('Crap after +++')
451 # We're done.
452 return
[email protected]cd619402011-04-09 00:08:00453
454 def _verify_svn_header(self):
455 """Sanity checks the header.
456
457 A svn diff can contain only property changes, in that case there will be no
458 proper header. To make things worse, this property change header is
459 localized.
460 """
461 lines = self.diff_header.splitlines()
[email protected]97366be2011-06-03 20:02:46462 last_line = ''
463
[email protected]cd619402011-04-09 00:08:00464 while lines:
[email protected]97366be2011-06-03 20:02:46465 line = lines.pop(0)
466 self._verify_svn_header_process_line(lines, line, last_line)
467 last_line = line
468
469 # Cheap check to make sure the file name is at least mentioned in the
470 # 'diff' header. That the only remaining invariant.
[email protected]8fab6b62012-02-16 21:50:35471 if not self.filename_utf8 in self.diff_header:
[email protected]97366be2011-06-03 20:02:46472 self._fail('Diff seems corrupted.')
473
474 def _verify_svn_header_process_line(self, lines, line, last_line):
475 """Processes a single line of the header.
476
477 Returns True if it should continue looping.
478 """
479 match = re.match(r'^--- ([^\t]+).*$', line)
480 if match:
481 if last_line[:3] in ('---', '+++'):
482 self._fail('--- and +++ are reversed')
[email protected]8baaea72011-09-08 12:55:29483 if match.group(1) == '/dev/null':
484 self.is_new = True
Edward Lesmesf8792072017-09-13 08:05:12485 elif self.mangle(match.group(1)) != self.filename_utf8:
[email protected]8baaea72011-09-08 12:55:29486 # guess the source filename.
[email protected]8fab6b62012-02-16 21:50:35487 self.source_filename = match.group(1).decode('utf-8')
[email protected]8baaea72011-09-08 12:55:29488 self.is_new = True
[email protected]97366be2011-06-03 20:02:46489 if not lines or not lines[0].startswith('+++'):
[email protected]c4b5e762011-04-20 23:56:08490 self._fail('Nothing after header.')
[email protected]97366be2011-06-03 20:02:46491 return
492
493 match = re.match(r'^\+\+\+ ([^\t]+).*$', line)
494 if match:
495 if not last_line.startswith('---'):
[email protected]cd619402011-04-09 00:08:00496 self._fail('Unexpected diff: --- not following +++.')
[email protected]be605652011-09-02 20:28:07497 if match.group(1) == '/dev/null':
498 self.is_delete = True
Edward Lesmesf8792072017-09-13 08:05:12499 elif self.mangle(match.group(1)) != self.filename_utf8:
[email protected]cd619402011-04-09 00:08:00500 self._fail('Unexpected diff: %s.' % match.group(1))
[email protected]97366be2011-06-03 20:02:46501 if lines:
502 self._fail('Crap after +++')
503 # We're done.
504 return
[email protected]b3727a32011-04-04 19:31:44505
[email protected]4dd9f722012-10-01 16:23:03506 def dump(self):
507 """Dumps itself in a verbose way to help diagnosing."""
508 return str(self) + '\n' + self.get(True)
509
[email protected]b3727a32011-04-04 19:31:44510
511class PatchSet(object):
512 """A list of FilePatch* objects."""
513
514 def __init__(self, patches):
[email protected]5e975632011-09-29 18:07:06515 for p in patches:
[email protected]8a1396c2011-04-22 00:14:24516 assert isinstance(p, FilePatchBase)
[email protected]b3727a32011-04-04 19:31:44517
[email protected]5e975632011-09-29 18:07:06518 def key(p):
519 """Sort by ordering of application.
520
521 File move are first.
522 Deletes are last.
523 """
[email protected]de800ff2012-09-12 19:25:24524 # The bool is necessary because None < 'string' but the reverse is needed.
525 return (
526 p.is_delete,
527 # False is before True, so files *with* a source file will be first.
528 not bool(p.source_filename),
529 p.source_filename_utf8,
530 p.filename_utf8)
[email protected]5e975632011-09-29 18:07:06531
532 self.patches = sorted(patches, key=key)
533
[email protected]cd619402011-04-09 00:08:00534 def set_relpath(self, relpath):
535 """Used to offset the patch into a subdirectory."""
536 for patch in self.patches:
537 patch.set_relpath(relpath)
538
[email protected]b3727a32011-04-04 19:31:44539 def __iter__(self):
540 for patch in self.patches:
541 yield patch
542
[email protected]5e975632011-09-29 18:07:06543 def __getitem__(self, key):
544 return self.patches[key]
545
[email protected]b3727a32011-04-04 19:31:44546 @property
547 def filenames(self):
548 return [p.filename for p in self.patches]