blob: 1bc608c9f9ef03fbd25752b51bc91eb75bff211a [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.org8fab6b62012-02-16 21:50:35 +000040 @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
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000049 @staticmethod
50 def _process_filename(filename):
51 filename = filename.replace('\\', '/')
maruel@chromium.orgcd619402011-04-09 00:08:00 +000052 # Blacklist a few characters for simplicity.
phajdan.jr@chromium.orgca858012015-03-27 15:21:53 +000053 for i in ('$', '..', '\'', '"', '<', '>', ':', '|', '?', '*'):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000054 if i in filename:
55 raise UnsupportedPatchFormat(
56 filename, 'Can\'t use \'%s\' in filename.' % i)
sullivan@chromium.org8770f482015-05-27 18:26:46 +000057 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)
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000066 return filename
maruel@chromium.orgcd619402011-04-09 00:08:00 +000067
maruel@chromium.orgcd619402011-04-09 00:08:00 +000068 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])
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000074 self.filename = self._process_filename(
75 posixpath.join(relpath, self.filename))
maruel@chromium.orga19047c2011-09-08 12:49:58 +000076 if self.source_filename:
77 self.source_filename = self._process_filename(
78 posixpath.join(relpath, self.source_filename))
maruel@chromium.orgcd619402011-04-09 00:08:00 +000079
80 def _fail(self, msg):
maruel@chromium.orgbe113f12011-09-01 15:05:34 +000081 """Shortcut function to raise UnsupportedPatchFormat."""
maruel@chromium.orgcd619402011-04-09 00:08:00 +000082 raise UnsupportedPatchFormat(self.filename, msg)
83
maruel@chromium.org5e975632011-09-29 18:07:06 +000084 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 += ' '
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000103 out += ' '
104 if self.source_filename:
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000105 out += '%s->' % self.source_filename_utf8
106 return out + self.filename_utf8
maruel@chromium.org5e975632011-09-29 18:07:06 +0000107
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000108 def dump(self):
109 """Dumps itself in a verbose way to help diagnosing."""
110 return str(self)
111
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000112
113class FilePatchDelete(FilePatchBase):
114 """Deletes a file."""
115 is_delete = True
116
117 def __init__(self, filename, is_binary):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000118 super(FilePatchDelete, self).__init__(filename)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000119 self.is_binary = is_binary
120
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000121
122class FilePatchBinary(FilePatchBase):
123 """Content of a new binary file."""
124 is_binary = True
125
maruel@chromium.org97366be2011-06-03 20:02:46 +0000126 def __init__(self, filename, data, svn_properties, is_new):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000127 super(FilePatchBinary, self).__init__(filename)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000128 self.data = data
129 self.svn_properties = svn_properties or []
maruel@chromium.org97366be2011-06-03 20:02:46 +0000130 self.is_new = is_new
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000131
132 def get(self):
133 return self.data
134
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000135 def __str__(self):
136 return str(super(FilePatchBinary, self)) + ' %d bytes' % len(self.data)
137
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000138
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000139class 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
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000150 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
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000155
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000156class FilePatchDiff(FilePatchBase):
157 """Patch for a single file."""
158
159 def __init__(self, filename, diff, svn_properties):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000160 super(FilePatchDiff, self).__init__(filename)
maruel@chromium.org61e0b692011-04-12 21:01:01 +0000161 if not diff:
162 self._fail('File doesn\'t have a diff.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000163 self.diff_header, self.diff_hunks = self._split_header(diff)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000164 self.svn_properties = svn_properties or []
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000165 self.is_git_diff = self._is_git_diff_header(self.diff_header)
Edward Lesmesf8792072017-09-13 08:05:12 +0000166 self.patchlevel = 0
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000167 if self.is_git_diff:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000168 self._verify_git_header()
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000169 else:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000170 self._verify_svn_header()
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000171 self.hunks = self._split_hunks()
maruel@chromium.org5e975632011-09-29 18:07:06 +0000172 if self.source_filename and not self.is_new:
173 self._fail('If source_filename is set, is_new must be also be set')
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000174
maruel@chromium.org5e975632011-09-29 18:07:06 +0000175 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 (
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000182 self.diff_header.replace(
183 self.source_filename_utf8, self.filename_utf8) +
maruel@chromium.org5e975632011-09-29 18:07:06 +0000184 self.diff_hunks)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000185
186 def set_relpath(self, relpath):
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000187 old_filename = self.filename_utf8
188 old_source_filename = self.source_filename_utf8 or self.filename_utf8
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000189 super(FilePatchDiff, self).set_relpath(relpath)
190 # Update the header too.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000191 filename = self.filename_utf8
192 source_filename = self.source_filename_utf8 or self.filename_utf8
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000193 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(
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000198 'b/' + old_filename, filename)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000199 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('+++'):
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000202 lines[i] = line.replace(old_filename, filename)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000203 self.diff_header = ''.join(lines)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000204
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', '+++', '---')
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000228 basename = os.path.basename(self.filename_utf8)
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000229 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)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000234
235 @staticmethod
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000236 def _is_git_diff_header(diff_header):
237 """Returns True if the diff for a single files was generated with git."""
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000238 # 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
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000243 return any(l.startswith('diff --git') for l in diff_header.splitlines())
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000244
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000245 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('@@'):
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000250 match = re.match(r'^@@ -([\d,]+) \+([\d,]+) @@.*$', line)
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000251 # 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.
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000254 # svn diff for a single line file rewrite "@@ -1 +1 @@". Fun.
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000255 # "@@ -1 +1,N @@" is also valid where N is the length of the new file.
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000256 if not match:
257 self._fail('Hunk header is unparsable')
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000258 count = match.group(1).count(',')
259 if not count:
260 start_src = int(match.group(1))
261 lines_src = 1
262 elif count == 1:
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000263 start_src, lines_src = map(int, match.group(1).split(',', 1))
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000264 else:
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000265 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:
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000272 start_dst, lines_dst = map(int, match.group(2).split(',', 1))
273 else:
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000274 self._fail('Hunk header is malformed')
maruel@chromium.orgdb1fd782012-01-11 01:51:29 +0000275 new_hunk = Hunk(start_src, lines_src, start_dst, lines_dst)
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000276 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(
maruel@chromium.org17fa4be2012-08-29 17:18:12 +0000301 'Hunk header is incorrect: %d vs %d; %r' % (
302 variation, hunk.variation, hunk))
maruel@chromium.orgcf602552012-01-10 19:49:31 +0000303 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:12 +0000317 def mangle(self, string):
318 """Mangle a file path."""
319 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:])
320
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000321 def _verify_git_header(self):
322 """Sanity checks the header.
323
324 Expects the following format:
325
nick@chromium.orgff526192013-06-10 19:30:26 +0000326 <garbage>
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000327 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
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000343 while lines:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000344 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0))
345 if not match:
346 continue
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000347 if match.group(1).startswith('a/') and match.group(2).startswith('b/'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000348 self.patchlevel = 1
Edward Lesmesf8792072017-09-13 08:05:12 +0000349 old = self.mangle(match.group(1))
350 new = self.mangle(match.group(2))
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000351
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000352 # The rename is about the new file so the old file can be anything.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000353 if new not in (self.filename_utf8, 'dev/null'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000354 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
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000362 if old not in (self.filename_utf8, 'dev/null'):
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000363 # Copy or rename.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000364 self.source_filename = old.decode('utf-8')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000365 self.is_new = True
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000366
maruel@chromium.org97366be2011-06-03 20:02:46 +0000367 last_line = ''
368
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000369 while lines:
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000370 line = lines.pop(0)
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000371 self._verify_git_header_process_line(lines, line, last_line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000372 last_line = line
maruel@chromium.orgb6ffdaf2011-06-03 19:23:16 +0000373
maruel@chromium.org97366be2011-06-03 20:02:46 +0000374 # Cheap check to make sure the file name is at least mentioned in the
375 # 'diff' header. That the only remaining invariant.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000376 if not self.filename_utf8 in self.diff_header:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000377 self._fail('Diff seems corrupted.')
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000378
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000379 def _verify_git_header_process_line(self, lines, line, last_line):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000380 """Processes a single line of the header.
381
382 Returns True if it should continue looping.
maruel@chromium.org378a4192011-06-06 13:36:02 +0000383
384 Format is described to
385 http://www.kernel.org/pub/software/scm/git/docs/git-diff.html
maruel@chromium.org97366be2011-06-03 20:02:46 +0000386 """
maruel@chromium.org97366be2011-06-03 20:02:46 +0000387 match = re.match(r'^(rename|copy) from (.+)$', line)
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000388 old = self.source_filename_utf8 or self.filename_utf8
maruel@chromium.org97366be2011-06-03 20:02:46 +0000389 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
maruel@chromium.org97366be2011-06-03 20:02:46 +0000398 match = re.match(r'^(rename|copy) to (.+)$', line)
399 if match:
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000400 if self.filename_utf8 != match.group(2):
maruel@chromium.org97366be2011-06-03 20:02:46 +0000401 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
maruel@chromium.org40052252011-11-11 20:54:55 +0000408 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
maruel@chromium.org378a4192011-06-06 13:36:02 +0000415 match = re.match(r'^new(| file) mode (\d{6})$', line)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000416 if match:
maruel@chromium.org378a4192011-06-06 13:36:02 +0000417 mode = match.group(2)
maruel@chromium.org97366be2011-06-03 20:02:46 +0000418 # Only look at owner ACL for executable.
maruel@chromium.org86eb9e72011-06-03 20:14:52 +0000419 if bool(int(mode[4]) & 1):
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000420 self.svn_properties.append(('svn:executable', '.'))
maruel@chromium.orgdffc73c2012-09-21 19:09:16 +0000421 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.
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000424 self.svn_properties.append(('svn:executable', None))
maruel@chromium.org40052252011-11-11 20:54:55 +0000425 return
maruel@chromium.org97366be2011-06-03 20:02:46 +0000426
maruel@chromium.org97366be2011-06-03 20:02:46 +0000427 match = re.match(r'^--- (.*)$', line)
428 if match:
429 if last_line[:3] in ('---', '+++'):
430 self._fail('--- and +++ are reversed')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000431 if match.group(1) == '/dev/null':
432 self.is_new = True
Edward Lesmesf8792072017-09-13 08:05:12 +0000433 elif self.mangle(match.group(1)) != old:
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000434 # git patches are always well formatted, do not allow random filenames.
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000435 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000436 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000437 self._fail('Missing git diff output name.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000438 return
439
maruel@chromium.org97366be2011-06-03 20:02:46 +0000440 match = re.match(r'^\+\+\+ (.*)$', line)
441 if match:
442 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000443 self._fail('Unexpected git diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000444 if '/dev/null' == match.group(1):
445 self.is_delete = True
Edward Lesmesf8792072017-09-13 08:05:12 +0000446 elif self.filename_utf8 != self.mangle(match.group(1)):
maruel@chromium.orga19047c2011-09-08 12:49:58 +0000447 self._fail(
448 'Unexpected git diff: %s != %s.' % (self.filename, match.group(1)))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000449 if lines:
450 self._fail('Crap after +++')
451 # We're done.
452 return
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000453
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()
maruel@chromium.org97366be2011-06-03 20:02:46 +0000462 last_line = ''
463
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000464 while lines:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000465 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.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000471 if not self.filename_utf8 in self.diff_header:
maruel@chromium.org97366be2011-06-03 20:02:46 +0000472 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')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000483 if match.group(1) == '/dev/null':
484 self.is_new = True
Edward Lesmesf8792072017-09-13 08:05:12 +0000485 elif self.mangle(match.group(1)) != self.filename_utf8:
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000486 # guess the source filename.
maruel@chromium.org8fab6b62012-02-16 21:50:35 +0000487 self.source_filename = match.group(1).decode('utf-8')
maruel@chromium.org8baaea72011-09-08 12:55:29 +0000488 self.is_new = True
maruel@chromium.org97366be2011-06-03 20:02:46 +0000489 if not lines or not lines[0].startswith('+++'):
maruel@chromium.orgc4b5e762011-04-20 23:56:08 +0000490 self._fail('Nothing after header.')
maruel@chromium.org97366be2011-06-03 20:02:46 +0000491 return
492
493 match = re.match(r'^\+\+\+ ([^\t]+).*$', line)
494 if match:
495 if not last_line.startswith('---'):
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000496 self._fail('Unexpected diff: --- not following +++.')
maruel@chromium.orgbe605652011-09-02 20:28:07 +0000497 if match.group(1) == '/dev/null':
498 self.is_delete = True
Edward Lesmesf8792072017-09-13 08:05:12 +0000499 elif self.mangle(match.group(1)) != self.filename_utf8:
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000500 self._fail('Unexpected diff: %s.' % match.group(1))
maruel@chromium.org97366be2011-06-03 20:02:46 +0000501 if lines:
502 self._fail('Crap after +++')
503 # We're done.
504 return
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000505
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000506 def dump(self):
507 """Dumps itself in a verbose way to help diagnosing."""
508 return str(self) + '\n' + self.get(True)
509
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000510
511class PatchSet(object):
512 """A list of FilePatch* objects."""
513
514 def __init__(self, patches):
maruel@chromium.org5e975632011-09-29 18:07:06 +0000515 for p in patches:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000516 assert isinstance(p, FilePatchBase)
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000517
maruel@chromium.org5e975632011-09-29 18:07:06 +0000518 def key(p):
519 """Sort by ordering of application.
520
521 File move are first.
522 Deletes are last.
523 """
maruel@chromium.orgde800ff2012-09-12 19:25:24 +0000524 # 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)
maruel@chromium.org5e975632011-09-29 18:07:06 +0000531
532 self.patches = sorted(patches, key=key)
533
maruel@chromium.orgcd619402011-04-09 00:08:00 +0000534 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
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000539 def __iter__(self):
540 for patch in self.patches:
541 yield patch
542
maruel@chromium.org5e975632011-09-29 18:07:06 +0000543 def __getitem__(self, key):
544 return self.patches[key]
545
maruel@chromium.orgb3727a32011-04-04 19:31:44 +0000546 @property
547 def filenames(self):
548 return [p.filename for p in self.patches]