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

Side by Side Diff: patch.py

Issue 8038056: Fix handling of file renames. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Rebase against HEAD Created 9 years, 2 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 | « checkout.py ('k') | tests/checkout_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 7 import posixpath
8 import os 8 import os
9 import re 9 import re
10 10
(...skipping 14 matching lines...) Expand all
25 class FilePatchBase(object): 25 class FilePatchBase(object):
26 """Defines a single file being modified. 26 """Defines a single file being modified.
27 27
28 '/' is always used instead of os.sep for consistency. 28 '/' is always used instead of os.sep for consistency.
29 """ 29 """
30 is_delete = False 30 is_delete = False
31 is_binary = False 31 is_binary = False
32 is_new = False 32 is_new = False
33 33
34 def __init__(self, filename): 34 def __init__(self, filename):
35 assert self.__class__ is not FilePatchBase
35 self.filename = self._process_filename(filename) 36 self.filename = self._process_filename(filename)
36 # Set when the file is copied or moved. 37 # Set when the file is copied or moved.
37 self.source_filename = None 38 self.source_filename = None
38 39
39 @staticmethod 40 @staticmethod
40 def _process_filename(filename): 41 def _process_filename(filename):
41 filename = filename.replace('\\', '/') 42 filename = filename.replace('\\', '/')
42 # Blacklist a few characters for simplicity. 43 # Blacklist a few characters for simplicity.
43 for i in ('%', '$', '..', '\'', '"'): 44 for i in ('%', '$', '..', '\'', '"'):
44 if i in filename: 45 if i in filename:
45 raise UnsupportedPatchFormat( 46 raise UnsupportedPatchFormat(
46 filename, 'Can\'t use \'%s\' in filename.' % i) 47 filename, 'Can\'t use \'%s\' in filename.' % i)
47 for i in ('/', 'CON', 'COM'): 48 for i in ('/', 'CON', 'COM'):
48 if filename.startswith(i): 49 if filename.startswith(i):
49 raise UnsupportedPatchFormat( 50 raise UnsupportedPatchFormat(
50 filename, 'Filename can\'t start with \'%s\'.' % i) 51 filename, 'Filename can\'t start with \'%s\'.' % i)
51 return filename 52 return filename
52 53
53 def get(self): # pragma: no coverage
54 raise NotImplementedError('Nothing to grab')
55
56 def set_relpath(self, relpath): 54 def set_relpath(self, relpath):
57 if not relpath: 55 if not relpath:
58 return 56 return
59 relpath = relpath.replace('\\', '/') 57 relpath = relpath.replace('\\', '/')
60 if relpath[0] == '/': 58 if relpath[0] == '/':
61 self._fail('Relative path starts with %s' % relpath[0]) 59 self._fail('Relative path starts with %s' % relpath[0])
62 self.filename = self._process_filename( 60 self.filename = self._process_filename(
63 posixpath.join(relpath, self.filename)) 61 posixpath.join(relpath, self.filename))
64 if self.source_filename: 62 if self.source_filename:
65 self.source_filename = self._process_filename( 63 self.source_filename = self._process_filename(
66 posixpath.join(relpath, self.source_filename)) 64 posixpath.join(relpath, self.source_filename))
67 65
68 def _fail(self, msg): 66 def _fail(self, msg):
69 """Shortcut function to raise UnsupportedPatchFormat.""" 67 """Shortcut function to raise UnsupportedPatchFormat."""
70 raise UnsupportedPatchFormat(self.filename, msg) 68 raise UnsupportedPatchFormat(self.filename, msg)
71 69
70 def __str__(self):
71 # Use a status-like board.
72 out = ''
73 if self.is_binary:
74 out += 'B'
75 else:
76 out += ' '
77 if self.is_delete:
78 out += 'D'
79 else:
80 out += ' '
81 if self.is_new:
82 out += 'N'
83 else:
84 out += ' '
85 if self.source_filename:
86 out += 'R'
87 else:
88 out += ' '
89 return out + ' %s->%s' % (self.source_filename, self.filename)
90
72 91
73 class FilePatchDelete(FilePatchBase): 92 class FilePatchDelete(FilePatchBase):
74 """Deletes a file.""" 93 """Deletes a file."""
75 is_delete = True 94 is_delete = True
76 95
77 def __init__(self, filename, is_binary): 96 def __init__(self, filename, is_binary):
78 super(FilePatchDelete, self).__init__(filename) 97 super(FilePatchDelete, self).__init__(filename)
79 self.is_binary = is_binary 98 self.is_binary = is_binary
80 99
81 def get(self):
82 raise NotImplementedError('Nothing to grab')
83
84 100
85 class FilePatchBinary(FilePatchBase): 101 class FilePatchBinary(FilePatchBase):
86 """Content of a new binary file.""" 102 """Content of a new binary file."""
87 is_binary = True 103 is_binary = True
88 104
89 def __init__(self, filename, data, svn_properties, is_new): 105 def __init__(self, filename, data, svn_properties, is_new):
90 super(FilePatchBinary, self).__init__(filename) 106 super(FilePatchBinary, self).__init__(filename)
91 self.data = data 107 self.data = data
92 self.svn_properties = svn_properties or [] 108 self.svn_properties = svn_properties or []
93 self.is_new = is_new 109 self.is_new = is_new
(...skipping 10 matching lines...) Expand all
104 if not diff: 120 if not diff:
105 self._fail('File doesn\'t have a diff.') 121 self._fail('File doesn\'t have a diff.')
106 self.diff_header, self.diff_hunks = self._split_header(diff) 122 self.diff_header, self.diff_hunks = self._split_header(diff)
107 self.svn_properties = svn_properties or [] 123 self.svn_properties = svn_properties or []
108 self.is_git_diff = self._is_git_diff_header(self.diff_header) 124 self.is_git_diff = self._is_git_diff_header(self.diff_header)
109 self.patchlevel = 0 125 self.patchlevel = 0
110 if self.is_git_diff: 126 if self.is_git_diff:
111 self._verify_git_header() 127 self._verify_git_header()
112 else: 128 else:
113 self._verify_svn_header() 129 self._verify_svn_header()
130 if self.source_filename and not self.is_new:
131 self._fail('If source_filename is set, is_new must be also be set')
114 132
115 def get(self): 133 def get(self, for_git):
116 return self.diff_header + self.diff_hunks 134 if for_git or not self.source_filename:
135 return self.diff_header + self.diff_hunks
136 else:
137 # patch is stupid. It patches the source_filename instead so get rid of
138 # any source_filename reference if needed.
139 return (
140 self.diff_header.replace(self.source_filename, self.filename) +
141 self.diff_hunks)
117 142
118 def set_relpath(self, relpath): 143 def set_relpath(self, relpath):
119 old_filename = self.filename 144 old_filename = self.filename
120 old_source_filename = self.source_filename or self.filename 145 old_source_filename = self.source_filename or self.filename
121 super(FilePatchDiff, self).set_relpath(relpath) 146 super(FilePatchDiff, self).set_relpath(relpath)
122 # Update the header too. 147 # Update the header too.
123 source_filename = self.source_filename or self.filename 148 source_filename = self.source_filename or self.filename
124 lines = self.diff_header.splitlines(True) 149 lines = self.diff_header.splitlines(True)
125 for i, line in enumerate(lines): 150 for i, line in enumerate(lines):
126 if line.startswith('diff --git'): 151 if line.startswith('diff --git'):
(...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after
257 match = re.match(r'^(rename|copy) to (.+)$', line) 282 match = re.match(r'^(rename|copy) to (.+)$', line)
258 if match: 283 if match:
259 if self.filename != match.group(2): 284 if self.filename != match.group(2):
260 self._fail('Unexpected git diff output name for line %s.' % line) 285 self._fail('Unexpected git diff output name for line %s.' % line)
261 if not last_line.startswith('%s from ' % match.group(1)): 286 if not last_line.startswith('%s from ' % match.group(1)):
262 self._fail( 287 self._fail(
263 'Confused %s from/to git diff for line %s.' % 288 'Confused %s from/to git diff for line %s.' %
264 (match.group(1), line)) 289 (match.group(1), line))
265 return 290 return
266 291
292 # Ignore "deleted file mode 100644" since it's not needed.
267 match = re.match(r'^new(| file) mode (\d{6})$', line) 293 match = re.match(r'^new(| file) mode (\d{6})$', line)
268 if match: 294 if match:
269 mode = match.group(2) 295 mode = match.group(2)
270 # Only look at owner ACL for executable. 296 # Only look at owner ACL for executable.
271 # TODO(maruel): Add support to remove a property. 297 # TODO(maruel): Add support to remove a property.
272 if bool(int(mode[4]) & 1): 298 if bool(int(mode[4]) & 1):
273 self.svn_properties.append(('svn:executable', '*')) 299 self.svn_properties.append(('svn:executable', '*'))
274 300
275 match = re.match(r'^--- (.*)$', line) 301 match = re.match(r'^--- (.*)$', line)
276 if match: 302 if match:
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after
349 if lines: 375 if lines:
350 self._fail('Crap after +++') 376 self._fail('Crap after +++')
351 # We're done. 377 # We're done.
352 return 378 return
353 379
354 380
355 class PatchSet(object): 381 class PatchSet(object):
356 """A list of FilePatch* objects.""" 382 """A list of FilePatch* objects."""
357 383
358 def __init__(self, patches): 384 def __init__(self, patches):
359 self.patches = patches 385 for p in patches:
360 for p in self.patches:
361 assert isinstance(p, FilePatchBase) 386 assert isinstance(p, FilePatchBase)
362 387
388 def key(p):
389 """Sort by ordering of application.
390
391 File move are first.
392 Deletes are last.
393 """
394 if p.source_filename:
395 return (p.is_delete, p.source_filename, p.filename)
396 else:
397 # tuple are always greater than string, abuse that fact.
398 return (p.is_delete, (p.filename,), p.filename)
399
400 self.patches = sorted(patches, key=key)
401
363 def set_relpath(self, relpath): 402 def set_relpath(self, relpath):
364 """Used to offset the patch into a subdirectory.""" 403 """Used to offset the patch into a subdirectory."""
365 for patch in self.patches: 404 for patch in self.patches:
366 patch.set_relpath(relpath) 405 patch.set_relpath(relpath)
367 406
368 def __iter__(self): 407 def __iter__(self):
369 for patch in self.patches: 408 for patch in self.patches:
370 yield patch 409 yield patch
371 410
411 def __getitem__(self, key):
412 return self.patches[key]
413
372 @property 414 @property
373 def filenames(self): 415 def filenames(self):
374 return [p.filename for p in self.patches] 416 return [p.filename for p in self.patches]
OLDNEW
« no previous file with comments | « checkout.py ('k') | tests/checkout_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698