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

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

Issue 2680173002: Move webkitpy/common/checkout/scm/* -> webkitpy/common/checkout/. (Closed)
Patch Set: Created 3 years, 10 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
(Empty)
1 # Copyright (c) 2009, 2010, 2011 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
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.
29
30 import datetime
31 import logging
32 import re
33
34 from webkitpy.common.memoized import memoized
35 from webkitpy.common.system.executive import Executive, ScriptError
36 from webkitpy.common.system.filesystem import FileSystem
37
38 _log = logging.getLogger(__name__)
39
40
41 class Git(object):
42 # Unless otherwise specified, methods are expected to return paths relative
43 # to self.checkout_root.
44
45 # Git doesn't appear to document error codes, but seems to return
46 # 1 or 128, mostly.
47 ERROR_FILE_IS_MISSING = 128
48
49 executable_name = 'git'
50
51 def __init__(self, cwd=None, executive=None, filesystem=None):
52 self._executive = executive or Executive()
53 self._filesystem = filesystem or FileSystem()
54
55 self.cwd = cwd or self._filesystem.abspath(self._filesystem.getcwd())
56 if not Git.in_working_directory(self.cwd, executive=self._executive):
57 module_directory = self._filesystem.abspath(
58 self._filesystem.dirname(self._filesystem.path_to_module(self.__ module__)))
59 _log.info('The current directory (%s) is not in a git repo, trying d irectory %s.',
60 cwd, module_directory)
61 if Git.in_working_directory(module_directory, executive=self._execut ive):
62 self.cwd = module_directory
63 _log.error('Failed to find Git repo for %s or %s', cwd, module_direc tory)
64
65 self._init_executable_name()
66 self.checkout_root = self.find_checkout_root(self.cwd)
67
68 def _init_executable_name(self):
69 # FIXME: This is a hack and should be removed.
70 try:
71 self._executive.run_command(['git', 'help'])
72 except OSError:
73 try:
74 self._executive.run_command(['git.bat', 'help'])
75 # The Win port uses the depot_tools package, which contains a nu mber
76 # of development tools, including Python and git. Instead of usi ng a
77 # real git executable, depot_tools indirects via a batch file, c alled
78 # "git.bat". This batch file allows depot_tools to auto-update t he real
79 # git executable, which is contained in a subdirectory.
80 _log.debug('Engaging git.bat Windows hack.')
81 self.executable_name = 'git.bat'
82 except OSError:
83 _log.debug('Failed to engage git.bat Windows hack.')
84
85 def _run_git(self,
86 command_args,
87 cwd=None,
88 input=None, # pylint: disable=redefined-builtin
89 timeout_seconds=None,
90 decode_output=True,
91 return_exit_code=False):
92 full_command_args = [self.executable_name] + command_args
93 cwd = cwd or self.checkout_root
94 return self._executive.run_command(
95 full_command_args,
96 cwd=cwd,
97 input=input,
98 timeout_seconds=timeout_seconds,
99 return_exit_code=return_exit_code,
100 decode_output=decode_output)
101
102 # SCM always returns repository relative path, but sometimes we need
103 # absolute paths to pass to rm, etc.
104 def absolute_path(self, repository_relative_path):
105 return self._filesystem.join(self.checkout_root, repository_relative_pat h)
106
107 @classmethod
108 def in_working_directory(cls, path, executive=None):
109 try:
110 executive = executive or Executive()
111 return executive.run_command([cls.executable_name, 'rev-parse', '--i s-inside-work-tree'],
112 cwd=path, error_handler=Executive.ignor e_error).rstrip() == "true"
113 except OSError:
114 # The Windows bots seem to through a WindowsError when git isn't ins talled.
115 return False
116
117 def find_checkout_root(self, path):
118 # "git rev-parse --show-cdup" would be another way to get to the root
119 checkout_root = self._run_git(['rev-parse', '--show-toplevel'], cwd=(pat h or "./")).strip()
120 if not self._filesystem.isabs(checkout_root): # Sometimes git returns r elative paths
121 checkout_root = self._filesystem.join(path, checkout_root)
122 return checkout_root
123
124 @classmethod
125 def read_git_config(cls, key, cwd=None, executive=None):
126 # FIXME: This should probably use cwd=self.checkout_root.
127 # Pass --get-all for cases where the config has multiple values
128 # Pass the cwd if provided so that we can handle the case of running web kit-patch outside of the working directory.
129 # FIXME: This should use an Executive.
130 executive = executive or Executive()
131 return executive.run_command(
132 [cls.executable_name, "config", "--get-all", key], error_handler=Exe cutive.ignore_error, cwd=cwd).rstrip('\n')
133
134 def _discard_local_commits(self):
135 self._run_git(['reset', '--hard', self._remote_branch_ref()])
136
137 def _local_commits(self, ref='HEAD'):
138 return self._run_git(['log', '--pretty=oneline', ref + '...' + self._rem ote_branch_ref()]).splitlines()
139
140 def _rebase_in_progress(self):
141 return self._filesystem.exists(self.absolute_path(self._filesystem.join( '.git', 'rebase-apply')))
142
143 def has_working_directory_changes(self, pathspec=None):
144 """Checks whether there are uncommitted changes."""
145 command = ['diff', 'HEAD', '--no-renames', '--name-only']
146 if pathspec:
147 command.extend(['--', pathspec])
148 return self._run_git(command) != ''
149
150 def _discard_working_directory_changes(self):
151 # Could run git clean here too, but that wouldn't match subversion
152 self._run_git(['reset', 'HEAD', '--hard'])
153 # Aborting rebase even though this does not match subversion
154 if self._rebase_in_progress():
155 self._run_git(['rebase', '--abort'])
156
157 def unstaged_changes(self):
158 """Lists files with unstaged changes, including untracked files.
159
160 Returns a dict mapping modified file paths (relative to checkout root)
161 to one-character codes identifying the change, e.g. 'M' for modified,
162 'D' for deleted, '?' for untracked.
163 """
164 # `git status -z` is a version of `git status -s`, that's recommended
165 # for machine parsing. Lines are terminated with NUL rather than LF.
166 change_lines = self._run_git(['status', '-z']).rstrip('\x00')
167 if not change_lines:
168 return {} # No changes.
169 unstaged_changes = {}
170 for line in change_lines.split('\x00'):
171 assert len(line) > 4, 'Unexpected change line format %s' % line
172 if line[1] == ' ':
173 continue # Already staged for commit.
174 path = line[3:]
175 unstaged_changes[path] = line[1]
176 return unstaged_changes
177
178 def add_all(self, pathspec=None):
179 command = ['add', '--all']
180 if pathspec:
181 command.append(pathspec)
182 return self._run_git(command)
183
184 def add_list(self, paths, return_exit_code=False):
185 return self._run_git(["add"] + paths, return_exit_code=return_exit_code)
186
187 def delete_list(self, paths):
188 return self._run_git(["rm", "-f"] + paths)
189
190 def move(self, origin, destination):
191 return self._run_git(["mv", "-f", origin, destination])
192
193 def exists(self, path):
194 return_code = self._run_git(["show", "HEAD:%s" % path], return_exit_code =True, decode_output=False)
195 return return_code != self.ERROR_FILE_IS_MISSING
196
197 def _branch_from_ref(self, ref):
198 return ref.replace('refs/heads/', '')
199
200 def current_branch(self):
201 """Returns the name of the current branch, or empty string if HEAD is de tached."""
202 ref = self._run_git(['rev-parse', '--symbolic-full-name', 'HEAD']).strip ()
203 if ref == 'HEAD':
204 # HEAD is detached; return an empty string.
205 return ''
206 return self._branch_from_ref(ref)
207
208 def current_branch_or_ref(self):
209 """Returns the name of the current branch, or the commit hash if HEAD is detached."""
210 branch_name = self.current_branch()
211 if not branch_name:
212 # HEAD is detached; use commit SHA instead.
213 return self._run_git(['rev-parse', 'HEAD']).strip()
214 return branch_name
215
216 def _upstream_branch(self):
217 current_branch = self.current_branch()
218 return self._branch_from_ref(self.read_git_config(
219 'branch.%s.merge' % current_branch, cwd=self.checkout_root, executiv e=self._executive).strip())
220
221 def _merge_base(self, git_commit=None):
222 if git_commit:
223 # Rewrite UPSTREAM to the upstream branch
224 if 'UPSTREAM' in git_commit:
225 upstream = self._upstream_branch()
226 if not upstream:
227 raise ScriptError(message='No upstream/tracking branch set.' )
228 git_commit = git_commit.replace('UPSTREAM', upstream)
229
230 # Special-case <refname>.. to include working copy changes, e.g., 'H EAD....' shows only the diffs from HEAD.
231 if git_commit.endswith('....'):
232 return git_commit[:-4]
233
234 if '..' not in git_commit:
235 git_commit = git_commit + "^.." + git_commit
236 return git_commit
237
238 return self._remote_merge_base()
239
240 def changed_files(self, git_commit=None):
241 # FIXME: --diff-filter could be used to avoid the "extract_filenames" st ep.
242 status_command = ['diff', '-r', '--name-status',
243 "--no-renames", "--no-ext-diff", "--full-index", self. _merge_base(git_commit)]
244 # FIXME: I'm not sure we're returning the same set of files that SVN.cha nged_files is.
245 # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
246 return self._run_status_and_extract_filenames(status_command, self._stat us_regexp("ADM"))
247
248 def added_files(self):
249 return self._run_status_and_extract_filenames(self.status_command(), sel f._status_regexp("A"))
250
251 def _run_status_and_extract_filenames(self, status_command, status_regexp):
252 filenames = []
253 # We run with cwd=self.checkout_root so that returned-paths are root-rel ative.
254 for line in self._run_git(status_command, cwd=self.checkout_root).splitl ines():
255 match = re.search(status_regexp, line)
256 if not match:
257 continue
258 # status = match.group('status')
259 filename = match.group('filename')
260 filenames.append(filename)
261 return filenames
262
263 def status_command(self):
264 # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.
265 # No file contents printed, thus utf-8 autodecoding in self.run is fine.
266 return ["diff", "--name-status", "--no-renames", "HEAD"]
267
268 def _status_regexp(self, expected_types):
269 return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types
270
271 @staticmethod
272 def supports_local_commits():
273 return True
274
275 def display_name(self):
276 return "git"
277
278 def most_recent_log_matching(self, grep_str, path):
279 # We use '--grep=' + foo rather than '--grep', foo because
280 # git 1.7.0.4 (and earlier) didn't support the separate arg.
281 return self._run_git(['log', '-1', '--grep=' + grep_str, '--date=iso', s elf.find_checkout_root(path)])
282
283 def _commit_position_from_git_log(self, git_log):
284 match = re.search(r"^\s*Cr-Commit-Position:.*@\{#(?P<commit_position>\d+ )\}", git_log, re.MULTILINE)
285 if not match:
286 return ""
287 return int(match.group('commit_position'))
288
289 def commit_position(self, path):
290 """Returns the latest chromium commit position found in the checkout."""
291 git_log = self.most_recent_log_matching('Cr-Commit-Position:', path)
292 return self._commit_position_from_git_log(git_log)
293
294 def _commit_position_regex_for_timestamp(self):
295 return 'Cr-Commit-Position:.*@{#%s}'
296
297 def timestamp_of_revision(self, path, revision):
298 git_log = self.most_recent_log_matching(self._commit_position_regex_for_ timestamp() % revision, path)
299 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)
300 if not match:
301 return ""
302
303 # Manually modify the timezone since Git doesn't have an option to show it in UTC.
304 # Git also truncates milliseconds but we're going to ignore that for now .
305 time_with_timezone = datetime.datetime(int(match.group(1)), int(match.gr oup(2)), int(match.group(3)),
306 int(match.group(4)), int(match.gr oup(5)), int(match.group(6)), 0)
307
308 sign = 1 if match.group(7) == '+' else -1
309 time_without_timezone = time_with_timezone - \
310 datetime.timedelta(hours=sign * int(match.group(8)), minutes=int(mat ch.group(9)))
311 return time_without_timezone.strftime('%Y-%m-%dT%H:%M:%SZ')
312
313 def create_patch(self, git_commit=None, changed_files=None):
314 """Returns a byte array (str()) representing the patch file.
315 Patch files are effectively binary since they may contain
316 files of multiple different encodings.
317 """
318 order = self._patch_order()
319 command = [
320 'diff',
321 '--binary',
322 '--no-color',
323 "--no-ext-diff",
324 "--full-index",
325 "--no-renames",
326 "--src-prefix=a/",
327 "--dst-prefix=b/",
328
329 ]
330 if order:
331 command.append(order)
332 command += [self._merge_base(git_commit), "--"]
333 if changed_files:
334 command += changed_files
335 return self._run_git(command, decode_output=False, cwd=self.checkout_roo t)
336
337 def _patch_order(self):
338 # Put code changes at the top of the patch and layout tests
339 # at the bottom, this makes for easier reviewing.
340 config_path = self._filesystem.dirname(self._filesystem.path_to_module(' webkitpy.common.config'))
341 order_file = self._filesystem.join(config_path, 'orderfile')
342 if self._filesystem.exists(order_file):
343 return "-O%s" % order_file
344 return ""
345
346 @memoized
347 def commit_position_from_git_commit(self, git_commit):
348 git_log = self.git_commit_detail(git_commit)
349 return self._commit_position_from_git_log(git_log)
350
351 def checkout_branch(self, name):
352 self._run_git(['checkout', '-q', name])
353
354 def create_clean_branch(self, name):
355 self._run_git(['checkout', '-q', '-b', name, self._remote_branch_ref()])
356
357 def blame(self, path):
358 return self._run_git(['blame', '--show-email', path])
359
360 # Git-specific methods:
361 def _branch_ref_exists(self, branch_ref):
362 return self._run_git(['show-ref', '--quiet', '--verify', branch_ref], re turn_exit_code=True) == 0
363
364 def delete_branch(self, branch_name):
365 if self._branch_ref_exists('refs/heads/' + branch_name):
366 self._run_git(['branch', '-D', branch_name])
367
368 def _remote_merge_base(self):
369 return self._run_git(['merge-base', self._remote_branch_ref(), 'HEAD']). strip()
370
371 def _remote_branch_ref(self):
372 # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists.
373 remote_master_ref = 'refs/remotes/origin/master'
374 if not self._branch_ref_exists(remote_master_ref):
375 raise ScriptError(message="Can't find a branch to diff against. %s d oes not exist" % remote_master_ref)
376 return remote_master_ref
377
378 def commit_locally_with_message(self, message):
379 command = ['commit', '--all', '-F', '-']
380 self._run_git(command, input=message)
381
382 def pull(self, timeout_seconds=None):
383 self._run_git(['pull'], timeout_seconds=timeout_seconds)
384
385 def latest_git_commit(self):
386 return self._run_git(['log', '-1', '--format=%H']).strip()
387
388 def git_commits_since(self, commit):
389 return self._run_git(['log', commit + '..master', '--format=%H', '--reve rse']).split()
390
391 def git_commit_detail(self, commit, format=None): # pylint: disable=redefin ed-builtin
392 args = ['log', '-1', commit]
393 if format:
394 args.append('--format=' + format)
395 return self._run_git(args)
396
397 def affected_files(self, commit):
398 output = self._run_git(['log', '-1', '--format=', '--name-only', commit] )
399 return output.strip().split('\n')
400
401 def _branch_tracking_remote_master(self):
402 origin_info = self._run_git(['remote', 'show', 'origin', '-n'])
403 match = re.search(r"^\s*(?P<branch_name>\S+)\s+merges with remote master $", origin_info, re.MULTILINE)
404 if not match:
405 raise ScriptError(message="Unable to find local branch tracking orig in/master.")
406 branch = str(match.group("branch_name"))
407 return self._branch_from_ref(self._run_git(['rev-parse', '--symbolic-ful l-name', branch]).strip())
408
409 def is_cleanly_tracking_remote_master(self):
410 if self.has_working_directory_changes():
411 return False
412 if self.current_branch() != self._branch_tracking_remote_master():
413 return False
414 if len(self._local_commits(self._branch_tracking_remote_master())) > 0:
415 return False
416 return True
417
418 def ensure_cleanly_tracking_remote_master(self):
419 self._discard_working_directory_changes()
420 self._run_git(['checkout', '-q', self._branch_tracking_remote_master()])
421 self._discard_local_commits()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698