blob: 00a16460193db7180e380953a0f8630b9ebb5202 [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)
Edward Lemur5908f992017-09-12 23:49:41 +020037 self.patchlevel = 0
maruel@chromium.orga19047c2011-09-08 12:49:58 +000038 # Set when the file is copied or moved.
39 self.source_filename = None
maruel@chromium.orgcd619402011-04-09 00:08:00 +000040
maruel@chromium.org8fab6b62012-02-16 21:50:35 +000041 @property
42 def filename_utf8(self):
43 return self.filename.encode('utf-8')
44
45 @property
46 def source_filename_utf8(self):
47 if self.source_filename is not None:
48 return self.source_filename.encode('utf-8')
49
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000050 @staticmethod
51 def _process_filename(filename):
52 filename = filename.replace('\\', '/')
maruel@chromium.orgcd619402011-04-09 00:08:00 +000053 # Blacklist a few characters for simplicity.
phajdan.jr@chromium.orgca858012015-03-27 15:21:53 +000054 for i in ('$', '..', '\'', '"', '<', '>', ':', '|', '?', '*'):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000055 if i in filename:
56 raise UnsupportedPatchFormat(
57 filename, 'Can\'t use \'%s\' in filename.' % i)
sullivan@chromium.org8770f482015-05-27 18:26:46 +000058 if filename.startswith('/'):
59 raise UnsupportedPatchFormat(
60 filename, 'Filename can\'t start with \'/\'.')
61 if filename == 'CON':
62 raise UnsupportedPatchFormat(
63 filename, 'Filename can\'t be \'CON\'.')
64 if re.match('COM\d', filename):
65 raise UnsupportedPatchFormat(
66 filename, 'Filename can\'t be \'%s\'.' % filename)
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000067 return filename
maruel@chromium.orgcd619402011-04-09 00:08:00 +000068
Edward Lemur5908f992017-09-12 23:49:41 +020069 def filename_after_patchlevel(self):
70 """Applies patchlevel to self.filename.
71
72 Applies patchlevel to self.filename so the resulting filename is the same as
73 the one git-apply would have used.
74 """
75 # We use self.patchlevel-1 since git-apply considers the "a/" in the diff
76 # as part of the file path.
77 return self._apply_patchlevel(self.filename, self.patchlevel-1)
78
79 def _apply_patchlevel(self, string, patchlevel=None):
80 """Apply patchlevel to a file path.
81
82 This function replaces backslashes with slashes and removes the first
83 patchlevel elements of string. patchlevel is self.patchlevel by default.
84 """
85 patchlevel = patchlevel or self.patchlevel
86 return '/'.join(string.replace('\\', '/').split('/')[patchlevel:])
87
maruel@chromium.orgcd619402011-04-09 00:08:00 +000088 def set_relpath(self, relpath):
89 if not relpath:
90 return
91 relpath = relpath.replace('\\', '/')
92 if relpath[0] == '/':
93 self._fail('Relative path starts with %s' % relpath[0])
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000094 self.filename = self._process_filename(
95 posixpath.join(relpath, self.filename))
maruel@chromium.orga19047c2011-09-08 12:49:58 +000096 if self.source_filename:
97 self.source_filename = self._process_filename(
98 posixpath.join(relpath, self.source_filename))
maruel@chromium.orgcd619402011-04-09 00:08:00 +000099
100 def _fail(self, msg):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +0000101 """Shortcut function to raise UnsupportedPatchFormat."""
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000102 raise UnsupportedPatchFormat(self.filename, msg)
103
maruel@chromium.org5e975632011-09-29 18:07:06 +0000104 def __str__(self):
105 # Use a status-like board.
106 out = ''
107 if self.is_binary:
108 out += 'B'
109 else:
110 out += ' '
111 if self.is_delete:
112 out += 'D'
113 else:
114 out += ' '
115 if self.is_new:
116 out += 'N'
117 else:
118 out += ' '
119 if self.source_filename:
120 out += 'R'
121 else:
122 out += ' '
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000123 out += ' '
124 if self.source_filename:
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000125 out += '%s->' % self.source_filename_utf8
126 return out + self.filename_utf8
maruel@chromium.org5e975632011-09-29 18:07:06 +0000127
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000128 def dump(self):
129 """Dumps itself in a verbose way to help diagnosing."""
130 return str(self)
131
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000132
133class FilePatchDelete(FilePatchBase):
134 """Deletes a file."""
135 is_delete = True
136
137 def __init__(self, filename, is_binary):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000138 super(FilePatchDelete, self).__init__(filename)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000139 self.is_binary = is_binary
140
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000141
142class FilePatchBinary(FilePatchBase):
143 """Content of a new binary file."""
144 is_binary = True
145
maruel@chromium.org97366be2011-06-03 20:02:46 +0000146 def __init__(self, filename, data, svn_properties, is_new):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000147 super(FilePatchBinary, self).__init__(filename)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000148 self.data = data
149 self.svn_properties = svn_properties or []
maruel@chromium.org97366be2011-06-03 20:02:46 +0000150 self.is_new = is_new
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000151
152 def get(self):
153 return self.data
154
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000155 def __str__(self):
156 return str(super(FilePatchBinary, self)) + ' %d bytes' % len(self.data)
157
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000158
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000159class Hunk(object):
160 """Parsed hunk data container."""
161
162 def __init__(self, start_src, lines_src, start_dst, lines_dst):
163 self.start_src = start_src
164 self.lines_src = lines_src
165 self.start_dst = start_dst
166 self.lines_dst = lines_dst
167 self.variation = self.lines_dst - self.lines_src
168 self.text = []
169
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000170 def __repr__(self):
171 return '%s<(%d, %d) to (%d, %d)>' % (
172 self.__class__.__name__,
173 self.start_src, self.lines_src, self.start_dst, self.lines_dst)
174
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000175
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000176class FilePatchDiff(FilePatchBase):
177 """Patch for a single file."""
178
179 def __init__(self, filename, diff, svn_properties):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000180 super(FilePatchDiff, self).__init__(filename)
maruel@chromium.org61e0b692011-04-12 21:01:01 +0000181 if not diff:
182 self._fail('File doesn\'t have a diff.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000183 self.diff_header, self.diff_hunks = self._split_header(diff)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000184 self.svn_properties = svn_properties or []
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000185 self.is_git_diff = self._is_git_diff_header(self.diff_header)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000186 if self.is_git_diff:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000187 self._verify_git_header()
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000188 else:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000189 self._verify_svn_header()
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000190 self.hunks = self._split_hunks()
maruel@chromium.org5e975632011-09-29 18:07:06 +0000191 if self.source_filename and not self.is_new:
192 self._fail('If source_filename is set, is_new must be also be set')
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000193
maruel@chromium.org5e975632011-09-29 18:07:06 +0000194 def get(self, for_git):
195 if for_git or not self.source_filename:
196 return self.diff_header + self.diff_hunks
197 else:
198 # patch is stupid. It patches the source_filename instead so get rid of
199 # any source_filename reference if needed.
200 return (
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000201 self.diff_header.replace(
202 self.source_filename_utf8, self.filename_utf8) +
maruel@chromium.org5e975632011-09-29 18:07:06 +0000203 self.diff_hunks)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000204
205 def set_relpath(self, relpath):
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000206 old_filename = self.filename_utf8
207 old_source_filename = self.source_filename_utf8 or self.filename_utf8
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000208 super(FilePatchDiff, self).set_relpath(relpath)
209 # Update the header too.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000210 filename = self.filename_utf8
211 source_filename = self.source_filename_utf8 or self.filename_utf8
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000212 lines = self.diff_header.splitlines(True)
213 for i, line in enumerate(lines):
214 if line.startswith('diff --git'):
215 lines[i] = line.replace(
216 'a/' + old_source_filename, source_filename).replace(
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000217 'b/' + old_filename, filename)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000218 elif re.match(r'^\w+ from .+$', line) or line.startswith('---'):
219 lines[i] = line.replace(old_source_filename, source_filename)
220 elif re.match(r'^\w+ to .+$', line) or line.startswith('+++'):
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000221 lines[i] = line.replace(old_filename, filename)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000222 self.diff_header = ''.join(lines)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000223
224 def _split_header(self, diff):
225 """Splits a diff in two: the header and the hunks."""
226 header = []
227 hunks = diff.splitlines(True)
228 while hunks:
229 header.append(hunks.pop(0))
230 if header[-1].startswith('--- '):
231 break
232 else:
233 # Some diff may not have a ---/+++ set like a git rename with no change or
234 # a svn diff with only property change.
235 pass
236
237 if hunks:
238 if not hunks[0].startswith('+++ '):
239 self._fail('Inconsistent header')
240 header.append(hunks.pop(0))
241 if hunks:
242 if not hunks[0].startswith('@@ '):
243 self._fail('Inconsistent hunk header')
244
245 # Mangle any \\ in the header to /.
246 header_lines = ('Index:', 'diff', 'copy', 'rename', '+++', '---')
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000247 basename = os.path.basename(self.filename_utf8)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000248 for i in xrange(len(header)):
249 if (header[i].split(' ', 1)[0] in header_lines or
250 header[i].endswith(basename)):
251 header[i] = header[i].replace('\\', '/')
252 return ''.join(header), ''.join(hunks)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000253
254 @staticmethod
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000255 def _is_git_diff_header(diff_header):
256 """Returns True if the diff for a single files was generated with git."""
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000257 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff
258 # Rename partial change:
259 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff
260 # Rename no change:
261 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000262 return any(l.startswith('diff --git') for l in diff_header.splitlines())
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000263
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000264 def _split_hunks(self):
265 """Splits the hunks and does verification."""
266 hunks = []
267 for line in self.diff_hunks.splitlines(True):
268 if line.startswith('@@'):
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000269 match = re.match(r'^@@ -([\d,]+) \+([\d,]+) @@.*$', line)
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000270 # File add will result in "-0,0 +1" but file deletion will result in
271 # "-1,N +0,0" where N is the number of lines deleted. That's from diff
272 # and svn diff. git diff doesn't exhibit this behavior.
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000273 # svn diff for a single line file rewrite "@@ -1 +1 @@". Fun.
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000274 # "@@ -1 +1,N @@" is also valid where N is the length of the new file.
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000275 if not match:
276 self._fail('Hunk header is unparsable')
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000277 count = match.group(1).count(',')
278 if not count:
279 start_src = int(match.group(1))
280 lines_src = 1
281 elif count == 1:
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000282 start_src, lines_src = map(int, match.group(1).split(',', 1))
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000283 else:
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000284 self._fail('Hunk header is malformed')
285
286 count = match.group(2).count(',')
287 if not count:
288 start_dst = int(match.group(2))
289 lines_dst = 1
290 elif count == 1:
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000291 start_dst, lines_dst = map(int, match.group(2).split(',', 1))
292 else:
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000293 self._fail('Hunk header is malformed')
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000294 new_hunk = Hunk(start_src, lines_src, start_dst, lines_dst)
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000295 if hunks:
296 if new_hunk.start_src <= hunks[-1].start_src:
297 self._fail('Hunks source lines are not ordered')
298 if new_hunk.start_dst <= hunks[-1].start_dst:
299 self._fail('Hunks destination lines are not ordered')
300 hunks.append(new_hunk)
301 continue
302 hunks[-1].text.append(line)
303
304 if len(hunks) == 1:
305 if hunks[0].start_src == 0 and hunks[0].lines_src == 0:
306 self.is_new = True
307 if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0:
308 self.is_delete = True
309
310 if self.is_new and self.is_delete:
311 self._fail('Hunk header is all 0')
312
313 if not self.is_new and not self.is_delete:
314 for hunk in hunks:
315 variation = (
316 len([1 for i in hunk.text if i.startswith('+')]) -
317 len([1 for i in hunk.text if i.startswith('-')]))
318 if variation != hunk.variation:
319 self._fail(
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000320 'Hunk header is incorrect: %d vs %d; %r' % (
321 variation, hunk.variation, hunk))
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000322 if not hunk.start_src:
323 self._fail(
324 'Hunk header start line is incorrect: %d' % hunk.start_src)
325 if not hunk.start_dst:
326 self._fail(
327 'Hunk header start line is incorrect: %d' % hunk.start_dst)
328 hunk.start_src -= 1
329 hunk.start_dst -= 1
330 if self.is_new and hunks:
331 hunks[0].start_dst -= 1
332 if self.is_delete and hunks:
333 hunks[0].start_src -= 1
334 return hunks
335
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000336 def _verify_git_header(self):
337 """Sanity checks the header.
338
339 Expects the following format:
340
nick@chromium.orgff526192013-06-10 19:30:26 +0000341 <garbage>
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000342 diff --git (|a/)<filename> (|b/)<filename>
343 <similarity>
344 <filemode changes>
345 <index>
346 <copy|rename from>
347 <copy|rename to>
348 --- <filename>
349 +++ <filename>
350
351 Everything is optional except the diff --git line.
352 """
353 lines = self.diff_header.splitlines()
354
355 # Verify the diff --git line.
356 old = None
357 new = None
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000358 while lines:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000359 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0))
360 if not match:
361 continue
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000362 if match.group(1).startswith('a/') and match.group(2).startswith('b/'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000363 self.patchlevel = 1
Edward Lemur5908f992017-09-12 23:49:41 +0200364 old = self._apply_patchlevel(match.group(1))
365 new = self._apply_patchlevel(match.group(2))
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000366
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000367 # The rename is about the new file so the old file can be anything.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000368 if new not in (self.filename_utf8, 'dev/null'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000369 self._fail('Unexpected git diff output name %s.' % new)
370 if old == 'dev/null' and new == 'dev/null':
371 self._fail('Unexpected /dev/null git diff.')
372 break
373
374 if not old or not new:
375 self._fail('Unexpected git diff; couldn\'t find git header.')
376
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000377 if old not in (self.filename_utf8, 'dev/null'):
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000378 # Copy or rename.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000379 self.source_filename = old.decode('utf-8')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000380 self.is_new = True
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000381
maruel@chromium.org97366be2011-06-03 20:02:46 +0000382 last_line = ''
383
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000384 while lines:
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000385 line = lines.pop(0)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000386 self._verify_git_header_process_line(lines, line, last_line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000387 last_line = line
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000388
maruel@chromium.org97366be2011-06-03 20:02:46 +0000389 # Cheap check to make sure the file name is at least mentioned in the
390 # 'diff' header. That the only remaining invariant.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000391 if not self.filename_utf8 in self.diff_header:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000392 self._fail('Diff seems corrupted.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000393
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000394 def _verify_git_header_process_line(self, lines, line, last_line):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000395 """Processes a single line of the header.
396
397 Returns True if it should continue looping.
maruel@chromium.org378a4192011-06-06 13:36:02 +0000398
399 Format is described to
400 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html
maruel@chromium.org97366be2011-06-03 20:02:46 +0000401 """
maruel@chromium.org97366be2011-06-03 20:02:46 +0000402 match = re.match(r'^(rename|copy) from (.+)$', line)
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000403 old = self.source_filename_utf8 or self.filename_utf8
maruel@chromium.org97366be2011-06-03 20:02:46 +0000404 if match:
405 if old != match.group(2):
406 self._fail('Unexpected git diff input name for line %s.' % line)
407 if not lines or not lines[0].startswith('%s to ' % match.group(1)):
408 self._fail(
409 'Confused %s from/to git diff for line %s.' %
410 (match.group(1), line))
411 return
412
maruel@chromium.org97366be2011-06-03 20:02:46 +0000413 match = re.match(r'^(rename|copy) to (.+)$', line)
414 if match:
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000415 if self.filename_utf8 != match.group(2):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000416 self._fail('Unexpected git diff output name for line %s.' % line)
417 if not last_line.startswith('%s from ' % match.group(1)):
418 self._fail(
419 'Confused %s from/to git diff for line %s.' %
420 (match.group(1), line))
421 return
422
maruel@chromium.org40052252011-11-11 20:54:55 +0000423 match = re.match(r'^deleted file mode (\d{6})$', line)
424 if match:
425 # It is necessary to parse it because there may be no hunk, like when the
426 # file was empty.
427 self.is_delete = True
428 return
429
maruel@chromium.org378a4192011-06-06 13:36:02 +0000430 match = re.match(r'^new(| file) mode (\d{6})$', line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000431 if match:
maruel@chromium.org378a4192011-06-06 13:36:02 +0000432 mode = match.group(2)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000433 # Only look at owner ACL for executable.
maruel@chromium.org86eb9e72011-06-03 20:14:52 +0000434 if bool(int(mode[4]) & 1):
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000435 self.svn_properties.append(('svn:executable', '.'))
maruel@chromium.orgdffc73c2012-09-21 19:09:16 +0000436 elif not self.source_filename and self.is_new:
437 # It's a new file, not from a rename/copy, then there's no property to
438 # delete.
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000439 self.svn_properties.append(('svn:executable', None))
maruel@chromium.org40052252011-11-11 20:54:55 +0000440 return
maruel@chromium.org97366be2011-06-03 20:02:46 +0000441
maruel@chromium.org97366be2011-06-03 20:02:46 +0000442 match = re.match(r'^--- (.*)$', line)
443 if match:
444 if last_line[:3] in ('---', '+++'):
445 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000446 if match.group(1) == '/dev/null':
447 self.is_new = True
Edward Lemur5908f992017-09-12 23:49:41 +0200448 elif self._apply_patchlevel(match.group(1)) != old:
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000449 # git patches are always well formatted, do not allow random filenames.
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000450 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000451 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000452 self._fail('Missing git diff output name.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000453 return
454
maruel@chromium.org97366be2011-06-03 20:02:46 +0000455 match = re.match(r'^\+\+\+ (.*)$', line)
456 if match:
457 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000458 self._fail('Unexpected git diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000459 if '/dev/null' == match.group(1):
460 self.is_delete = True
Edward Lemur5908f992017-09-12 23:49:41 +0200461 elif self.filename_utf8 != self._apply_patchlevel(match.group(1)):
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000462 self._fail(
463 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000464 if lines:
465 self._fail('Crap after +++')
466 # We're done.
467 return
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000468
469 def _verify_svn_header(self):
470 """Sanity checks the header.
471
472 A svn diff can contain only property changes, in that case there will be no
473 proper header. To make things worse, this property change header is
474 localized.
475 """
476 lines = self.diff_header.splitlines()
maruel@chromium.org97366be2011-06-03 20:02:46 +0000477 last_line = ''
478
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000479 while lines:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000480 line = lines.pop(0)
481 self._verify_svn_header_process_line(lines, line, last_line)
482 last_line = line
483
484 # Cheap check to make sure the file name is at least mentioned in the
485 # 'diff' header. That the only remaining invariant.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000486 if not self.filename_utf8 in self.diff_header:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000487 self._fail('Diff seems corrupted.')
488
489 def _verify_svn_header_process_line(self, lines, line, last_line):
490 """Processes a single line of the header.
491
492 Returns True if it should continue looping.
493 """
494 match = re.match(r'^--- ([^\t]+).*$', line)
495 if match:
496 if last_line[:3] in ('---', '+++'):
497 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000498 if match.group(1) == '/dev/null':
499 self.is_new = True
Edward Lemur5908f992017-09-12 23:49:41 +0200500 elif self._apply_patchlevel(match.group(1)) != self.filename_utf8:
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000501 # guess the source filename.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000502 self.source_filename = match.group(1).decode('utf-8')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000503 self.is_new = True
maruel@chromium.org97366be2011-06-03 20:02:46 +0000504 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgc4b5e762011-04-20 23:56:08 +0000505 self._fail('Nothing after header.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000506 return
507
508 match = re.match(r'^\+\+\+ ([^\t]+).*$', line)
509 if match:
510 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000511 self._fail('Unexpected diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000512 if match.group(1) == '/dev/null':
513 self.is_delete = True
Edward Lemur5908f992017-09-12 23:49:41 +0200514 elif self._apply_patchlevel(match.group(1)) != self.filename_utf8:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000515 self._fail('Unexpected diff: %s.' % match.group(1))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000516 if lines:
517 self._fail('Crap after +++')
518 # We're done.
519 return
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000520
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000521 def dump(self):
522 """Dumps itself in a verbose way to help diagnosing."""
523 return str(self) + '\n' + self.get(True)
524
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000525
526class PatchSet(object):
527 """A list of FilePatch* objects."""
528
529 def __init__(self, patches):
maruel@chromium.org5e975632011-09-29 18:07:06 +0000530 for p in patches:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000531 assert isinstance(p, FilePatchBase)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000532
maruel@chromium.org5e975632011-09-29 18:07:06 +0000533 def key(p):
534 """Sort by ordering of application.
535
536 File move are first.
537 Deletes are last.
538 """
maruel@chromium.orgde800ff2012-09-12 19:25:24 +0000539 # The bool is necessary because None < 'string' but the reverse is needed.
540 return (
541 p.is_delete,
542 # False is before True, so files *with* a source file will be first.
543 not bool(p.source_filename),
544 p.source_filename_utf8,
545 p.filename_utf8)
maruel@chromium.org5e975632011-09-29 18:07:06 +0000546
547 self.patches = sorted(patches, key=key)
548
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000549 def set_relpath(self, relpath):
550 """Used to offset the patch into a subdirectory."""
551 for patch in self.patches:
552 patch.set_relpath(relpath)
553
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000554 def __iter__(self):
555 for patch in self.patches:
556 yield patch
557
maruel@chromium.org5e975632011-09-29 18:07:06 +0000558 def __getitem__(self, key):
559 return self.patches[key]
560
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000561 @property
562 def filenames(self):
563 return [p.filename for p in self.patches]