OLD | NEW |
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 | 7 import posixpath |
8 import os | 8 import os |
9 import re | 9 import re |
10 | 10 |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
79 else: | 79 else: |
80 out += ' ' | 80 out += ' ' |
81 if self.is_new: | 81 if self.is_new: |
82 out += 'N' | 82 out += 'N' |
83 else: | 83 else: |
84 out += ' ' | 84 out += ' ' |
85 if self.source_filename: | 85 if self.source_filename: |
86 out += 'R' | 86 out += 'R' |
87 else: | 87 else: |
88 out += ' ' | 88 out += ' ' |
89 return out + ' %s->%s' % (self.source_filename, self.filename) | 89 out += ' ' |
| 90 if self.source_filename: |
| 91 out += '%s->' % self.source_filename |
| 92 return out + str(self.filename) |
90 | 93 |
91 | 94 |
92 class FilePatchDelete(FilePatchBase): | 95 class FilePatchDelete(FilePatchBase): |
93 """Deletes a file.""" | 96 """Deletes a file.""" |
94 is_delete = True | 97 is_delete = True |
95 | 98 |
96 def __init__(self, filename, is_binary): | 99 def __init__(self, filename, is_binary): |
97 super(FilePatchDelete, self).__init__(filename) | 100 super(FilePatchDelete, self).__init__(filename) |
98 self.is_binary = is_binary | 101 self.is_binary = is_binary |
99 | 102 |
100 | 103 |
101 class FilePatchBinary(FilePatchBase): | 104 class FilePatchBinary(FilePatchBase): |
102 """Content of a new binary file.""" | 105 """Content of a new binary file.""" |
103 is_binary = True | 106 is_binary = True |
104 | 107 |
105 def __init__(self, filename, data, svn_properties, is_new): | 108 def __init__(self, filename, data, svn_properties, is_new): |
106 super(FilePatchBinary, self).__init__(filename) | 109 super(FilePatchBinary, self).__init__(filename) |
107 self.data = data | 110 self.data = data |
108 self.svn_properties = svn_properties or [] | 111 self.svn_properties = svn_properties or [] |
109 self.is_new = is_new | 112 self.is_new = is_new |
110 | 113 |
111 def get(self): | 114 def get(self): |
112 return self.data | 115 return self.data |
113 | 116 |
114 | 117 |
| 118 class Hunk(object): |
| 119 """Parsed hunk data container.""" |
| 120 |
| 121 def __init__(self, start_src, lines_src, start_dst, lines_dst): |
| 122 self.start_src = start_src |
| 123 self.lines_src = lines_src |
| 124 self.start_dst = start_dst |
| 125 self.lines_dst = lines_dst |
| 126 self.variation = self.lines_dst - self.lines_src |
| 127 self.text = [] |
| 128 |
| 129 |
115 class FilePatchDiff(FilePatchBase): | 130 class FilePatchDiff(FilePatchBase): |
116 """Patch for a single file.""" | 131 """Patch for a single file.""" |
117 | 132 |
118 def __init__(self, filename, diff, svn_properties): | 133 def __init__(self, filename, diff, svn_properties): |
119 super(FilePatchDiff, self).__init__(filename) | 134 super(FilePatchDiff, self).__init__(filename) |
120 if not diff: | 135 if not diff: |
121 self._fail('File doesn\'t have a diff.') | 136 self._fail('File doesn\'t have a diff.') |
122 self.diff_header, self.diff_hunks = self._split_header(diff) | 137 self.diff_header, self.diff_hunks = self._split_header(diff) |
123 self.svn_properties = svn_properties or [] | 138 self.svn_properties = svn_properties or [] |
124 self.is_git_diff = self._is_git_diff_header(self.diff_header) | 139 self.is_git_diff = self._is_git_diff_header(self.diff_header) |
125 self.patchlevel = 0 | 140 self.patchlevel = 0 |
126 if self.is_git_diff: | 141 if self.is_git_diff: |
127 self._verify_git_header() | 142 self._verify_git_header() |
128 else: | 143 else: |
129 self._verify_svn_header() | 144 self._verify_svn_header() |
| 145 self.hunks = self._split_hunks() |
130 if self.source_filename and not self.is_new: | 146 if self.source_filename and not self.is_new: |
131 self._fail('If source_filename is set, is_new must be also be set') | 147 self._fail('If source_filename is set, is_new must be also be set') |
132 | 148 |
133 def get(self, for_git): | 149 def get(self, for_git): |
134 if for_git or not self.source_filename: | 150 if for_git or not self.source_filename: |
135 return self.diff_header + self.diff_hunks | 151 return self.diff_header + self.diff_hunks |
136 else: | 152 else: |
137 # patch is stupid. It patches the source_filename instead so get rid of | 153 # patch is stupid. It patches the source_filename instead so get rid of |
138 # any source_filename reference if needed. | 154 # any source_filename reference if needed. |
139 return ( | 155 return ( |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
191 @staticmethod | 207 @staticmethod |
192 def _is_git_diff_header(diff_header): | 208 def _is_git_diff_header(diff_header): |
193 """Returns True if the diff for a single files was generated with git.""" | 209 """Returns True if the diff for a single files was generated with git.""" |
194 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff | 210 # Delete: http://codereview.chromium.org/download/issue6368055_22_29.diff |
195 # Rename partial change: | 211 # Rename partial change: |
196 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff | 212 # http://codereview.chromium.org/download/issue6250123_3013_6010.diff |
197 # Rename no change: | 213 # Rename no change: |
198 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff | 214 # http://codereview.chromium.org/download/issue6287022_3001_4010.diff |
199 return any(l.startswith('diff --git') for l in diff_header.splitlines()) | 215 return any(l.startswith('diff --git') for l in diff_header.splitlines()) |
200 | 216 |
| 217 def _split_hunks(self): |
| 218 """Splits the hunks and does verification.""" |
| 219 hunks = [] |
| 220 for line in self.diff_hunks.splitlines(True): |
| 221 if line.startswith('@@'): |
| 222 match = re.match(r'^@@ -(\d+),(\d+) \+([\d,]+) @@.*$', line) |
| 223 # File add will result in "-0,0 +1" but file deletion will result in |
| 224 # "-1,N +0,0" where N is the number of lines deleted. That's from diff |
| 225 # and svn diff. git diff doesn't exhibit this behavior. |
| 226 if not match: |
| 227 self._fail('Hunk header is unparsable') |
| 228 if ',' in match.group(3): |
| 229 start_dst, lines_dst = map(int, match.group(3).split(',', 1)) |
| 230 else: |
| 231 start_dst = int(match.group(3)) |
| 232 lines_dst = 0 |
| 233 new_hunk = Hunk(int(match.group(1)), int(match.group(2)), |
| 234 start_dst, lines_dst) |
| 235 if hunks: |
| 236 if new_hunk.start_src <= hunks[-1].start_src: |
| 237 self._fail('Hunks source lines are not ordered') |
| 238 if new_hunk.start_dst <= hunks[-1].start_dst: |
| 239 self._fail('Hunks destination lines are not ordered') |
| 240 hunks.append(new_hunk) |
| 241 continue |
| 242 hunks[-1].text.append(line) |
| 243 |
| 244 if len(hunks) == 1: |
| 245 if hunks[0].start_src == 0 and hunks[0].lines_src == 0: |
| 246 self.is_new = True |
| 247 if hunks[0].start_dst == 0 and hunks[0].lines_dst == 0: |
| 248 self.is_delete = True |
| 249 |
| 250 if self.is_new and self.is_delete: |
| 251 self._fail('Hunk header is all 0') |
| 252 |
| 253 if not self.is_new and not self.is_delete: |
| 254 for hunk in hunks: |
| 255 variation = ( |
| 256 len([1 for i in hunk.text if i.startswith('+')]) - |
| 257 len([1 for i in hunk.text if i.startswith('-')])) |
| 258 if variation != hunk.variation: |
| 259 self._fail( |
| 260 'Hunk header is incorrect: %d vs %d' % ( |
| 261 variation, hunk.variation)) |
| 262 if not hunk.start_src: |
| 263 self._fail( |
| 264 'Hunk header start line is incorrect: %d' % hunk.start_src) |
| 265 if not hunk.start_dst: |
| 266 self._fail( |
| 267 'Hunk header start line is incorrect: %d' % hunk.start_dst) |
| 268 hunk.start_src -= 1 |
| 269 hunk.start_dst -= 1 |
| 270 if self.is_new and hunks: |
| 271 hunks[0].start_dst -= 1 |
| 272 if self.is_delete and hunks: |
| 273 hunks[0].start_src -= 1 |
| 274 return hunks |
| 275 |
201 def mangle(self, string): | 276 def mangle(self, string): |
202 """Mangle a file path.""" | 277 """Mangle a file path.""" |
203 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) | 278 return '/'.join(string.replace('\\', '/').split('/')[self.patchlevel:]) |
204 | 279 |
205 def _verify_git_header(self): | 280 def _verify_git_header(self): |
206 """Sanity checks the header. | 281 """Sanity checks the header. |
207 | 282 |
208 Expects the following format: | 283 Expects the following format: |
209 | 284 |
210 <garbagge> | 285 <garbagge> |
(...skipping 203 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
414 def __iter__(self): | 489 def __iter__(self): |
415 for patch in self.patches: | 490 for patch in self.patches: |
416 yield patch | 491 yield patch |
417 | 492 |
418 def __getitem__(self, key): | 493 def __getitem__(self, key): |
419 return self.patches[key] | 494 return self.patches[key] |
420 | 495 |
421 @property | 496 @property |
422 def filenames(self): | 497 def filenames(self): |
423 return [p.filename for p in self.patches] | 498 return [p.filename for p in self.patches] |
OLD | NEW |