Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(273)

Side by Side Diff: patch.py

Issue 6802021: Add support to put a patchset into a subdirectory. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: fixed and more tests Created 9 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | tests/patch_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # coding=utf8 1 # coding=utf8
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 """Utility functions to handle patches.""" 5 """Utility functions to handle patches."""
6 6
7 import posixpath
8 import os
7 import re 9 import re
8 10
9 11
10 class UnsupportedPatchFormat(Exception): 12 class UnsupportedPatchFormat(Exception):
11 def __init__(self, filename, status): 13 def __init__(self, filename, status):
12 super(UnsupportedPatchFormat, self).__init__(filename, status) 14 super(UnsupportedPatchFormat, self).__init__(filename, status)
13 self.filename = filename 15 self.filename = filename
14 self.status = status 16 self.status = status
15 17
16 def __str__(self): 18 def __str__(self):
17 out = 'Can\'t process patch for file %s.' % self.filename 19 out = 'Can\'t process patch for file %s.' % self.filename
18 if self.status: 20 if self.status:
19 out += '\n%s' % self.status 21 out += '\n%s' % self.status
20 return out 22 return out
21 23
22 24
23 class FilePatchBase(object): 25 class FilePatchBase(object):
24 """Defines a single file being modified.""" 26 """Defines a single file being modified.
27
28 '/' is always used instead of os.sep for consistency.
29 """
25 is_delete = False 30 is_delete = False
26 is_binary = False 31 is_binary = False
27 32
33 def __init__(self, filename):
34 self.filename = None
35 self._set_filename(filename)
36
37 def _set_filename(self, filename):
38 self.filename = filename.replace('\\', '/')
39 # Blacklist a few characters for simplicity.
40 for i in ('%', '$', '..', '\'', '"'):
41 if i in self.filename:
42 self._fail('Can\'t use \'%s\' in filename.' % i)
43 for i in ('/', 'CON', 'COM'):
44 if self.filename.startswith(i):
45 self._fail('Filename can\'t start with \'%s\'.' % i)
46
28 def get(self): 47 def get(self):
29 raise NotImplementedError('Nothing to grab') 48 raise NotImplementedError('Nothing to grab')
30 49
50 def set_relpath(self, relpath):
51 if not relpath:
52 return
53 relpath = relpath.replace('\\', '/')
54 if relpath[0] == '/':
55 self._fail('Relative path starts with %s' % relpath[0])
56 self._set_filename(posixpath.join(relpath, self.filename))
57
58 def _fail(self, msg):
59 raise UnsupportedPatchFormat(self.filename, msg)
60
31 61
32 class FilePatchDelete(FilePatchBase): 62 class FilePatchDelete(FilePatchBase):
33 """Deletes a file.""" 63 """Deletes a file."""
34 is_delete = True 64 is_delete = True
35 65
36 def __init__(self, filename, is_binary): 66 def __init__(self, filename, is_binary):
37 super(FilePatchDelete, self).__init__() 67 super(FilePatchDelete, self).__init__(filename)
38 self.filename = filename
39 self.is_binary = is_binary 68 self.is_binary = is_binary
40 69
41 def get(self): 70 def get(self):
42 raise NotImplementedError('Nothing to grab') 71 raise NotImplementedError('Nothing to grab')
43 72
44 73
45 class FilePatchBinary(FilePatchBase): 74 class FilePatchBinary(FilePatchBase):
46 """Content of a new binary file.""" 75 """Content of a new binary file."""
47 is_binary = True 76 is_binary = True
48 77
49 def __init__(self, filename, data, svn_properties): 78 def __init__(self, filename, data, svn_properties):
50 super(FilePatchBinary, self).__init__() 79 super(FilePatchBinary, self).__init__(filename)
51 self.filename = filename
52 self.data = data 80 self.data = data
53 self.svn_properties = svn_properties or [] 81 self.svn_properties = svn_properties or []
54 82
55 def get(self): 83 def get(self):
56 return self.data 84 return self.data
57 85
58 86
59 class FilePatchDiff(FilePatchBase): 87 class FilePatchDiff(FilePatchBase):
60 """Patch for a single file.""" 88 """Patch for a single file."""
61 89
62 def __init__(self, filename, diff, svn_properties): 90 def __init__(self, filename, diff, svn_properties):
63 super(FilePatchDiff, self).__init__() 91 super(FilePatchDiff, self).__init__(filename)
64 self.filename = filename 92 self.diff_header, self.diff_hunks = self._split_header(diff)
65 self.diff = diff
66 self.svn_properties = svn_properties or [] 93 self.svn_properties = svn_properties or []
67 self.is_git_diff = self._is_git_diff(diff) 94 self.is_git_diff = self._is_git_diff_header(self.diff_header)
95 self.patchlevel = 0
68 if self.is_git_diff: 96 if self.is_git_diff:
69 self.patchlevel = 1 97 self._verify_git_header()
70 self._verify_git_patch(filename, diff)
71 assert not svn_properties 98 assert not svn_properties
72 else: 99 else:
73 self.patchlevel = 0 100 self._verify_svn_header()
74 self._verify_svn_patch(filename, diff)
75 101
76 def get(self): 102 def get(self):
77 return self.diff 103 return self.diff_header + self.diff_hunks
104
105 def set_relpath(self, relpath):
106 old_filename = self.filename
107 super(FilePatchDiff, self).set_relpath(relpath)
108 # Update the header too.
109 self.diff_header = self.diff_header.replace(old_filename, self.filename)
110
111 def _split_header(self, diff):
112 """Splits a diff in two: the header and the hunks."""
113 header = []
114 hunks = diff.splitlines(True)
115 while hunks:
116 header.append(hunks.pop(0))
117 if header[-1].startswith('--- '):
118 break
119 else:
120 # Some diff may not have a ---/+++ set like a git rename with no change or
121 # a svn diff with only property change.
122 pass
123
124 if hunks:
125 if not hunks[0].startswith('+++ '):
126 self._fail('Inconsistent header')
127 header.append(hunks.pop(0))
128 if hunks:
129 if not hunks[0].startswith('@@ '):
130 self._fail('Inconsistent hunk header')
131
132 # Mangle any \\ in the header to /.
133 header_lines = ('Index:', 'diff', 'copy', 'rename', '+++', '---')
134 basename = os.path.basename(self.filename)
135 for i in xrange(len(header)):
136 if (header[i].split(' ', 1)[0] in header_lines or
137 header[i].endswith(basename)):
138 header[i] = header[i].replace('\\', '/')
139 return ''.join(header), ''.join(hunks)
78 140
79 @staticmethod 141 @staticmethod
80 def _is_git_diff(diff): 142 def _is_git_diff_header(diff_header):
81 """Returns True if the diff for a single files was generated with gt. 143 """Returns True if the diff for a single files was generated with git."""
82
83 Expects the following format:
84
85 Index: <filename>
86 diff --git a/<filename> b/<filename>
87 <filemode changes>
88 <index>
89 --- <filename>
90 +++ <filename>
91 <hunks>
92
93 Index: is a rietveld specific line.
94 """
95 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff 144 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff
96 # Rename partial change: 145 # Rename partial change:
97 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff 146 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff
98 # Rename no change: 147 # Rename no change:
99 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff 148 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff
100 return any(l.startswith('diff --git') for l in diff.splitlines()[:3]) 149 return any(l.startswith('diff --git') for l in diff_header.splitlines())
101 150
102 @staticmethod 151 def mangle(self, string):
103 def _verify_git_patch(filename, diff): 152 """Mangle a file path."""
104 lines = diff.splitlines() 153 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:])
105 # First fine the git diff header: 154
155 def _verify_git_header(self):
156 """Sanity checks the header.
157
158 Expects the following format:
159
160 <garbagge>
161 diff --git (|a/)<filename> (|b/)<filename>
162 <similarity>
163 <filemode changes>
164 <index>
165 <copy|rename from>
166 <copy|rename to>
167 --- <filename>
168 +++ <filename>
169
170 Everything is optional except the diff --git line.
171 """
172 lines = self.diff_header.splitlines()
173
174 # Verify the diff --git line.
175 old = None
176 new = None
106 while lines: 177 while lines:
107 line = lines.pop(0) 178 match = re.match(r'^diff \-\-git (.*?) (.*)$', lines.pop(0))
108 match = re.match(r'^diff \-\-git a\/(.*?) b\/(.*)$', line) 179 if not match:
109 if match: 180 continue
110 a = match.group(1) 181 old = match.group(1).replace('\\', '/')
111 b = match.group(2) 182 new = match.group(2).replace('\\', '/')
112 if a != filename and a != 'dev/null': 183 if old.startswith('a/') and new.startswith('b/'):
113 raise UnsupportedPatchFormat( 184 self.patchlevel = 1
114 filename, 'Unexpected git diff input name.') 185 old = old[2:]
115 if b != filename and b != 'dev/null': 186 new = new[2:]
116 raise UnsupportedPatchFormat( 187 # The rename is about the new file so the old file can be anything.
117 filename, 'Unexpected git diff output name.') 188 if new not in (self.filename, 'dev/null'):
118 if a == 'dev/null' and b == 'dev/null': 189 self._fail('Unexpected git diff output name %s.' % new)
119 raise UnsupportedPatchFormat( 190 if old == 'dev/null' and new == 'dev/null':
120 filename, 'Unexpected /dev/null git diff.') 191 self._fail('Unexpected /dev/null git diff.')
192 break
193
194 if not old or not new:
195 self._fail('Unexpected git diff; couldn\'t find git header.')
196
197 # Handle these:
198 # rename from <>
199 # rename to <>
200 # copy from <>
201 # copy to <>
202 while lines:
203 if lines[0].startswith('--- '):
121 break 204 break
205 match = re.match(r'^(rename|copy) from (.+)$', lines.pop(0))
206 if not match:
207 continue
208 if old != match.group(2):
209 self._fail('Unexpected git diff input name for %s.' % match.group(1))
210 if not lines:
211 self._fail('Missing git diff output name for %s.' % match.group(1))
212 match = re.match(r'^(rename|copy) to (.+)$', lines.pop(0))
213 if not match:
214 self._fail('Missing git diff output name for %s.' % match.group(1))
215 if new != match.group(2):
216 self._fail('Unexpected git diff output name for %s.' % match.group(1))
217
218 # Handle ---/+++
219 while lines:
220 match = re.match(r'^--- (.*)$', lines.pop(0))
221 if not match:
222 continue
223 if old != self.mangle(match.group(1)) and match.group(1) != '/dev/null':
224 self._fail('Unexpected git diff: %s != %s.' % (old, match.group(1)))
225 if not lines:
226 self._fail('Missing git diff output name.')
227 match = re.match(r'^\+\+\+ (.*)$', lines.pop(0))
228 if not match:
229 self._fail('Unexpected git diff: --- not following +++.')
230 if new != self.mangle(match.group(1)) and '/dev/null' != match.group(1):
231 self._fail('Unexpected git diff: %s != %s.' % (new, match.group(1)))
232 assert not lines, '_split_header() is broken'
233 break
234
235 def _verify_svn_header(self):
236 """Sanity checks the header.
237
238 A svn diff can contain only property changes, in that case there will be no
239 proper header. To make things worse, this property change header is
240 localized.
241 """
242 lines = self.diff_header.splitlines()
243 while lines:
244 match = re.match(r'^--- ([^\t]+).*$', lines.pop(0))
245 if not match:
246 continue
247 if match.group(1) not in (self.filename, '/dev/null'):
248 self._fail('Unexpected diff: %s.' % match.group(1))
249 match = re.match(r'^\+\+\+ ([^\t]+).*$', lines.pop(0))
250 if not match:
251 self._fail('Unexpected diff: --- not following +++.')
252 if match.group(1) not in (self.filename, '/dev/null'):
253 self._fail('Unexpected diff: %s.' % match.group(1))
254 assert not lines, '_split_header() is broken'
255 break
122 else: 256 else:
123 raise UnsupportedPatchFormat( 257 # Cheap check to make sure the file name is at least mentioned in the
124 filename, 'Unexpected git diff; couldn\'t find git header.') 258 # 'diff' header. That the only remaining invariant.
125 259 if not self.filename in self.diff_header:
126 while lines: 260 self._fail('Diff seems corrupted.')
127 line = lines.pop(0)
128 match = re.match(r'^--- a/(.*)$', line)
129 if match:
130 if a != match.group(1):
131 raise UnsupportedPatchFormat(
132 filename, 'Unexpected git diff: %s != %s.' % (a, match.group(1)))
133 match = re.match(r'^\+\+\+ b/(.*)$', lines.pop(0))
134 if not match:
135 raise UnsupportedPatchFormat(
136 filename, 'Unexpected git diff: --- not following +++.')
137 if b != match.group(1):
138 raise UnsupportedPatchFormat(
139 filename, 'Unexpected git diff: %s != %s.' % (b, match.group(1)))
140 break
141 # Don't fail if the patch header is not found, the diff could be a
142 # file-mode-change-only diff.
143
144 @staticmethod
145 def _verify_svn_patch(filename, diff):
146 lines = diff.splitlines()
147 while lines:
148 line = lines.pop(0)
149 match = re.match(r'^--- ([^\t]+).*$', line)
150 if match:
151 if match.group(1) not in (filename, '/dev/null'):
152 raise UnsupportedPatchFormat(
153 filename,
154 'Unexpected diff: %s != %s.' % (filename, match.group(1)))
155 # Grab next line.
156 line2 = lines.pop(0)
157 match = re.match(r'^\+\+\+ ([^\t]+).*$', line2)
158 if not match:
159 raise UnsupportedPatchFormat(
160 filename,
161 'Unexpected diff: --- not following +++.:\n%s\n%s' % (
162 line, line2))
163 if match.group(1) not in (filename, '/dev/null'):
164 raise UnsupportedPatchFormat(
165 filename,
166 'Unexpected diff: %s != %s.' % (filename, match.group(1)))
167 break
168 # Don't fail if the patch header is not found, the diff could be a
169 # svn-property-change-only diff.
170 261
171 262
172 class PatchSet(object): 263 class PatchSet(object):
173 """A list of FilePatch* objects.""" 264 """A list of FilePatch* objects."""
174 265
175 def __init__(self, patches): 266 def __init__(self, patches):
176 self.patches = patches 267 self.patches = patches
177 268
269 def set_relpath(self, relpath):
270 """Used to offset the patch into a subdirectory."""
271 for patch in self.patches:
272 patch.set_relpath(relpath)
273
178 def __iter__(self): 274 def __iter__(self):
179 for patch in self.patches: 275 for patch in self.patches:
180 yield patch 276 yield patch
181 277
182 @property 278 @property
183 def filenames(self): 279 def filenames(self):
184 return [p.filename for p in self.patches] 280 return [p.filename for p in self.patches]
OLDNEW
« no previous file with comments | « no previous file | tests/patch_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698