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

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: Rebased Created 3 years, 11 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
« no previous file with comments | « no previous file | third_party/WebKit/Tools/Scripts/webkitpy/common/checkout/scm/git_mock.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 # 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 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
123 # for machine parsing. Lines are terminated with NUL rather than LF. 137 # for machine parsing. Lines are terminated with NUL rather than LF.
124 unstaged_changes = {} 138 unstaged_changes = {}
125 change_lines = self._run_git(['status', '-z']).rstrip('\x00').split('\x0 0') 139 change_lines = self._run_git(['status', '-z']).rstrip('\x00').split('\x0 0')
126 for line in change_lines: 140 for line in change_lines:
127 if line[1] == ' ': 141 if line[1] == ' ':
128 continue # Already staged for commit. 142 continue # Already staged for commit.
129 path = line[3:] 143 path = line[3:]
130 unstaged_changes[path] = line[1] 144 unstaged_changes[path] = line[1]
131 return unstaged_changes 145 return unstaged_changes
132 146
133 def status_command(self):
134 # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
135 # No file contents printed, thus utf-8 autodecoding in self.run is fine.
136 return [self.executable_name, "diff", "--name-status", "--no-renames", " HEAD"]
137
138 def _status_regexp(self, expected_types):
139 return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types
140
141 def add_all(self, pathspec=None): 147 def add_all(self, pathspec=None):
142 command = ['add', '--all'] 148 command = ['add', '--all']
143 if pathspec: 149 if pathspec:
144 command.append(pathspec) 150 command.append(pathspec)
145 return self._run_git(command) 151 return self._run_git(command)
146 152
147 def add_list(self, paths, return_exit_code=False, recurse=True): 153 def add_list(self, paths, return_exit_code=False):
148 return self._run_git(["add"] + paths, return_exit_code=return_exit_code) 154 return self._run_git(["add"] + paths, return_exit_code=return_exit_code)
149 155
150 def delete_list(self, paths): 156 def delete_list(self, paths):
151 return self._run_git(["rm", "-f"] + paths) 157 return self._run_git(["rm", "-f"] + paths)
152 158
153 def move(self, origin, destination): 159 def move(self, origin, destination):
154 return self._run_git(["mv", "-f", origin, destination]) 160 return self._run_git(["mv", "-f", origin, destination])
155 161
156 def exists(self, path): 162 def exists(self, path):
157 return_code = self._run_git(["show", "HEAD:%s" % path], return_exit_code =True, decode_output=False) 163 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
201 return self._remote_merge_base() 207 return self._remote_merge_base()
202 208
203 def changed_files(self, git_commit=None): 209 def changed_files(self, git_commit=None):
204 # FIXME: --diff-filter could be used to avoid the "extract_filenames" st ep. 210 # FIXME: --diff-filter could be used to avoid the "extract_filenames" st ep.
205 status_command = [self.executable_name, 'diff', '-r', '--name-status', 211 status_command = [self.executable_name, 'diff', '-r', '--name-status',
206 "--no-renames", "--no-ext-diff", "--full-index", self. _merge_base(git_commit)] 212 "--no-renames", "--no-ext-diff", "--full-index", self. _merge_base(git_commit)]
207 # FIXME: I'm not sure we're returning the same set of files that SVN.cha nged_files is. 213 # FIXME: I'm not sure we're returning the same set of files that SVN.cha nged_files is.
208 # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R) 214 # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
209 return self._run_status_and_extract_filenames(status_command, self._stat us_regexp("ADM")) 215 return self._run_status_and_extract_filenames(status_command, self._stat us_regexp("ADM"))
210 216
211 def _added_files(self): 217 def added_files(self):
212 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("A")) 218 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("A"))
213 219
214 def _deleted_files(self): 220 def _run_status_and_extract_filenames(self, status_command, status_regexp):
215 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("D")) 221 filenames = []
222 # We run with cwd=self.checkout_root so that returned-paths are root-rel ative.
223 for line in self._run_git(status_command, cwd=self.checkout_root).splitl ines():
224 match = re.search(status_regexp, line)
225 if not match:
226 continue
227 # status = match.group('status')
228 filename = match.group('filename')
229 filenames.append(filename)
230 return filenames
231
232 def status_command(self):
233 # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
234 # No file contents printed, thus utf-8 autodecoding in self.run is fine.
235 return ["diff", "--name-status", "--no-renames", "HEAD"]
236
237 def _status_regexp(self, expected_types):
238 return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types
216 239
217 @staticmethod 240 @staticmethod
218 def supports_local_commits(): 241 def supports_local_commits():
219 return True 242 return True
220 243
221 def display_name(self): 244 def display_name(self):
222 return "git" 245 return "git"
223 246
224 def most_recent_log_matching(self, grep_str, path): 247 def most_recent_log_matching(self, grep_str, path):
225 # We use '--grep=' + foo rather than '--grep', foo because 248 # We use '--grep=' + foo rather than '--grep', foo because
226 # git 1.7.0.4 (and earlier) didn't support the separate arg. 249 # git 1.7.0.4 (and earlier) didn't support the separate arg.
227 return self._run_git(['log', '-1', '--grep=' + grep_str, '--date=iso', s elf.find_checkout_root(path)]) 250 return self._run_git(['log', '-1', '--grep=' + grep_str, '--date=iso', s elf.find_checkout_root(path)])
228 251
229 def _commit_position_from_git_log(self, git_log): 252 def _commit_position_from_git_log(self, git_log):
230 match = re.search(r"^\s*Cr-Commit-Position:.*@\{#(?P<commit_position>\d+ )\}", git_log, re.MULTILINE) 253 match = re.search(r"^\s*Cr-Commit-Position:.*@\{#(?P<commit_position>\d+ )\}", git_log, re.MULTILINE)
231 if not match: 254 if not match:
232 return "" 255 return ""
233 return int(match.group('commit_position')) 256 return int(match.group('commit_position'))
234 257
235 def commit_position(self, path): 258 def commit_position(self, path):
259 """Returns the latest chromium commit position found in the checkout."""
236 git_log = self.most_recent_log_matching('Cr-Commit-Position:', path) 260 git_log = self.most_recent_log_matching('Cr-Commit-Position:', path)
237 return self._commit_position_from_git_log(git_log) 261 return self._commit_position_from_git_log(git_log)
238 262
239 def _commit_position_regex_for_timestamp(self): 263 def _commit_position_regex_for_timestamp(self):
240 return 'Cr-Commit-Position:.*@{#%s}' 264 return 'Cr-Commit-Position:.*@{#%s}'
241 265
242 def timestamp_of_revision(self, path, revision): 266 def timestamp_of_revision(self, path, revision):
243 git_log = self.most_recent_log_matching(self._commit_position_regex_for_ timestamp() % revision, path) 267 git_log = self.most_recent_log_matching(self._commit_position_regex_for_ timestamp() % revision, path)
244 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) 268 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)
245 if not match: 269 if not match:
246 return "" 270 return ""
247 271
248 # Manually modify the timezone since Git doesn't have an option to show it in UTC. 272 # Manually modify the timezone since Git doesn't have an option to show it in UTC.
249 # Git also truncates milliseconds but we're going to ignore that for now . 273 # Git also truncates milliseconds but we're going to ignore that for now .
250 time_with_timezone = datetime.datetime(int(match.group(1)), int(match.gr oup(2)), int(match.group(3)), 274 time_with_timezone = datetime.datetime(int(match.group(1)), int(match.gr oup(2)), int(match.group(3)),
251 int(match.group(4)), int(match.gr oup(5)), int(match.group(6)), 0) 275 int(match.group(4)), int(match.gr oup(5)), int(match.group(6)), 0)
252 276
253 sign = 1 if match.group(7) == '+' else -1 277 sign = 1 if match.group(7) == '+' else -1
254 time_without_timezone = time_with_timezone - \ 278 time_without_timezone = time_with_timezone - \
255 datetime.timedelta(hours=sign * int(match.group(8)), minutes=int(mat ch.group(9))) 279 datetime.timedelta(hours=sign * int(match.group(8)), minutes=int(mat ch.group(9)))
256 return time_without_timezone.strftime('%Y-%m-%dT%H:%M:%SZ') 280 return time_without_timezone.strftime('%Y-%m-%dT%H:%M:%SZ')
257 281
258 def create_patch(self, git_commit=None, changed_files=None): 282 def create_patch(self, git_commit=None, changed_files=None):
259 """Returns a byte array (str()) representing the patch file. 283 """Returns a byte array (str()) representing the patch file.
260 Patch files are effectively binary since they may contain 284 Patch files are effectively binary since they may contain
261 files of multiple different encodings. 285 files of multiple different encodings.
262 """ 286 """
287 order = self._patch_order()
288 command = [
289 'diff',
290 '--binary',
291 '--no-color',
292 "--no-ext-diff",
293 "--full-index",
294 "--no-renames",
295 "--src-prefix=a/",
296 "--dst-prefix=b/",
263 297
298 ]
299 if order:
300 command.append(order)
301 command += [self._merge_base(git_commit), "--"]
302 if changed_files:
303 command += changed_files
304 return self._run_git(command, decode_output=False, cwd=self.checkout_roo t)
305
306 def _patch_order(self):
264 # Put code changes at the top of the patch and layout tests 307 # Put code changes at the top of the patch and layout tests
265 # at the bottom, this makes for easier reviewing. 308 # at the bottom, this makes for easier reviewing.
266 config_path = self._filesystem.dirname(self._filesystem.path_to_module(' webkitpy.common.config')) 309 config_path = self._filesystem.dirname(self._filesystem.path_to_module(' webkitpy.common.config'))
267 order_file = self._filesystem.join(config_path, 'orderfile') 310 order_file = self._filesystem.join(config_path, 'orderfile')
268 order = ""
269 if self._filesystem.exists(order_file): 311 if self._filesystem.exists(order_file):
270 order = "-O%s" % order_file 312 return "-O%s" % order_file
271 313 return ""
272 command = [self.executable_name, 'diff', '--binary', '--no-color', "--no -ext-diff",
273 "--full-index", "--no-renames", "--src-prefix=a/", "--dst-pre fix=b/",
274 order, self._merge_base(git_commit), "--"]
275 if changed_files:
276 command += changed_files
277 return self._run(command, decode_output=False, cwd=self.checkout_root)
278 314
279 @memoized 315 @memoized
280 def commit_position_from_git_commit(self, git_commit): 316 def commit_position_from_git_commit(self, git_commit):
281 git_log = self.git_commit_detail(git_commit) 317 git_log = self.git_commit_detail(git_commit)
282 return self._commit_position_from_git_log(git_log) 318 return self._commit_position_from_git_log(git_log)
283 319
284 def checkout_branch(self, name): 320 def checkout_branch(self, name):
285 self._run_git(['checkout', '-q', name]) 321 self._run_git(['checkout', '-q', name])
286 322
287 def create_clean_branch(self, name): 323 def create_clean_branch(self, name):
(...skipping 17 matching lines...) Expand all
305 # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists. 341 # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists.
306 remote_master_ref = 'refs/remotes/origin/master' 342 remote_master_ref = 'refs/remotes/origin/master'
307 if not self._branch_ref_exists(remote_master_ref): 343 if not self._branch_ref_exists(remote_master_ref):
308 raise ScriptError(message="Can't find a branch to diff against. %s d oes not exist" % remote_master_ref) 344 raise ScriptError(message="Can't find a branch to diff against. %s d oes not exist" % remote_master_ref)
309 return remote_master_ref 345 return remote_master_ref
310 346
311 def commit_locally_with_message(self, message): 347 def commit_locally_with_message(self, message):
312 command = ['commit', '--all', '-F', '-'] 348 command = ['commit', '--all', '-F', '-']
313 self._run_git(command, input=message) 349 self._run_git(command, input=message)
314 350
315 # These methods are git specific and are meant to provide support for the Gi t oriented workflow
316 # that Blink is moving towards, hence there are no equivalent methods in the SVN class.
317
318 def pull(self, timeout_seconds=None): 351 def pull(self, timeout_seconds=None):
319 self._run_git(['pull'], timeout_seconds=timeout_seconds) 352 self._run_git(['pull'], timeout_seconds=timeout_seconds)
320 353
321 def latest_git_commit(self): 354 def latest_git_commit(self):
322 return self._run_git(['log', '-1', '--format=%H']).strip() 355 return self._run_git(['log', '-1', '--format=%H']).strip()
323 356
324 def git_commits_since(self, commit): 357 def git_commits_since(self, commit):
325 return self._run_git(['log', commit + '..master', '--format=%H', '--reve rse']).split() 358 return self._run_git(['log', commit + '..master', '--format=%H', '--reve rse']).split()
326 359
327 def git_commit_detail(self, commit, format=None): 360 def git_commit_detail(self, commit, format=None): # pylint: disable=redefin ed-builtin
328 args = ['log', '-1', commit] 361 args = ['log', '-1', commit]
329 if format: 362 if format:
330 args.append('--format=' + format) 363 args.append('--format=' + format)
331 return self._run_git(args) 364 return self._run_git(args)
332 365
333 def affected_files(self, commit): 366 def affected_files(self, commit):
334 output = self._run_git(['log', '-1', '--format=', '--name-only', commit] ) 367 output = self._run_git(['log', '-1', '--format=', '--name-only', commit] )
335 return output.strip().split('\n') 368 return output.strip().split('\n')
336 369
337 def _branch_tracking_remote_master(self): 370 def _branch_tracking_remote_master(self):
(...skipping 10 matching lines...) Expand all
348 if self.current_branch() != self._branch_tracking_remote_master(): 381 if self.current_branch() != self._branch_tracking_remote_master():
349 return False 382 return False
350 if len(self._local_commits(self._branch_tracking_remote_master())) > 0: 383 if len(self._local_commits(self._branch_tracking_remote_master())) > 0:
351 return False 384 return False
352 return True 385 return True
353 386
354 def ensure_cleanly_tracking_remote_master(self): 387 def ensure_cleanly_tracking_remote_master(self):
355 self._discard_working_directory_changes() 388 self._discard_working_directory_changes()
356 self._run_git(['checkout', '-q', self._branch_tracking_remote_master()]) 389 self._run_git(['checkout', '-q', self._branch_tracking_remote_master()])
357 self._discard_local_commits() 390 self._discard_local_commits()
OLDNEW
« no previous file with comments | « no previous file | third_party/WebKit/Tools/Scripts/webkitpy/common/checkout/scm/git_mock.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698