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

Side by Side Diff: third_party/WebKit/Tools/Scripts/webkitpy/common/checkout/scm/git.py

Issue 2594513003: Merge scm.py and git.py in webkitpy. (Closed)
Patch Set: Created 3 years, 12 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
OLDNEW
1 # Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved. 1 # Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved. 2 # Copyright (c) 2009 Apple Inc. All rights reserved.
3 # 3 #
4 # Redistribution and use in source and binary forms, with or without 4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are 5 # modification, are permitted provided that the following conditions are
6 # met: 6 # met:
7 # 7 #
8 # * Redistributions of source code must retain the above copyright 8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer. 9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above 10 # * Redistributions in binary form must reproduce the above
(...skipping 10 matching lines...) Expand all
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 29
30 import datetime 30 import datetime
31 import logging
31 import re 32 import re
32 33
33 from webkitpy.common.checkout.scm.scm import SCM
34 from webkitpy.common.memoized import memoized 34 from webkitpy.common.memoized import memoized
35 from webkitpy.common.system.executive import Executive, ScriptError 35 from webkitpy.common.system.executive import Executive, ScriptError
36 from webkitpy.common.system.filesystem import FileSystem
37
38 _log = logging.getLogger(__name__)
36 39
37 40
38 class AmbiguousCommitError(Exception): 41 class Git(object):
39 42 # Unless otherwise specified, methods are expected to return paths relative
40 def __init__(self, num_local_commits, has_working_directory_changes): 43 # to self.checkout_root.
41 Exception.__init__(self, "Found %s local commits and the working directo ry is %s" % (
42 num_local_commits, ["clean", "not clean"][has_working_directory_chan ges]))
43 self.num_local_commits = num_local_commits
44 self.has_working_directory_changes = has_working_directory_changes
45
46
47 class Git(SCM):
48 44
49 # Git doesn't appear to document error codes, but seems to return 45 # Git doesn't appear to document error codes, but seems to return
50 # 1 or 128, mostly. 46 # 1 or 128, mostly.
51 ERROR_FILE_IS_MISSING = 128 47 ERROR_FILE_IS_MISSING = 128
52 48
53 executable_name = 'git' 49 executable_name = 'git'
54 50
55 def __init__(self, cwd, **kwargs): 51 def __init__(self, cwd, executive=None, filesystem=None):
56 SCM.__init__(self, cwd, **kwargs) 52 self.cwd = cwd
53 self._executive = executive or Executive()
54 self._filesystem = filesystem or FileSystem()
55 self.checkout_root = self.find_checkout_root(self.cwd)
57 56
58 def _run_git(self, command_args, **kwargs): 57 def _run_git(self,
58 command_args,
59 cwd=None,
60 input=None, # pylint: disable=redefined-builtin
61 timeout_seconds=None,
62 decode_output=True,
63 return_exit_code=False):
59 full_command_args = [self.executable_name] + command_args 64 full_command_args = [self.executable_name] + command_args
60 full_kwargs = kwargs 65 cwd = cwd or self.checkout_root
61 if 'cwd' not in full_kwargs: 66 return self._executive.run_command(
62 full_kwargs['cwd'] = self.checkout_root 67 full_command_args,
63 return self._run(full_command_args, **full_kwargs) 68 cwd=cwd,
69 input=input,
70 timeout_seconds=timeout_seconds,
71 return_exit_code=return_exit_code,
72 decode_output=decode_output)
73
74 # SCM always returns repository relative path, but sometimes we need
75 # absolute paths to pass to rm, etc.
76 def absolute_path(self, repository_relative_path):
77 return self._filesystem.join(self.checkout_root, repository_relative_pat h)
64 78
65 @classmethod 79 @classmethod
66 def in_working_directory(cls, path, executive=None): 80 def in_working_directory(cls, path, executive=None):
67 try: 81 try:
68 executive = executive or Executive() 82 executive = executive or Executive()
69 return executive.run_command([cls.executable_name, 'rev-parse', '--i s-inside-work-tree'], 83 return executive.run_command([cls.executable_name, 'rev-parse', '--i s-inside-work-tree'],
70 cwd=path, error_handler=Executive.ignor e_error).rstrip() == "true" 84 cwd=path, error_handler=Executive.ignor e_error).rstrip() == "true"
71 except OSError: 85 except OSError:
72 # The Windows bots seem to through a WindowsError when git isn't ins talled. 86 # The Windows bots seem to through a WindowsError when git isn't ins talled.
73 return False 87 return False
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
105 command.extend(['--', pathspec]) 119 command.extend(['--', pathspec])
106 return self._run_git(command) != '' 120 return self._run_git(command) != ''
107 121
108 def _discard_working_directory_changes(self): 122 def _discard_working_directory_changes(self):
109 # Could run git clean here too, but that wouldn't match subversion 123 # Could run git clean here too, but that wouldn't match subversion
110 self._run_git(['reset', 'HEAD', '--hard']) 124 self._run_git(['reset', 'HEAD', '--hard'])
111 # Aborting rebase even though this does not match subversion 125 # Aborting rebase even though this does not match subversion
112 if self._rebase_in_progress(): 126 if self._rebase_in_progress():
113 self._run_git(['rebase', '--abort']) 127 self._run_git(['rebase', '--abort'])
114 128
115 def status_command(self):
116 # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
117 # No file contents printed, thus utf-8 autodecoding in self.run is fine.
118 return [self.executable_name, "diff", "--name-status", "--no-renames", " HEAD"]
119
120 def _status_regexp(self, expected_types):
121 return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types
122
123 def add_all(self, pathspec=None): 129 def add_all(self, pathspec=None):
124 command = ['add', '--all'] 130 command = ['add', '--all']
125 if pathspec: 131 if pathspec:
126 command.append(pathspec) 132 command.append(pathspec)
127 return self._run_git(command) 133 return self._run_git(command)
128 134
129 def add_list(self, paths, return_exit_code=False, recurse=True): 135 def add_list(self, paths, return_exit_code=False):
130 return self._run_git(["add"] + paths, return_exit_code=return_exit_code) 136 return self._run_git(["add"] + paths, return_exit_code=return_exit_code)
131 137
132 def delete_list(self, paths): 138 def delete_list(self, paths):
133 return self._run_git(["rm", "-f"] + paths) 139 return self._run_git(["rm", "-f"] + paths)
134 140
135 def move(self, origin, destination): 141 def move(self, origin, destination):
136 return self._run_git(["mv", "-f", origin, destination]) 142 return self._run_git(["mv", "-f", origin, destination])
137 143
138 def exists(self, path): 144 def exists(self, path):
139 return_code = self._run_git(["show", "HEAD:%s" % path], return_exit_code =True, decode_output=False) 145 return_code = self._run_git(["show", "HEAD:%s" % path], return_exit_code =True, decode_output=False)
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
183 return self._remote_merge_base() 189 return self._remote_merge_base()
184 190
185 def changed_files(self, git_commit=None): 191 def changed_files(self, git_commit=None):
186 # FIXME: --diff-filter could be used to avoid the "extract_filenames" st ep. 192 # FIXME: --diff-filter could be used to avoid the "extract_filenames" st ep.
187 status_command = [self.executable_name, 'diff', '-r', '--name-status', 193 status_command = [self.executable_name, 'diff', '-r', '--name-status',
188 "--no-renames", "--no-ext-diff", "--full-index", self. _merge_base(git_commit)] 194 "--no-renames", "--no-ext-diff", "--full-index", self. _merge_base(git_commit)]
189 # FIXME: I'm not sure we're returning the same set of files that SVN.cha nged_files is. 195 # FIXME: I'm not sure we're returning the same set of files that SVN.cha nged_files is.
190 # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R) 196 # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
191 return self._run_status_and_extract_filenames(status_command, self._stat us_regexp("ADM")) 197 return self._run_status_and_extract_filenames(status_command, self._stat us_regexp("ADM"))
192 198
193 def _added_files(self): 199 def added_files(self):
194 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("A")) 200 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("A"))
195 201
196 def _deleted_files(self): 202 def _run_status_and_extract_filenames(self, status_command, status_regexp):
197 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("D")) 203 filenames = []
204 # We run with cwd=self.checkout_root so that returned-paths are root-rel ative.
205 for line in self._run_git(status_command, cwd=self.checkout_root).splitl ines():
206 match = re.search(status_regexp, line)
207 if not match:
208 continue
209 # status = match.group('status')
210 filename = match.group('filename')
211 filenames.append(filename)
212 return filenames
213
214 def status_command(self):
215 # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
216 # No file contents printed, thus utf-8 autodecoding in self.run is fine.
217 return ["diff", "--name-status", "--no-renames", "HEAD"]
218
219 def _status_regexp(self, expected_types):
220 return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types
198 221
199 @staticmethod 222 @staticmethod
200 def supports_local_commits(): 223 def supports_local_commits():
201 return True 224 return True
202 225
203 def display_name(self): 226 def display_name(self):
204 return "git" 227 return "git"
205 228
206 def most_recent_log_matching(self, grep_str, path): 229 def most_recent_log_matching(self, grep_str, path):
207 # We use '--grep=' + foo rather than '--grep', foo because 230 # We use '--grep=' + foo rather than '--grep', foo because
208 # git 1.7.0.4 (and earlier) didn't support the separate arg. 231 # git 1.7.0.4 (and earlier) didn't support the separate arg.
209 return self._run_git(['log', '-1', '--grep=' + grep_str, '--date=iso', s elf.find_checkout_root(path)]) 232 return self._run_git(['log', '-1', '--grep=' + grep_str, '--date=iso', s elf.find_checkout_root(path)])
210 233
211 def _commit_position_from_git_log(self, git_log): 234 def _commit_position_from_git_log(self, git_log):
212 match = re.search(r"^\s*Cr-Commit-Position:.*@\{#(?P<commit_position>\d+ )\}", git_log, re.MULTILINE) 235 match = re.search(r"^\s*Cr-Commit-Position:.*@\{#(?P<commit_position>\d+ )\}", git_log, re.MULTILINE)
213 if not match: 236 if not match:
214 return "" 237 return ""
215 return int(match.group('commit_position')) 238 return int(match.group('commit_position'))
216 239
217 def commit_position(self, path): 240 def commit_position(self, path):
241 """Returns the latest chromium commit position found in the checkout."""
218 git_log = self.most_recent_log_matching('Cr-Commit-Position:', path) 242 git_log = self.most_recent_log_matching('Cr-Commit-Position:', path)
219 return self._commit_position_from_git_log(git_log) 243 return self._commit_position_from_git_log(git_log)
220 244
221 def _commit_position_regex_for_timestamp(self): 245 def _commit_position_regex_for_timestamp(self):
222 return 'Cr-Commit-Position:.*@{#%s}' 246 return 'Cr-Commit-Position:.*@{#%s}'
223 247
224 def timestamp_of_revision(self, path, revision): 248 def timestamp_of_revision(self, path, revision):
225 git_log = self.most_recent_log_matching(self._commit_position_regex_for_ timestamp() % revision, path) 249 git_log = self.most_recent_log_matching(self._commit_position_regex_for_ timestamp() % revision, path)
226 match = re.search(r"^Date:\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d {2}) ([+-])(\d{2})(\d{2})$", git_log, re.MULTILINE) 250 match = re.search(r"^Date:\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d {2}) ([+-])(\d{2})(\d{2})$", git_log, re.MULTILINE)
227 if not match: 251 if not match:
228 return "" 252 return ""
229 253
230 # Manually modify the timezone since Git doesn't have an option to show it in UTC. 254 # Manually modify the timezone since Git doesn't have an option to show it in UTC.
231 # Git also truncates milliseconds but we're going to ignore that for now . 255 # Git also truncates milliseconds but we're going to ignore that for now .
232 time_with_timezone = datetime.datetime(int(match.group(1)), int(match.gr oup(2)), int(match.group(3)), 256 time_with_timezone = datetime.datetime(int(match.group(1)), int(match.gr oup(2)), int(match.group(3)),
233 int(match.group(4)), int(match.gr oup(5)), int(match.group(6)), 0) 257 int(match.group(4)), int(match.gr oup(5)), int(match.group(6)), 0)
234 258
235 sign = 1 if match.group(7) == '+' else -1 259 sign = 1 if match.group(7) == '+' else -1
236 time_without_timezone = time_with_timezone - \ 260 time_without_timezone = time_with_timezone - \
237 datetime.timedelta(hours=sign * int(match.group(8)), minutes=int(mat ch.group(9))) 261 datetime.timedelta(hours=sign * int(match.group(8)), minutes=int(mat ch.group(9)))
238 return time_without_timezone.strftime('%Y-%m-%dT%H:%M:%SZ') 262 return time_without_timezone.strftime('%Y-%m-%dT%H:%M:%SZ')
239 263
240 def create_patch(self, git_commit=None, changed_files=None): 264 def create_patch(self, git_commit=None, changed_files=None):
241 """Returns a byte array (str()) representing the patch file. 265 """Returns a byte array (str()) representing the patch file.
242 Patch files are effectively binary since they may contain 266 Patch files are effectively binary since they may contain
243 files of multiple different encodings. 267 files of multiple different encodings.
244 """ 268 """
269 order = self._patch_order()
270 command = [
271 'diff',
272 '--binary',
273 '--no-color',
274 "--no-ext-diff",
275 "--full-index",
276 "--no-renames",
277 "--src-prefix=a/",
278 "--dst-prefix=b/",
245 279
280 ]
281 if order:
282 command.append(order)
283 command += [self._merge_base(git_commit), "--"]
284 if changed_files:
285 command += changed_files
286 return self._run_git(command, decode_output=False, cwd=self.checkout_roo t)
287
288 def _patch_order(self):
246 # Put code changes at the top of the patch and layout tests 289 # Put code changes at the top of the patch and layout tests
247 # at the bottom, this makes for easier reviewing. 290 # at the bottom, this makes for easier reviewing.
248 config_path = self._filesystem.dirname(self._filesystem.path_to_module(' webkitpy.common.config')) 291 config_path = self._filesystem.dirname(self._filesystem.path_to_module(' webkitpy.common.config'))
249 order_file = self._filesystem.join(config_path, 'orderfile') 292 order_file = self._filesystem.join(config_path, 'orderfile')
250 order = ""
251 if self._filesystem.exists(order_file): 293 if self._filesystem.exists(order_file):
252 order = "-O%s" % order_file 294 return "-O%s" % order_file
253 295 return ""
254 command = [self.executable_name, 'diff', '--binary', '--no-color', "--no -ext-diff",
255 "--full-index", "--no-renames", "--src-prefix=a/", "--dst-pre fix=b/",
256 order, self._merge_base(git_commit), "--"]
257 if changed_files:
258 command += changed_files
259 return self._run(command, decode_output=False, cwd=self.checkout_root)
260 296
261 @memoized 297 @memoized
262 def commit_position_from_git_commit(self, git_commit): 298 def commit_position_from_git_commit(self, git_commit):
263 git_log = self.git_commit_detail(git_commit) 299 git_log = self.git_commit_detail(git_commit)
264 return self._commit_position_from_git_log(git_log) 300 return self._commit_position_from_git_log(git_log)
265 301
266 def checkout_branch(self, name): 302 def checkout_branch(self, name):
267 self._run_git(['checkout', '-q', name]) 303 self._run_git(['checkout', '-q', name])
268 304
269 def create_clean_branch(self, name): 305 def create_clean_branch(self, name):
(...skipping 17 matching lines...) Expand all
287 # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists. 323 # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists.
288 remote_master_ref = 'refs/remotes/origin/master' 324 remote_master_ref = 'refs/remotes/origin/master'
289 if not self._branch_ref_exists(remote_master_ref): 325 if not self._branch_ref_exists(remote_master_ref):
290 raise ScriptError(message="Can't find a branch to diff against. %s d oes not exist" % remote_master_ref) 326 raise ScriptError(message="Can't find a branch to diff against. %s d oes not exist" % remote_master_ref)
291 return remote_master_ref 327 return remote_master_ref
292 328
293 def commit_locally_with_message(self, message): 329 def commit_locally_with_message(self, message):
294 command = ['commit', '--all', '-F', '-'] 330 command = ['commit', '--all', '-F', '-']
295 self._run_git(command, input=message) 331 self._run_git(command, input=message)
296 332
297 # These methods are git specific and are meant to provide support for the Gi t oriented workflow
298 # that Blink is moving towards, hence there are no equivalent methods in the SVN class.
299
300 def pull(self, timeout_seconds=None): 333 def pull(self, timeout_seconds=None):
301 self._run_git(['pull'], timeout_seconds=timeout_seconds) 334 self._run_git(['pull'], timeout_seconds=timeout_seconds)
302 335
303 def latest_git_commit(self): 336 def latest_git_commit(self):
304 return self._run_git(['log', '-1', '--format=%H']).strip() 337 return self._run_git(['log', '-1', '--format=%H']).strip()
305 338
306 def git_commits_since(self, commit): 339 def git_commits_since(self, commit):
307 return self._run_git(['log', commit + '..master', '--format=%H', '--reve rse']).split() 340 return self._run_git(['log', commit + '..master', '--format=%H', '--reve rse']).split()
308 341
309 def git_commit_detail(self, commit, format=None): 342 def git_commit_detail(self, commit, format=None): # pylint: disable=redefin ed-builtin
310 args = ['log', '-1', commit] 343 args = ['log', '-1', commit]
311 if format: 344 if format:
312 args.append('--format=' + format) 345 args.append('--format=' + format)
313 return self._run_git(args) 346 return self._run_git(args)
314 347
315 def affected_files(self, commit): 348 def affected_files(self, commit):
316 output = self._run_git(['log', '-1', '--format=', '--name-only', commit] ) 349 output = self._run_git(['log', '-1', '--format=', '--name-only', commit] )
317 return output.strip().split('\n') 350 return output.strip().split('\n')
318 351
319 def _branch_tracking_remote_master(self): 352 def _branch_tracking_remote_master(self):
(...skipping 10 matching lines...) Expand all
330 if self.current_branch() != self._branch_tracking_remote_master(): 363 if self.current_branch() != self._branch_tracking_remote_master():
331 return False 364 return False
332 if len(self._local_commits(self._branch_tracking_remote_master())) > 0: 365 if len(self._local_commits(self._branch_tracking_remote_master())) > 0:
333 return False 366 return False
334 return True 367 return True
335 368
336 def ensure_cleanly_tracking_remote_master(self): 369 def ensure_cleanly_tracking_remote_master(self):
337 self._discard_working_directory_changes() 370 self._discard_working_directory_changes()
338 self._run_git(['checkout', '-q', self._branch_tracking_remote_master()]) 371 self._run_git(['checkout', '-q', self._branch_tracking_remote_master()])
339 self._discard_local_commits() 372 self._discard_local_commits()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698