OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 # Copyright (C) 2008 Evan Martin <martine@danga.com> | |
7 | |
8 """A git-command for integrating reviews on Rietveld.""" | |
9 | |
10 from distutils.version import LooseVersion | |
11 import glob | |
12 import json | |
13 import logging | |
14 import optparse | |
15 import os | |
16 import Queue | |
17 import re | |
18 import stat | |
19 import sys | |
20 import textwrap | |
21 import threading | |
22 import urllib2 | |
23 import urlparse | |
24 import webbrowser | |
25 | |
26 try: | |
27 import readline # pylint: disable=F0401,W0611 | |
28 except ImportError: | |
29 pass | |
30 | |
31 | |
32 from third_party import colorama | |
33 from third_party import upload | |
34 import breakpad # pylint: disable=W0611 | |
35 import clang_format | |
36 import fix_encoding | |
37 import gclient_utils | |
38 import presubmit_support | |
39 import rietveld | |
40 import scm | |
41 import subcommand | |
42 import subprocess2 | |
43 import watchlists | |
44 import owners_finder | |
45 | |
46 __version__ = '1.0' | |
47 | |
48 DEFAULT_SERVER = 'https://codereview.appspot.com' | |
49 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' | |
50 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' | |
51 GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit' | |
52 CHANGE_ID = 'Change-Id:' | |
53 | |
54 # Shortcut since it quickly becomes redundant. | |
55 Fore = colorama.Fore | |
56 | |
57 # Initialized in main() | |
58 settings = None | |
59 | |
60 | |
61 def DieWithError(message): | |
62 print >> sys.stderr, message | |
63 sys.exit(1) | |
64 | |
65 | |
66 def GetNoGitPagerEnv(): | |
67 env = os.environ.copy() | |
68 # 'cat' is a magical git string that disables pagers on all platforms. | |
69 env['GIT_PAGER'] = 'cat' | |
70 return env | |
71 | |
72 def RunCommand(args, error_ok=False, error_message=None, **kwargs): | |
73 try: | |
74 return subprocess2.check_output(args, shell=False, **kwargs) | |
75 except subprocess2.CalledProcessError as e: | |
76 logging.debug('Failed running %s', args) | |
77 if not error_ok: | |
78 DieWithError( | |
79 'Command "%s" failed.\n%s' % ( | |
80 ' '.join(args), error_message or e.stdout or '')) | |
81 return e.stdout | |
82 | |
83 | |
84 def RunGit(args, **kwargs): | |
85 """Returns stdout.""" | |
86 return RunCommand(['git'] + args, **kwargs) | |
87 | |
88 | |
89 def RunGitWithCode(args, suppress_stderr=False): | |
90 """Returns return code and stdout.""" | |
91 try: | |
92 if suppress_stderr: | |
93 stderr = subprocess2.VOID | |
94 else: | |
95 stderr = sys.stderr | |
96 out, code = subprocess2.communicate(['git'] + args, | |
97 env=GetNoGitPagerEnv(), | |
98 stdout=subprocess2.PIPE, | |
99 stderr=stderr) | |
100 return code, out[0] | |
101 except ValueError: | |
102 # When the subprocess fails, it returns None. That triggers a ValueError | |
103 # when trying to unpack the return value into (out, code). | |
104 return 1, '' | |
105 | |
106 | |
107 def IsGitVersionAtLeast(min_version): | |
108 prefix = 'git version ' | |
109 version = RunGit(['--version']).strip() | |
110 return (version.startswith(prefix) and | |
111 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version)) | |
112 | |
113 | |
114 def ask_for_data(prompt): | |
115 try: | |
116 return raw_input(prompt) | |
117 except KeyboardInterrupt: | |
118 # Hide the exception. | |
119 sys.exit(1) | |
120 | |
121 | |
122 def git_set_branch_value(key, value): | |
123 branch = Changelist().GetBranch() | |
124 if not branch: | |
125 return | |
126 | |
127 cmd = ['config'] | |
128 if isinstance(value, int): | |
129 cmd.append('--int') | |
130 git_key = 'branch.%s.%s' % (branch, key) | |
131 RunGit(cmd + [git_key, str(value)]) | |
132 | |
133 | |
134 def git_get_branch_default(key, default): | |
135 branch = Changelist().GetBranch() | |
136 if branch: | |
137 git_key = 'branch.%s.%s' % (branch, key) | |
138 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key]) | |
139 try: | |
140 return int(stdout.strip()) | |
141 except ValueError: | |
142 pass | |
143 return default | |
144 | |
145 | |
146 def add_git_similarity(parser): | |
147 parser.add_option( | |
148 '--similarity', metavar='SIM', type='int', action='store', | |
149 help='Sets the percentage that a pair of files need to match in order to' | |
150 ' be considered copies (default 50)') | |
151 parser.add_option( | |
152 '--find-copies', action='store_true', | |
153 help='Allows git to look for copies.') | |
154 parser.add_option( | |
155 '--no-find-copies', action='store_false', dest='find_copies', | |
156 help='Disallows git from looking for copies.') | |
157 | |
158 old_parser_args = parser.parse_args | |
159 def Parse(args): | |
160 options, args = old_parser_args(args) | |
161 | |
162 if options.similarity is None: | |
163 options.similarity = git_get_branch_default('git-cl-similarity', 50) | |
164 else: | |
165 print('Note: Saving similarity of %d%% in git config.' | |
166 % options.similarity) | |
167 git_set_branch_value('git-cl-similarity', options.similarity) | |
168 | |
169 options.similarity = max(0, min(options.similarity, 100)) | |
170 | |
171 if options.find_copies is None: | |
172 options.find_copies = bool( | |
173 git_get_branch_default('git-find-copies', True)) | |
174 else: | |
175 git_set_branch_value('git-find-copies', int(options.find_copies)) | |
176 | |
177 print('Using %d%% similarity for rename/copy detection. ' | |
178 'Override with --similarity.' % options.similarity) | |
179 | |
180 return options, args | |
181 parser.parse_args = Parse | |
182 | |
183 | |
184 def is_dirty_git_tree(cmd): | |
185 # Make sure index is up-to-date before running diff-index. | |
186 RunGit(['update-index', '--refresh', '-q'], error_ok=True) | |
187 dirty = RunGit(['diff-index', '--name-status', 'HEAD']) | |
188 if dirty: | |
189 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd | |
190 print 'Uncommitted files: (git diff-index --name-status HEAD)' | |
191 print dirty[:4096] | |
192 if len(dirty) > 4096: | |
193 print '... (run "git diff-index --name-status HEAD" to see full output).' | |
194 return True | |
195 return False | |
196 | |
197 | |
198 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | |
199 """Return the corresponding git ref if |base_url| together with |glob_spec| | |
200 matches the full |url|. | |
201 | |
202 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | |
203 """ | |
204 fetch_suburl, as_ref = glob_spec.split(':') | |
205 if allow_wildcards: | |
206 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | |
207 if glob_match: | |
208 # Parse specs like "branches/*/src:refs/remotes/svn/*" or | |
209 # "branches/{472,597,648}/src:refs/remotes/svn/*". | |
210 branch_re = re.escape(base_url) | |
211 if glob_match.group(1): | |
212 branch_re += '/' + re.escape(glob_match.group(1)) | |
213 wildcard = glob_match.group(2) | |
214 if wildcard == '*': | |
215 branch_re += '([^/]*)' | |
216 else: | |
217 # Escape and replace surrounding braces with parentheses and commas | |
218 # with pipe symbols. | |
219 wildcard = re.escape(wildcard) | |
220 wildcard = re.sub('^\\\\{', '(', wildcard) | |
221 wildcard = re.sub('\\\\,', '|', wildcard) | |
222 wildcard = re.sub('\\\\}$', ')', wildcard) | |
223 branch_re += wildcard | |
224 if glob_match.group(3): | |
225 branch_re += re.escape(glob_match.group(3)) | |
226 match = re.match(branch_re, url) | |
227 if match: | |
228 return re.sub('\*$', match.group(1), as_ref) | |
229 | |
230 # Parse specs like "trunk/src:refs/remotes/origin/trunk". | |
231 if fetch_suburl: | |
232 full_url = base_url + '/' + fetch_suburl | |
233 else: | |
234 full_url = base_url | |
235 if full_url == url: | |
236 return as_ref | |
237 return None | |
238 | |
239 | |
240 def print_stats(similarity, find_copies, args): | |
241 """Prints statistics about the change to the user.""" | |
242 # --no-ext-diff is broken in some versions of Git, so try to work around | |
243 # this by overriding the environment (but there is still a problem if the | |
244 # git config key "diff.external" is used). | |
245 env = GetNoGitPagerEnv() | |
246 if 'GIT_EXTERNAL_DIFF' in env: | |
247 del env['GIT_EXTERNAL_DIFF'] | |
248 | |
249 if find_copies: | |
250 similarity_options = ['--find-copies-harder', '-l100000', | |
251 '-C%s' % similarity] | |
252 else: | |
253 similarity_options = ['-M%s' % similarity] | |
254 | |
255 return subprocess2.call( | |
256 ['git', | |
257 'diff', '--no-ext-diff', '--stat'] + similarity_options + args, | |
258 env=env) | |
259 | |
260 | |
261 class Settings(object): | |
262 def __init__(self): | |
263 self.default_server = None | |
264 self.cc = None | |
265 self.root = None | |
266 self.is_git_svn = None | |
267 self.svn_branch = None | |
268 self.tree_status_url = None | |
269 self.viewvc_url = None | |
270 self.updated = False | |
271 self.is_gerrit = None | |
272 self.git_editor = None | |
273 | |
274 def LazyUpdateIfNeeded(self): | |
275 """Updates the settings from a codereview.settings file, if available.""" | |
276 if not self.updated: | |
277 # The only value that actually changes the behavior is | |
278 # autoupdate = "false". Everything else means "true". | |
279 autoupdate = RunGit(['config', 'rietveld.autoupdate'], | |
280 error_ok=True | |
281 ).strip().lower() | |
282 | |
283 cr_settings_file = FindCodereviewSettingsFile() | |
284 if autoupdate != 'false' and cr_settings_file: | |
285 LoadCodereviewSettingsFromFile(cr_settings_file) | |
286 # set updated to True to avoid infinite calling loop | |
287 # through DownloadHooks | |
288 self.updated = True | |
289 DownloadHooks(False) | |
290 self.updated = True | |
291 | |
292 def GetDefaultServerUrl(self, error_ok=False): | |
293 if not self.default_server: | |
294 self.LazyUpdateIfNeeded() | |
295 self.default_server = gclient_utils.UpgradeToHttps( | |
296 self._GetRietveldConfig('server', error_ok=True)) | |
297 if error_ok: | |
298 return self.default_server | |
299 if not self.default_server: | |
300 error_message = ('Could not find settings file. You must configure ' | |
301 'your review setup by running "git cl config".') | |
302 self.default_server = gclient_utils.UpgradeToHttps( | |
303 self._GetRietveldConfig('server', error_message=error_message)) | |
304 return self.default_server | |
305 | |
306 @staticmethod | |
307 def GetRelativeRoot(): | |
308 return RunGit(['rev-parse', '--show-cdup']).strip() | |
309 | |
310 def GetRoot(self): | |
311 if self.root is None: | |
312 self.root = os.path.abspath(self.GetRelativeRoot()) | |
313 return self.root | |
314 | |
315 def GetIsGitSvn(self): | |
316 """Return true if this repo looks like it's using git-svn.""" | |
317 if self.is_git_svn is None: | |
318 # If you have any "svn-remote.*" config keys, we think you're using svn. | |
319 self.is_git_svn = RunGitWithCode( | |
320 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0 | |
321 return self.is_git_svn | |
322 | |
323 def GetSVNBranch(self): | |
324 if self.svn_branch is None: | |
325 if not self.GetIsGitSvn(): | |
326 DieWithError('Repo doesn\'t appear to be a git-svn repo.') | |
327 | |
328 # Try to figure out which remote branch we're based on. | |
329 # Strategy: | |
330 # 1) iterate through our branch history and find the svn URL. | |
331 # 2) find the svn-remote that fetches from the URL. | |
332 | |
333 # regexp matching the git-svn line that contains the URL. | |
334 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE) | |
335 | |
336 # We don't want to go through all of history, so read a line from the | |
337 # pipe at a time. | |
338 # The -100 is an arbitrary limit so we don't search forever. | |
339 cmd = ['git', 'log', '-100', '--pretty=medium'] | |
340 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, | |
341 env=GetNoGitPagerEnv()) | |
342 url = None | |
343 for line in proc.stdout: | |
344 match = git_svn_re.match(line) | |
345 if match: | |
346 url = match.group(1) | |
347 proc.stdout.close() # Cut pipe. | |
348 break | |
349 | |
350 if url: | |
351 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$') | |
352 remotes = RunGit(['config', '--get-regexp', | |
353 r'^svn-remote\..*\.url']).splitlines() | |
354 for remote in remotes: | |
355 match = svn_remote_re.match(remote) | |
356 if match: | |
357 remote = match.group(1) | |
358 base_url = match.group(2) | |
359 rewrite_root = RunGit( | |
360 ['config', 'svn-remote.%s.rewriteRoot' % remote], | |
361 error_ok=True).strip() | |
362 if rewrite_root: | |
363 base_url = rewrite_root | |
364 fetch_spec = RunGit( | |
365 ['config', 'svn-remote.%s.fetch' % remote], | |
366 error_ok=True).strip() | |
367 if fetch_spec: | |
368 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False) | |
369 if self.svn_branch: | |
370 break | |
371 branch_spec = RunGit( | |
372 ['config', 'svn-remote.%s.branches' % remote], | |
373 error_ok=True).strip() | |
374 if branch_spec: | |
375 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True) | |
376 if self.svn_branch: | |
377 break | |
378 tag_spec = RunGit( | |
379 ['config', 'svn-remote.%s.tags' % remote], | |
380 error_ok=True).strip() | |
381 if tag_spec: | |
382 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True) | |
383 if self.svn_branch: | |
384 break | |
385 | |
386 if not self.svn_branch: | |
387 DieWithError('Can\'t guess svn branch -- try specifying it on the ' | |
388 'command line') | |
389 | |
390 return self.svn_branch | |
391 | |
392 def GetTreeStatusUrl(self, error_ok=False): | |
393 if not self.tree_status_url: | |
394 error_message = ('You must configure your tree status URL by running ' | |
395 '"git cl config".') | |
396 self.tree_status_url = self._GetRietveldConfig( | |
397 'tree-status-url', error_ok=error_ok, error_message=error_message) | |
398 return self.tree_status_url | |
399 | |
400 def GetViewVCUrl(self): | |
401 if not self.viewvc_url: | |
402 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True) | |
403 return self.viewvc_url | |
404 | |
405 def GetBugPrefix(self): | |
406 return self._GetRietveldConfig('bug-prefix', error_ok=True) | |
407 | |
408 def GetDefaultCCList(self): | |
409 return self._GetRietveldConfig('cc', error_ok=True) | |
410 | |
411 def GetDefaultPrivateFlag(self): | |
412 return self._GetRietveldConfig('private', error_ok=True) | |
413 | |
414 def GetIsGerrit(self): | |
415 """Return true if this repo is assosiated with gerrit code review system.""" | |
416 if self.is_gerrit is None: | |
417 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True) | |
418 return self.is_gerrit | |
419 | |
420 def GetGitEditor(self): | |
421 """Return the editor specified in the git config, or None if none is.""" | |
422 if self.git_editor is None: | |
423 self.git_editor = self._GetConfig('core.editor', error_ok=True) | |
424 return self.git_editor or None | |
425 | |
426 def _GetRietveldConfig(self, param, **kwargs): | |
427 return self._GetConfig('rietveld.' + param, **kwargs) | |
428 | |
429 def _GetConfig(self, param, **kwargs): | |
430 self.LazyUpdateIfNeeded() | |
431 return RunGit(['config', param], **kwargs).strip() | |
432 | |
433 | |
434 def ShortBranchName(branch): | |
435 """Convert a name like 'refs/heads/foo' to just 'foo'.""" | |
436 return branch.replace('refs/heads/', '') | |
437 | |
438 | |
439 class Changelist(object): | |
440 def __init__(self, branchref=None, issue=None): | |
441 # Poke settings so we get the "configure your server" message if necessary. | |
442 global settings | |
443 if not settings: | |
444 # Happens when git_cl.py is used as a utility library. | |
445 settings = Settings() | |
446 settings.GetDefaultServerUrl() | |
447 self.branchref = branchref | |
448 if self.branchref: | |
449 self.branch = ShortBranchName(self.branchref) | |
450 else: | |
451 self.branch = None | |
452 self.rietveld_server = None | |
453 self.upstream_branch = None | |
454 self.lookedup_issue = False | |
455 self.issue = issue or None | |
456 self.has_description = False | |
457 self.description = None | |
458 self.lookedup_patchset = False | |
459 self.patchset = None | |
460 self._rpc_server = None | |
461 self.cc = None | |
462 self.watchers = () | |
463 self._remote = None | |
464 self._props = None | |
465 | |
466 def GetCCList(self): | |
467 """Return the users cc'd on this CL. | |
468 | |
469 Return is a string suitable for passing to gcl with the --cc flag. | |
470 """ | |
471 if self.cc is None: | |
472 base_cc = settings.GetDefaultCCList() | |
473 more_cc = ','.join(self.watchers) | |
474 self.cc = ','.join(filter(None, (base_cc, more_cc))) or '' | |
475 return self.cc | |
476 | |
477 def GetCCListWithoutDefault(self): | |
478 """Return the users cc'd on this CL excluding default ones.""" | |
479 if self.cc is None: | |
480 self.cc = ','.join(self.watchers) | |
481 return self.cc | |
482 | |
483 def SetWatchers(self, watchers): | |
484 """Set the list of email addresses that should be cc'd based on the changed | |
485 files in this CL. | |
486 """ | |
487 self.watchers = watchers | |
488 | |
489 def GetBranch(self): | |
490 """Returns the short branch name, e.g. 'master'.""" | |
491 if not self.branch: | |
492 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip() | |
493 self.branch = ShortBranchName(self.branchref) | |
494 return self.branch | |
495 | |
496 def GetBranchRef(self): | |
497 """Returns the full branch name, e.g. 'refs/heads/master'.""" | |
498 self.GetBranch() # Poke the lazy loader. | |
499 return self.branchref | |
500 | |
501 @staticmethod | |
502 def FetchUpstreamTuple(branch): | |
503 """Returns a tuple containing remote and remote ref, | |
504 e.g. 'origin', 'refs/heads/master' | |
505 """ | |
506 remote = '.' | |
507 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch], | |
508 error_ok=True).strip() | |
509 if upstream_branch: | |
510 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip() | |
511 else: | |
512 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'], | |
513 error_ok=True).strip() | |
514 if upstream_branch: | |
515 remote = RunGit(['config', 'rietveld.upstream-remote']).strip() | |
516 else: | |
517 # Fall back on trying a git-svn upstream branch. | |
518 if settings.GetIsGitSvn(): | |
519 upstream_branch = settings.GetSVNBranch() | |
520 else: | |
521 # Else, try to guess the origin remote. | |
522 remote_branches = RunGit(['branch', '-r']).split() | |
523 if 'origin/master' in remote_branches: | |
524 # Fall back on origin/master if it exits. | |
525 remote = 'origin' | |
526 upstream_branch = 'refs/heads/master' | |
527 elif 'origin/trunk' in remote_branches: | |
528 # Fall back on origin/trunk if it exists. Generally a shared | |
529 # git-svn clone | |
530 remote = 'origin' | |
531 upstream_branch = 'refs/heads/trunk' | |
532 else: | |
533 DieWithError("""Unable to determine default branch to diff against. | |
534 Either pass complete "git diff"-style arguments, like | |
535 git cl upload origin/master | |
536 or verify this branch is set up to track another (via the --track argument to | |
537 "git checkout -b ...").""") | |
538 | |
539 return remote, upstream_branch | |
540 | |
541 def GetCommonAncestorWithUpstream(self): | |
542 return RunGit(['merge-base', self.GetUpstreamBranch(), 'HEAD']).strip() | |
543 | |
544 def GetUpstreamBranch(self): | |
545 if self.upstream_branch is None: | |
546 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch()) | |
547 if remote is not '.': | |
548 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote) | |
549 self.upstream_branch = upstream_branch | |
550 return self.upstream_branch | |
551 | |
552 def GetRemoteBranch(self): | |
553 if not self._remote: | |
554 remote, branch = None, self.GetBranch() | |
555 seen_branches = set() | |
556 while branch not in seen_branches: | |
557 seen_branches.add(branch) | |
558 remote, branch = self.FetchUpstreamTuple(branch) | |
559 branch = ShortBranchName(branch) | |
560 if remote != '.' or branch.startswith('refs/remotes'): | |
561 break | |
562 else: | |
563 remotes = RunGit(['remote'], error_ok=True).split() | |
564 if len(remotes) == 1: | |
565 remote, = remotes | |
566 elif 'origin' in remotes: | |
567 remote = 'origin' | |
568 logging.warning('Could not determine which remote this change is ' | |
569 'associated with, so defaulting to "%s". This may ' | |
570 'not be what you want. You may prevent this message ' | |
571 'by running "git svn info" as documented here: %s', | |
572 self._remote, | |
573 GIT_INSTRUCTIONS_URL) | |
574 else: | |
575 logging.warn('Could not determine which remote this change is ' | |
576 'associated with. You may prevent this message by ' | |
577 'running "git svn info" as documented here: %s', | |
578 GIT_INSTRUCTIONS_URL) | |
579 branch = 'HEAD' | |
580 if branch.startswith('refs/remotes'): | |
581 self._remote = (remote, branch) | |
582 else: | |
583 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch)) | |
584 return self._remote | |
585 | |
586 def GitSanityChecks(self, upstream_git_obj): | |
587 """Checks git repo status and ensures diff is from local commits.""" | |
588 | |
589 # Verify the commit we're diffing against is in our current branch. | |
590 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip() | |
591 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip() | |
592 if upstream_sha != common_ancestor: | |
593 print >> sys.stderr, ( | |
594 'ERROR: %s is not in the current branch. You may need to rebase ' | |
595 'your tracking branch' % upstream_sha) | |
596 return False | |
597 | |
598 # List the commits inside the diff, and verify they are all local. | |
599 commits_in_diff = RunGit( | |
600 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines() | |
601 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch']) | |
602 remote_branch = remote_branch.strip() | |
603 if code != 0: | |
604 _, remote_branch = self.GetRemoteBranch() | |
605 | |
606 commits_in_remote = RunGit( | |
607 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines() | |
608 | |
609 common_commits = set(commits_in_diff) & set(commits_in_remote) | |
610 if common_commits: | |
611 print >> sys.stderr, ( | |
612 'ERROR: Your diff contains %d commits already in %s.\n' | |
613 'Run "git log --oneline %s..HEAD" to get a list of commits in ' | |
614 'the diff. If you are using a custom git flow, you can override' | |
615 ' the reference used for this check with "git config ' | |
616 'gitcl.remotebranch <git-ref>".' % ( | |
617 len(common_commits), remote_branch, upstream_git_obj)) | |
618 return False | |
619 return True | |
620 | |
621 def GetGitBaseUrlFromConfig(self): | |
622 """Return the configured base URL from branch.<branchname>.baseurl. | |
623 | |
624 Returns None if it is not set. | |
625 """ | |
626 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()], | |
627 error_ok=True).strip() | |
628 | |
629 def GetRemoteUrl(self): | |
630 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'. | |
631 | |
632 Returns None if there is no remote. | |
633 """ | |
634 remote, _ = self.GetRemoteBranch() | |
635 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip() | |
636 | |
637 def GetIssue(self): | |
638 """Returns the issue number as a int or None if not set.""" | |
639 if self.issue is None and not self.lookedup_issue: | |
640 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip() | |
641 self.issue = int(issue) or None if issue else None | |
642 self.lookedup_issue = True | |
643 return self.issue | |
644 | |
645 def GetRietveldServer(self): | |
646 if not self.rietveld_server: | |
647 # If we're on a branch then get the server potentially associated | |
648 # with that branch. | |
649 if self.GetIssue(): | |
650 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit( | |
651 ['config', self._RietveldServer()], error_ok=True).strip()) | |
652 if not self.rietveld_server: | |
653 self.rietveld_server = settings.GetDefaultServerUrl() | |
654 return self.rietveld_server | |
655 | |
656 def GetIssueURL(self): | |
657 """Get the URL for a particular issue.""" | |
658 if not self.GetIssue(): | |
659 return None | |
660 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue()) | |
661 | |
662 def GetDescription(self, pretty=False): | |
663 if not self.has_description: | |
664 if self.GetIssue(): | |
665 issue = self.GetIssue() | |
666 try: | |
667 self.description = self.RpcServer().get_description(issue).strip() | |
668 except urllib2.HTTPError, e: | |
669 if e.code == 404: | |
670 DieWithError( | |
671 ('\nWhile fetching the description for issue %d, received a ' | |
672 '404 (not found)\n' | |
673 'error. It is likely that you deleted this ' | |
674 'issue on the server. If this is the\n' | |
675 'case, please run\n\n' | |
676 ' git cl issue 0\n\n' | |
677 'to clear the association with the deleted issue. Then run ' | |
678 'this command again.') % issue) | |
679 else: | |
680 DieWithError( | |
681 '\nFailed to fetch issue description. HTTP error %d' % e.code) | |
682 self.has_description = True | |
683 if pretty: | |
684 wrapper = textwrap.TextWrapper() | |
685 wrapper.initial_indent = wrapper.subsequent_indent = ' ' | |
686 return wrapper.fill(self.description) | |
687 return self.description | |
688 | |
689 def GetPatchset(self): | |
690 """Returns the patchset number as a int or None if not set.""" | |
691 if self.patchset is None and not self.lookedup_patchset: | |
692 patchset = RunGit(['config', self._PatchsetSetting()], | |
693 error_ok=True).strip() | |
694 self.patchset = int(patchset) or None if patchset else None | |
695 self.lookedup_patchset = True | |
696 return self.patchset | |
697 | |
698 def SetPatchset(self, patchset): | |
699 """Set this branch's patchset. If patchset=0, clears the patchset.""" | |
700 if patchset: | |
701 RunGit(['config', self._PatchsetSetting(), str(patchset)]) | |
702 self.patchset = patchset | |
703 else: | |
704 RunGit(['config', '--unset', self._PatchsetSetting()], | |
705 stderr=subprocess2.PIPE, error_ok=True) | |
706 self.patchset = None | |
707 | |
708 def GetMostRecentPatchset(self): | |
709 return self.GetIssueProperties()['patchsets'][-1] | |
710 | |
711 def GetPatchSetDiff(self, issue, patchset): | |
712 return self.RpcServer().get( | |
713 '/download/issue%s_%s.diff' % (issue, patchset)) | |
714 | |
715 def GetIssueProperties(self): | |
716 if self._props is None: | |
717 issue = self.GetIssue() | |
718 if not issue: | |
719 self._props = {} | |
720 else: | |
721 self._props = self.RpcServer().get_issue_properties(issue, True) | |
722 return self._props | |
723 | |
724 def GetApprovingReviewers(self): | |
725 return get_approving_reviewers(self.GetIssueProperties()) | |
726 | |
727 def SetIssue(self, issue): | |
728 """Set this branch's issue. If issue=0, clears the issue.""" | |
729 if issue: | |
730 self.issue = issue | |
731 RunGit(['config', self._IssueSetting(), str(issue)]) | |
732 if self.rietveld_server: | |
733 RunGit(['config', self._RietveldServer(), self.rietveld_server]) | |
734 else: | |
735 current_issue = self.GetIssue() | |
736 if current_issue: | |
737 RunGit(['config', '--unset', self._IssueSetting()]) | |
738 self.issue = None | |
739 self.SetPatchset(None) | |
740 | |
741 def GetChange(self, upstream_branch, author): | |
742 if not self.GitSanityChecks(upstream_branch): | |
743 DieWithError('\nGit sanity check failure') | |
744 | |
745 root = settings.GetRelativeRoot() | |
746 if not root: | |
747 root = '.' | |
748 absroot = os.path.abspath(root) | |
749 | |
750 # We use the sha1 of HEAD as a name of this change. | |
751 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip() | |
752 # Need to pass a relative path for msysgit. | |
753 try: | |
754 files = scm.GIT.CaptureStatus([root], '.', upstream_branch) | |
755 except subprocess2.CalledProcessError: | |
756 DieWithError( | |
757 ('\nFailed to diff against upstream branch %s\n\n' | |
758 'This branch probably doesn\'t exist anymore. To reset the\n' | |
759 'tracking branch, please run\n' | |
760 ' git branch --set-upstream %s trunk\n' | |
761 'replacing trunk with origin/master or the relevant branch') % | |
762 (upstream_branch, self.GetBranch())) | |
763 | |
764 issue = self.GetIssue() | |
765 patchset = self.GetPatchset() | |
766 if issue: | |
767 description = self.GetDescription() | |
768 else: | |
769 # If the change was never uploaded, use the log messages of all commits | |
770 # up to the branch point, as git cl upload will prefill the description | |
771 # with these log messages. | |
772 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)] | |
773 description = RunGitWithCode(args)[1].strip() | |
774 | |
775 if not author: | |
776 author = RunGit(['config', 'user.email']).strip() or None | |
777 return presubmit_support.GitChange( | |
778 name, | |
779 description, | |
780 absroot, | |
781 files, | |
782 issue, | |
783 patchset, | |
784 author) | |
785 | |
786 def RunHook(self, committing, may_prompt, verbose, change): | |
787 """Calls sys.exit() if the hook fails; returns a HookResults otherwise.""" | |
788 | |
789 try: | |
790 return presubmit_support.DoPresubmitChecks(change, committing, | |
791 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin, | |
792 default_presubmit=None, may_prompt=may_prompt, | |
793 rietveld_obj=self.RpcServer()) | |
794 except presubmit_support.PresubmitFailure, e: | |
795 DieWithError( | |
796 ('%s\nMaybe your depot_tools is out of date?\n' | |
797 'If all fails, contact maruel@') % e) | |
798 | |
799 def UpdateDescription(self, description): | |
800 self.description = description | |
801 return self.RpcServer().update_description( | |
802 self.GetIssue(), self.description) | |
803 | |
804 def CloseIssue(self): | |
805 """Updates the description and closes the issue.""" | |
806 return self.RpcServer().close_issue(self.GetIssue()) | |
807 | |
808 def SetFlag(self, flag, value): | |
809 """Patchset must match.""" | |
810 if not self.GetPatchset(): | |
811 DieWithError('The patchset needs to match. Send another patchset.') | |
812 try: | |
813 return self.RpcServer().set_flag( | |
814 self.GetIssue(), self.GetPatchset(), flag, value) | |
815 except urllib2.HTTPError, e: | |
816 if e.code == 404: | |
817 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue()) | |
818 if e.code == 403: | |
819 DieWithError( | |
820 ('Access denied to issue %s. Maybe the patchset %s doesn\'t ' | |
821 'match?') % (self.GetIssue(), self.GetPatchset())) | |
822 raise | |
823 | |
824 def RpcServer(self): | |
825 """Returns an upload.RpcServer() to access this review's rietveld instance. | |
826 """ | |
827 if not self._rpc_server: | |
828 self._rpc_server = rietveld.CachingRietveld( | |
829 self.GetRietveldServer(), None, None) | |
830 return self._rpc_server | |
831 | |
832 def _IssueSetting(self): | |
833 """Return the git setting that stores this change's issue.""" | |
834 return 'branch.%s.rietveldissue' % self.GetBranch() | |
835 | |
836 def _PatchsetSetting(self): | |
837 """Return the git setting that stores this change's most recent patchset.""" | |
838 return 'branch.%s.rietveldpatchset' % self.GetBranch() | |
839 | |
840 def _RietveldServer(self): | |
841 """Returns the git setting that stores this change's rietveld server.""" | |
842 return 'branch.%s.rietveldserver' % self.GetBranch() | |
843 | |
844 | |
845 def GetCodereviewSettingsInteractively(): | |
846 """Prompt the user for settings.""" | |
847 # TODO(ukai): ask code review system is rietveld or gerrit? | |
848 server = settings.GetDefaultServerUrl(error_ok=True) | |
849 prompt = 'Rietveld server (host[:port])' | |
850 prompt += ' [%s]' % (server or DEFAULT_SERVER) | |
851 newserver = ask_for_data(prompt + ':') | |
852 if not server and not newserver: | |
853 newserver = DEFAULT_SERVER | |
854 if newserver: | |
855 newserver = gclient_utils.UpgradeToHttps(newserver) | |
856 if newserver != server: | |
857 RunGit(['config', 'rietveld.server', newserver]) | |
858 | |
859 def SetProperty(initial, caption, name, is_url): | |
860 prompt = caption | |
861 if initial: | |
862 prompt += ' ("x" to clear) [%s]' % initial | |
863 new_val = ask_for_data(prompt + ':') | |
864 if new_val == 'x': | |
865 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True) | |
866 elif new_val: | |
867 if is_url: | |
868 new_val = gclient_utils.UpgradeToHttps(new_val) | |
869 if new_val != initial: | |
870 RunGit(['config', 'rietveld.' + name, new_val]) | |
871 | |
872 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False) | |
873 SetProperty(settings.GetDefaultPrivateFlag(), | |
874 'Private flag (rietveld only)', 'private', False) | |
875 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL', | |
876 'tree-status-url', False) | |
877 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True) | |
878 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False) | |
879 | |
880 # TODO: configure a default branch to diff against, rather than this | |
881 # svn-based hackery. | |
882 | |
883 | |
884 class ChangeDescription(object): | |
885 """Contains a parsed form of the change description.""" | |
886 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' | |
887 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$' | |
888 | |
889 def __init__(self, description): | |
890 self._description_lines = (description or '').strip().splitlines() | |
891 | |
892 @property # www.logilab.org/ticket/89786 | |
893 def description(self): # pylint: disable=E0202 | |
894 return '\n'.join(self._description_lines) | |
895 | |
896 def set_description(self, desc): | |
897 if isinstance(desc, basestring): | |
898 lines = desc.splitlines() | |
899 else: | |
900 lines = [line.rstrip() for line in desc] | |
901 while lines and not lines[0]: | |
902 lines.pop(0) | |
903 while lines and not lines[-1]: | |
904 lines.pop(-1) | |
905 self._description_lines = lines | |
906 | |
907 def update_reviewers(self, reviewers): | |
908 """Rewrites the R=/TBR= line(s) as a single line each.""" | |
909 assert isinstance(reviewers, list), reviewers | |
910 if not reviewers: | |
911 return | |
912 reviewers = reviewers[:] | |
913 | |
914 # Get the set of R= and TBR= lines and remove them from the desciption. | |
915 regexp = re.compile(self.R_LINE) | |
916 matches = [regexp.match(line) for line in self._description_lines] | |
917 new_desc = [l for i, l in enumerate(self._description_lines) | |
918 if not matches[i]] | |
919 self.set_description(new_desc) | |
920 | |
921 # Construct new unified R= and TBR= lines. | |
922 r_names = [] | |
923 tbr_names = [] | |
924 for match in matches: | |
925 if not match: | |
926 continue | |
927 people = cleanup_list([match.group(2).strip()]) | |
928 if match.group(1) == 'TBR': | |
929 tbr_names.extend(people) | |
930 else: | |
931 r_names.extend(people) | |
932 for name in r_names: | |
933 if name not in reviewers: | |
934 reviewers.append(name) | |
935 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None | |
936 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None | |
937 | |
938 # Put the new lines in the description where the old first R= line was. | |
939 line_loc = next((i for i, match in enumerate(matches) if match), -1) | |
940 if 0 <= line_loc < len(self._description_lines): | |
941 if new_tbr_line: | |
942 self._description_lines.insert(line_loc, new_tbr_line) | |
943 if new_r_line: | |
944 self._description_lines.insert(line_loc, new_r_line) | |
945 else: | |
946 if new_r_line: | |
947 self.append_footer(new_r_line) | |
948 if new_tbr_line: | |
949 self.append_footer(new_tbr_line) | |
950 | |
951 def prompt(self): | |
952 """Asks the user to update the description.""" | |
953 self.set_description([ | |
954 '# Enter a description of the change.', | |
955 '# This will be displayed on the codereview site.', | |
956 '# The first line will also be used as the subject of the review.', | |
957 '#--------------------This line is 72 characters long' | |
958 '--------------------', | |
959 ] + self._description_lines) | |
960 | |
961 regexp = re.compile(self.BUG_LINE) | |
962 if not any((regexp.match(line) for line in self._description_lines)): | |
963 self.append_footer('BUG=%s' % settings.GetBugPrefix()) | |
964 content = gclient_utils.RunEditor(self.description, True, | |
965 git_editor=settings.GetGitEditor()) | |
966 if not content: | |
967 DieWithError('Running editor failed') | |
968 lines = content.splitlines() | |
969 | |
970 # Strip off comments. | |
971 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')] | |
972 if not clean_lines: | |
973 DieWithError('No CL description, aborting') | |
974 self.set_description(clean_lines) | |
975 | |
976 def append_footer(self, line): | |
977 if self._description_lines: | |
978 # Add an empty line if either the last line or the new line isn't a tag. | |
979 last_line = self._description_lines[-1] | |
980 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or | |
981 not presubmit_support.Change.TAG_LINE_RE.match(line)): | |
982 self._description_lines.append('') | |
983 self._description_lines.append(line) | |
984 | |
985 def get_reviewers(self): | |
986 """Retrieves the list of reviewers.""" | |
987 matches = [re.match(self.R_LINE, line) for line in self._description_lines] | |
988 reviewers = [match.group(2).strip() for match in matches if match] | |
989 return cleanup_list(reviewers) | |
990 | |
991 | |
992 def get_approving_reviewers(props): | |
993 """Retrieves the reviewers that approved a CL from the issue properties with | |
994 messages. | |
995 | |
996 Note that the list may contain reviewers that are not committer, thus are not | |
997 considered by the CQ. | |
998 """ | |
999 return sorted( | |
1000 set( | |
1001 message['sender'] | |
1002 for message in props['messages'] | |
1003 if message['approval'] and message['sender'] in props['reviewers'] | |
1004 ) | |
1005 ) | |
1006 | |
1007 | |
1008 def FindCodereviewSettingsFile(filename='codereview.settings'): | |
1009 """Finds the given file starting in the cwd and going up. | |
1010 | |
1011 Only looks up to the top of the repository unless an | |
1012 'inherit-review-settings-ok' file exists in the root of the repository. | |
1013 """ | |
1014 inherit_ok_file = 'inherit-review-settings-ok' | |
1015 cwd = os.getcwd() | |
1016 root = settings.GetRoot() | |
1017 if os.path.isfile(os.path.join(root, inherit_ok_file)): | |
1018 root = '/' | |
1019 while True: | |
1020 if filename in os.listdir(cwd): | |
1021 if os.path.isfile(os.path.join(cwd, filename)): | |
1022 return open(os.path.join(cwd, filename)) | |
1023 if cwd == root: | |
1024 break | |
1025 cwd = os.path.dirname(cwd) | |
1026 | |
1027 | |
1028 def LoadCodereviewSettingsFromFile(fileobj): | |
1029 """Parse a codereview.settings file and updates hooks.""" | |
1030 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read()) | |
1031 | |
1032 def SetProperty(name, setting, unset_error_ok=False): | |
1033 fullname = 'rietveld.' + name | |
1034 if setting in keyvals: | |
1035 RunGit(['config', fullname, keyvals[setting]]) | |
1036 else: | |
1037 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok) | |
1038 | |
1039 SetProperty('server', 'CODE_REVIEW_SERVER') | |
1040 # Only server setting is required. Other settings can be absent. | |
1041 # In that case, we ignore errors raised during option deletion attempt. | |
1042 SetProperty('cc', 'CC_LIST', unset_error_ok=True) | |
1043 SetProperty('private', 'PRIVATE', unset_error_ok=True) | |
1044 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True) | |
1045 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True) | |
1046 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True) | |
1047 | |
1048 if 'GERRIT_HOST' in keyvals: | |
1049 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']]) | |
1050 | |
1051 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals: | |
1052 #should be of the form | |
1053 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof | |
1054 #ORIGIN_URL_CONFIG: http://src.chromium.org/git | |
1055 RunGit(['config', keyvals['PUSH_URL_CONFIG'], | |
1056 keyvals['ORIGIN_URL_CONFIG']]) | |
1057 | |
1058 | |
1059 def urlretrieve(source, destination): | |
1060 """urllib is broken for SSL connections via a proxy therefore we | |
1061 can't use urllib.urlretrieve().""" | |
1062 with open(destination, 'w') as f: | |
1063 f.write(urllib2.urlopen(source).read()) | |
1064 | |
1065 | |
1066 def hasSheBang(fname): | |
1067 """Checks fname is a #! script.""" | |
1068 with open(fname) as f: | |
1069 return f.read(2).startswith('#!') | |
1070 | |
1071 | |
1072 def DownloadHooks(force): | |
1073 """downloads hooks | |
1074 | |
1075 Args: | |
1076 force: True to update hooks. False to install hooks if not present. | |
1077 """ | |
1078 if not settings.GetIsGerrit(): | |
1079 return | |
1080 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg' | |
1081 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg') | |
1082 if not os.access(dst, os.X_OK): | |
1083 if os.path.exists(dst): | |
1084 if not force: | |
1085 return | |
1086 try: | |
1087 urlretrieve(src, dst) | |
1088 if not hasSheBang(dst): | |
1089 DieWithError('Not a script: %s\n' | |
1090 'You need to download from\n%s\n' | |
1091 'into .git/hooks/commit-msg and ' | |
1092 'chmod +x .git/hooks/commit-msg' % (dst, src)) | |
1093 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) | |
1094 except Exception: | |
1095 if os.path.exists(dst): | |
1096 os.remove(dst) | |
1097 DieWithError('\nFailed to download hooks.\n' | |
1098 'You need to download from\n%s\n' | |
1099 'into .git/hooks/commit-msg and ' | |
1100 'chmod +x .git/hooks/commit-msg' % src) | |
1101 | |
1102 | |
1103 @subcommand.usage('[repo root containing codereview.settings]') | |
1104 def CMDconfig(parser, args): | |
1105 """Edits configuration for this tree.""" | |
1106 | |
1107 parser.add_option('--activate-update', action='store_true', | |
1108 help='activate auto-updating [rietveld] section in ' | |
1109 '.git/config') | |
1110 parser.add_option('--deactivate-update', action='store_true', | |
1111 help='deactivate auto-updating [rietveld] section in ' | |
1112 '.git/config') | |
1113 options, args = parser.parse_args(args) | |
1114 | |
1115 if options.deactivate_update: | |
1116 RunGit(['config', 'rietveld.autoupdate', 'false']) | |
1117 return | |
1118 | |
1119 if options.activate_update: | |
1120 RunGit(['config', '--unset', 'rietveld.autoupdate']) | |
1121 return | |
1122 | |
1123 if len(args) == 0: | |
1124 GetCodereviewSettingsInteractively() | |
1125 DownloadHooks(True) | |
1126 return 0 | |
1127 | |
1128 url = args[0] | |
1129 if not url.endswith('codereview.settings'): | |
1130 url = os.path.join(url, 'codereview.settings') | |
1131 | |
1132 # Load code review settings and download hooks (if available). | |
1133 LoadCodereviewSettingsFromFile(urllib2.urlopen(url)) | |
1134 DownloadHooks(True) | |
1135 return 0 | |
1136 | |
1137 | |
1138 def CMDbaseurl(parser, args): | |
1139 """Gets or sets base-url for this branch.""" | |
1140 branchref = RunGit(['symbolic-ref', 'HEAD']).strip() | |
1141 branch = ShortBranchName(branchref) | |
1142 _, args = parser.parse_args(args) | |
1143 if not args: | |
1144 print("Current base-url:") | |
1145 return RunGit(['config', 'branch.%s.base-url' % branch], | |
1146 error_ok=False).strip() | |
1147 else: | |
1148 print("Setting base-url to %s" % args[0]) | |
1149 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]], | |
1150 error_ok=False).strip() | |
1151 | |
1152 | |
1153 def CMDstatus(parser, args): | |
1154 """Show status of changelists. | |
1155 | |
1156 Colors are used to tell the state of the CL unless --fast is used: | |
1157 - Red not sent for review or broken | |
1158 - Blue waiting for review | |
1159 - Yellow waiting for you to reply to review | |
1160 - Green LGTM'ed | |
1161 - Magenta in the commit queue | |
1162 - Cyan was committed, branch can be deleted | |
1163 | |
1164 Also see 'git cl comments'. | |
1165 """ | |
1166 parser.add_option('--field', | |
1167 help='print only specific field (desc|id|patch|url)') | |
1168 parser.add_option('-f', '--fast', action='store_true', | |
1169 help='Do not retrieve review status') | |
1170 (options, args) = parser.parse_args(args) | |
1171 if args: | |
1172 parser.error('Unsupported args: %s' % args) | |
1173 | |
1174 if options.field: | |
1175 cl = Changelist() | |
1176 if options.field.startswith('desc'): | |
1177 print cl.GetDescription() | |
1178 elif options.field == 'id': | |
1179 issueid = cl.GetIssue() | |
1180 if issueid: | |
1181 print issueid | |
1182 elif options.field == 'patch': | |
1183 patchset = cl.GetPatchset() | |
1184 if patchset: | |
1185 print patchset | |
1186 elif options.field == 'url': | |
1187 url = cl.GetIssueURL() | |
1188 if url: | |
1189 print url | |
1190 return 0 | |
1191 | |
1192 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads']) | |
1193 if not branches: | |
1194 print('No local branch found.') | |
1195 return 0 | |
1196 | |
1197 changes = (Changelist(branchref=b) for b in branches.splitlines()) | |
1198 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes) | |
1199 alignment = max(5, max(len(b) for b in branches)) | |
1200 print 'Branches associated with reviews:' | |
1201 # Adhoc thread pool to request data concurrently. | |
1202 output = Queue.Queue() | |
1203 | |
1204 # Silence upload.py otherwise it becomes unweldly. | |
1205 upload.verbosity = 0 | |
1206 | |
1207 if not options.fast: | |
1208 def fetch(b): | |
1209 """Fetches information for an issue and returns (branch, issue, color).""" | |
1210 c = Changelist(branchref=b) | |
1211 i = c.GetIssueURL() | |
1212 props = {} | |
1213 r = None | |
1214 if i: | |
1215 try: | |
1216 props = c.GetIssueProperties() | |
1217 r = c.GetApprovingReviewers() if i else None | |
1218 except urllib2.HTTPError: | |
1219 # The issue probably doesn't exist anymore. | |
1220 i += ' (broken)' | |
1221 | |
1222 msgs = props.get('messages') or [] | |
1223 | |
1224 if not i: | |
1225 color = Fore.WHITE | |
1226 elif props.get('closed'): | |
1227 # Issue is closed. | |
1228 color = Fore.CYAN | |
1229 elif props.get('commit'): | |
1230 # Issue is in the commit queue. | |
1231 color = Fore.MAGENTA | |
1232 elif r: | |
1233 # Was LGTM'ed. | |
1234 color = Fore.GREEN | |
1235 elif not msgs: | |
1236 # No message was sent. | |
1237 color = Fore.RED | |
1238 elif msgs[-1]['sender'] != props.get('owner_email'): | |
1239 color = Fore.YELLOW | |
1240 else: | |
1241 color = Fore.BLUE | |
1242 output.put((b, i, color)) | |
1243 | |
1244 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches] | |
1245 for t in threads: | |
1246 t.daemon = True | |
1247 t.start() | |
1248 else: | |
1249 # Do not use GetApprovingReviewers(), since it requires an HTTP request. | |
1250 for b in branches: | |
1251 c = Changelist(branchref=b) | |
1252 url = c.GetIssueURL() | |
1253 output.put((b, url, Fore.BLUE if url else Fore.WHITE)) | |
1254 | |
1255 tmp = {} | |
1256 alignment = max(5, max(len(ShortBranchName(b)) for b in branches)) | |
1257 for branch in sorted(branches): | |
1258 while branch not in tmp: | |
1259 b, i, color = output.get() | |
1260 tmp[b] = (i, color) | |
1261 issue, color = tmp.pop(branch) | |
1262 reset = Fore.RESET | |
1263 if not sys.stdout.isatty(): | |
1264 color = '' | |
1265 reset = '' | |
1266 print ' %*s : %s%s%s' % ( | |
1267 alignment, ShortBranchName(branch), color, issue, reset) | |
1268 | |
1269 cl = Changelist() | |
1270 print | |
1271 print 'Current branch:', | |
1272 if not cl.GetIssue(): | |
1273 print 'no issue assigned.' | |
1274 return 0 | |
1275 print cl.GetBranch() | |
1276 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()) | |
1277 print 'Issue description:' | |
1278 print cl.GetDescription(pretty=True) | |
1279 return 0 | |
1280 | |
1281 | |
1282 def colorize_CMDstatus_doc(): | |
1283 """To be called once in main() to add colors to git cl status help.""" | |
1284 colors = [i for i in dir(Fore) if i[0].isupper()] | |
1285 | |
1286 def colorize_line(line): | |
1287 for color in colors: | |
1288 if color in line.upper(): | |
1289 # Extract whitespaces first and the leading '-'. | |
1290 indent = len(line) - len(line.lstrip(' ')) + 1 | |
1291 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET | |
1292 return line | |
1293 | |
1294 lines = CMDstatus.__doc__.splitlines() | |
1295 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines) | |
1296 | |
1297 | |
1298 @subcommand.usage('[issue_number]') | |
1299 def CMDissue(parser, args): | |
1300 """Sets or displays the current code review issue number. | |
1301 | |
1302 Pass issue number 0 to clear the current issue. | |
1303 """ | |
1304 _, args = parser.parse_args(args) | |
1305 | |
1306 cl = Changelist() | |
1307 if len(args) > 0: | |
1308 try: | |
1309 issue = int(args[0]) | |
1310 except ValueError: | |
1311 DieWithError('Pass a number to set the issue or none to list it.\n' | |
1312 'Maybe you want to run git cl status?') | |
1313 cl.SetIssue(issue) | |
1314 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()) | |
1315 return 0 | |
1316 | |
1317 | |
1318 def CMDcomments(parser, args): | |
1319 """Shows review comments of the current changelist.""" | |
1320 (_, args) = parser.parse_args(args) | |
1321 if args: | |
1322 parser.error('Unsupported argument: %s' % args) | |
1323 | |
1324 cl = Changelist() | |
1325 if cl.GetIssue(): | |
1326 data = cl.GetIssueProperties() | |
1327 for message in sorted(data['messages'], key=lambda x: x['date']): | |
1328 if message['disapproval']: | |
1329 color = Fore.RED | |
1330 elif message['approval']: | |
1331 color = Fore.GREEN | |
1332 elif message['sender'] == data['owner_email']: | |
1333 color = Fore.MAGENTA | |
1334 else: | |
1335 color = Fore.BLUE | |
1336 print '\n%s%s %s%s' % ( | |
1337 color, message['date'].split('.', 1)[0], message['sender'], | |
1338 Fore.RESET) | |
1339 if message['text'].strip(): | |
1340 print '\n'.join(' ' + l for l in message['text'].splitlines()) | |
1341 return 0 | |
1342 | |
1343 | |
1344 def CMDdescription(parser, args): | |
1345 """Brings up the editor for the current CL's description.""" | |
1346 cl = Changelist() | |
1347 if not cl.GetIssue(): | |
1348 DieWithError('This branch has no associated changelist.') | |
1349 description = ChangeDescription(cl.GetDescription()) | |
1350 description.prompt() | |
1351 cl.UpdateDescription(description.description) | |
1352 return 0 | |
1353 | |
1354 | |
1355 def CreateDescriptionFromLog(args): | |
1356 """Pulls out the commit log to use as a base for the CL description.""" | |
1357 log_args = [] | |
1358 if len(args) == 1 and not args[0].endswith('.'): | |
1359 log_args = [args[0] + '..'] | |
1360 elif len(args) == 1 and args[0].endswith('...'): | |
1361 log_args = [args[0][:-1]] | |
1362 elif len(args) == 2: | |
1363 log_args = [args[0] + '..' + args[1]] | |
1364 else: | |
1365 log_args = args[:] # Hope for the best! | |
1366 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args) | |
1367 | |
1368 | |
1369 def CMDpresubmit(parser, args): | |
1370 """Runs presubmit tests on the current changelist.""" | |
1371 parser.add_option('-u', '--upload', action='store_true', | |
1372 help='Run upload hook instead of the push/dcommit hook') | |
1373 parser.add_option('-f', '--force', action='store_true', | |
1374 help='Run checks even if tree is dirty') | |
1375 (options, args) = parser.parse_args(args) | |
1376 | |
1377 if not options.force and is_dirty_git_tree('presubmit'): | |
1378 print 'use --force to check even if tree is dirty.' | |
1379 return 1 | |
1380 | |
1381 cl = Changelist() | |
1382 if args: | |
1383 base_branch = args[0] | |
1384 else: | |
1385 # Default to diffing against the common ancestor of the upstream branch. | |
1386 base_branch = cl.GetCommonAncestorWithUpstream() | |
1387 | |
1388 cl.RunHook( | |
1389 committing=not options.upload, | |
1390 may_prompt=False, | |
1391 verbose=options.verbose, | |
1392 change=cl.GetChange(base_branch, None)) | |
1393 return 0 | |
1394 | |
1395 | |
1396 def AddChangeIdToCommitMessage(options, args): | |
1397 """Re-commits using the current message, assumes the commit hook is in | |
1398 place. | |
1399 """ | |
1400 log_desc = options.message or CreateDescriptionFromLog(args) | |
1401 git_command = ['commit', '--amend', '-m', log_desc] | |
1402 RunGit(git_command) | |
1403 new_log_desc = CreateDescriptionFromLog(args) | |
1404 if CHANGE_ID in new_log_desc: | |
1405 print 'git-cl: Added Change-Id to commit message.' | |
1406 else: | |
1407 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.' | |
1408 | |
1409 | |
1410 def GerritUpload(options, args, cl): | |
1411 """upload the current branch to gerrit.""" | |
1412 # We assume the remote called "origin" is the one we want. | |
1413 # It is probably not worthwhile to support different workflows. | |
1414 remote = 'origin' | |
1415 branch = 'master' | |
1416 if options.target_branch: | |
1417 branch = options.target_branch | |
1418 | |
1419 change_desc = ChangeDescription( | |
1420 options.message or CreateDescriptionFromLog(args)) | |
1421 if not change_desc.description: | |
1422 print "Description is empty; aborting." | |
1423 return 1 | |
1424 if CHANGE_ID not in change_desc.description: | |
1425 AddChangeIdToCommitMessage(options, args) | |
1426 if options.reviewers: | |
1427 change_desc.update_reviewers(options.reviewers) | |
1428 | |
1429 receive_options = [] | |
1430 cc = cl.GetCCList().split(',') | |
1431 if options.cc: | |
1432 cc.extend(options.cc) | |
1433 cc = filter(None, cc) | |
1434 if cc: | |
1435 receive_options += ['--cc=' + email for email in cc] | |
1436 if change_desc.get_reviewers(): | |
1437 receive_options.extend( | |
1438 '--reviewer=' + email for email in change_desc.get_reviewers()) | |
1439 | |
1440 git_command = ['push'] | |
1441 if receive_options: | |
1442 git_command.append('--receive-pack=git receive-pack %s' % | |
1443 ' '.join(receive_options)) | |
1444 git_command += [remote, 'HEAD:refs/for/' + branch] | |
1445 RunGit(git_command) | |
1446 # TODO(ukai): parse Change-Id: and set issue number? | |
1447 return 0 | |
1448 | |
1449 | |
1450 def RietveldUpload(options, args, cl): | |
1451 """upload the patch to rietveld.""" | |
1452 upload_args = ['--assume_yes'] # Don't ask about untracked files. | |
1453 upload_args.extend(['--server', cl.GetRietveldServer()]) | |
1454 if options.emulate_svn_auto_props: | |
1455 upload_args.append('--emulate_svn_auto_props') | |
1456 | |
1457 change_desc = None | |
1458 | |
1459 if options.email is not None: | |
1460 upload_args.extend(['--email', options.email]) | |
1461 | |
1462 if cl.GetIssue(): | |
1463 if options.title: | |
1464 upload_args.extend(['--title', options.title]) | |
1465 if options.message: | |
1466 upload_args.extend(['--message', options.message]) | |
1467 upload_args.extend(['--issue', str(cl.GetIssue())]) | |
1468 print ("This branch is associated with issue %s. " | |
1469 "Adding patch to that issue." % cl.GetIssue()) | |
1470 else: | |
1471 if options.title: | |
1472 upload_args.extend(['--title', options.title]) | |
1473 message = options.title or options.message or CreateDescriptionFromLog(args) | |
1474 change_desc = ChangeDescription(message) | |
1475 if options.reviewers: | |
1476 change_desc.update_reviewers(options.reviewers) | |
1477 if not options.force: | |
1478 change_desc.prompt() | |
1479 | |
1480 if not change_desc.description: | |
1481 print "Description is empty; aborting." | |
1482 return 1 | |
1483 | |
1484 upload_args.extend(['--message', change_desc.description]) | |
1485 if change_desc.get_reviewers(): | |
1486 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers())) | |
1487 if options.send_mail: | |
1488 if not change_desc.get_reviewers(): | |
1489 DieWithError("Must specify reviewers to send email.") | |
1490 upload_args.append('--send_mail') | |
1491 | |
1492 # We check this before applying rietveld.private assuming that in | |
1493 # rietveld.cc only addresses which we can send private CLs to are listed | |
1494 # if rietveld.private is set, and so we should ignore rietveld.cc only when | |
1495 # --private is specified explicitly on the command line. | |
1496 if options.private: | |
1497 logging.warn('rietveld.cc is ignored since private flag is specified. ' | |
1498 'You need to review and add them manually if necessary.') | |
1499 cc = cl.GetCCListWithoutDefault() | |
1500 else: | |
1501 cc = cl.GetCCList() | |
1502 cc = ','.join(filter(None, (cc, ','.join(options.cc)))) | |
1503 if cc: | |
1504 upload_args.extend(['--cc', cc]) | |
1505 | |
1506 if options.private or settings.GetDefaultPrivateFlag() == "True": | |
1507 upload_args.append('--private') | |
1508 | |
1509 upload_args.extend(['--git_similarity', str(options.similarity)]) | |
1510 if not options.find_copies: | |
1511 upload_args.extend(['--git_no_find_copies']) | |
1512 | |
1513 # Include the upstream repo's URL in the change -- this is useful for | |
1514 # projects that have their source spread across multiple repos. | |
1515 remote_url = cl.GetGitBaseUrlFromConfig() | |
1516 if not remote_url: | |
1517 if settings.GetIsGitSvn(): | |
1518 # URL is dependent on the current directory. | |
1519 data = RunGit(['svn', 'info'], cwd=settings.GetRoot()) | |
1520 if data: | |
1521 keys = dict(line.split(': ', 1) for line in data.splitlines() | |
1522 if ': ' in line) | |
1523 remote_url = keys.get('URL', None) | |
1524 else: | |
1525 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch(): | |
1526 remote_url = (cl.GetRemoteUrl() + '@' | |
1527 + cl.GetUpstreamBranch().split('/')[-1]) | |
1528 if remote_url: | |
1529 upload_args.extend(['--base_url', remote_url]) | |
1530 | |
1531 try: | |
1532 upload_args = ['upload'] + upload_args + args | |
1533 logging.info('upload.RealMain(%s)', upload_args) | |
1534 issue, patchset = upload.RealMain(upload_args) | |
1535 issue = int(issue) | |
1536 patchset = int(patchset) | |
1537 except KeyboardInterrupt: | |
1538 sys.exit(1) | |
1539 except: | |
1540 # If we got an exception after the user typed a description for their | |
1541 # change, back up the description before re-raising. | |
1542 if change_desc: | |
1543 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE) | |
1544 print '\nGot exception while uploading -- saving description to %s\n' \ | |
1545 % backup_path | |
1546 backup_file = open(backup_path, 'w') | |
1547 backup_file.write(change_desc.description) | |
1548 backup_file.close() | |
1549 raise | |
1550 | |
1551 if not cl.GetIssue(): | |
1552 cl.SetIssue(issue) | |
1553 cl.SetPatchset(patchset) | |
1554 | |
1555 if options.use_commit_queue: | |
1556 cl.SetFlag('commit', '1') | |
1557 return 0 | |
1558 | |
1559 | |
1560 def cleanup_list(l): | |
1561 """Fixes a list so that comma separated items are put as individual items. | |
1562 | |
1563 So that "--reviewers joe@c,john@c --reviewers joa@c" results in | |
1564 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']). | |
1565 """ | |
1566 items = sum((i.split(',') for i in l), []) | |
1567 stripped_items = (i.strip() for i in items) | |
1568 return sorted(filter(None, stripped_items)) | |
1569 | |
1570 | |
1571 @subcommand.usage('[args to "git diff"]') | |
1572 def CMDupload(parser, args): | |
1573 """Uploads the current changelist to codereview.""" | |
1574 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', | |
1575 help='bypass upload presubmit hook') | |
1576 parser.add_option('--bypass-watchlists', action='store_true', | |
1577 dest='bypass_watchlists', | |
1578 help='bypass watchlists auto CC-ing reviewers') | |
1579 parser.add_option('-f', action='store_true', dest='force', | |
1580 help="force yes to questions (don't prompt)") | |
1581 parser.add_option('-m', dest='message', help='message for patchset') | |
1582 parser.add_option('-t', dest='title', help='title for patchset') | |
1583 parser.add_option('-r', '--reviewers', | |
1584 action='append', default=[], | |
1585 help='reviewer email addresses') | |
1586 parser.add_option('--cc', | |
1587 action='append', default=[], | |
1588 help='cc email addresses') | |
1589 parser.add_option('-s', '--send-mail', action='store_true', | |
1590 help='send email to reviewer immediately') | |
1591 parser.add_option("--emulate_svn_auto_props", action="store_true", | |
1592 dest="emulate_svn_auto_props", | |
1593 help="Emulate Subversion's auto properties feature.") | |
1594 parser.add_option('-c', '--use-commit-queue', action='store_true', | |
1595 help='tell the commit queue to commit this patchset') | |
1596 parser.add_option('--private', action='store_true', | |
1597 help='set the review private (rietveld only)') | |
1598 parser.add_option('--target_branch', | |
1599 help='When uploading to gerrit, remote branch to ' | |
1600 'use for CL. Default: master') | |
1601 parser.add_option('--email', default=None, | |
1602 help='email address to use to connect to Rietveld') | |
1603 | |
1604 add_git_similarity(parser) | |
1605 (options, args) = parser.parse_args(args) | |
1606 | |
1607 if options.target_branch and not settings.GetIsGerrit(): | |
1608 parser.error('Use --target_branch for non gerrit repository.') | |
1609 | |
1610 if is_dirty_git_tree('upload'): | |
1611 return 1 | |
1612 | |
1613 options.reviewers = cleanup_list(options.reviewers) | |
1614 options.cc = cleanup_list(options.cc) | |
1615 | |
1616 cl = Changelist() | |
1617 if args: | |
1618 # TODO(ukai): is it ok for gerrit case? | |
1619 base_branch = args[0] | |
1620 else: | |
1621 # Default to diffing against common ancestor of upstream branch | |
1622 base_branch = cl.GetCommonAncestorWithUpstream() | |
1623 args = [base_branch, 'HEAD'] | |
1624 | |
1625 # Apply watchlists on upload. | |
1626 change = cl.GetChange(base_branch, None) | |
1627 watchlist = watchlists.Watchlists(change.RepositoryRoot()) | |
1628 files = [f.LocalPath() for f in change.AffectedFiles()] | |
1629 if not options.bypass_watchlists: | |
1630 cl.SetWatchers(watchlist.GetWatchersForPaths(files)) | |
1631 | |
1632 if not options.bypass_hooks: | |
1633 hook_results = cl.RunHook(committing=False, | |
1634 may_prompt=not options.force, | |
1635 verbose=options.verbose, | |
1636 change=change) | |
1637 if not hook_results.should_continue(): | |
1638 return 1 | |
1639 if not options.reviewers and hook_results.reviewers: | |
1640 options.reviewers = hook_results.reviewers.split(',') | |
1641 | |
1642 if cl.GetIssue(): | |
1643 latest_patchset = cl.GetMostRecentPatchset() | |
1644 local_patchset = cl.GetPatchset() | |
1645 if latest_patchset and local_patchset and local_patchset != latest_patchset: | |
1646 print ('The last upload made from this repository was patchset #%d but ' | |
1647 'the most recent patchset on the server is #%d.' | |
1648 % (local_patchset, latest_patchset)) | |
1649 print ('Uploading will still work, but if you\'ve uploaded to this issue ' | |
1650 'from another machine or branch the patch you\'re uploading now ' | |
1651 'might not include those changes.') | |
1652 ask_for_data('About to upload; enter to confirm.') | |
1653 | |
1654 print_stats(options.similarity, options.find_copies, args) | |
1655 if settings.GetIsGerrit(): | |
1656 return GerritUpload(options, args, cl) | |
1657 ret = RietveldUpload(options, args, cl) | |
1658 if not ret: | |
1659 git_set_branch_value('last-upload-hash', | |
1660 RunGit(['rev-parse', 'HEAD']).strip()) | |
1661 | |
1662 return ret | |
1663 | |
1664 | |
1665 def IsSubmoduleMergeCommit(ref): | |
1666 # When submodules are added to the repo, we expect there to be a single | |
1667 # non-git-svn merge commit at remote HEAD with a signature comment. | |
1668 pattern = '^SVN changes up to revision [0-9]*$' | |
1669 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref] | |
1670 return RunGit(cmd) != '' | |
1671 | |
1672 | |
1673 def SendUpstream(parser, args, cmd): | |
1674 """Common code for CmdPush and CmdDCommit | |
1675 | |
1676 Squashed commit into a single. | |
1677 Updates changelog with metadata (e.g. pointer to review). | |
1678 Pushes/dcommits the code upstream. | |
1679 Updates review and closes. | |
1680 """ | |
1681 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks', | |
1682 help='bypass upload presubmit hook') | |
1683 parser.add_option('-m', dest='message', | |
1684 help="override review description") | |
1685 parser.add_option('-f', action='store_true', dest='force', | |
1686 help="force yes to questions (don't prompt)") | |
1687 parser.add_option('-c', dest='contributor', | |
1688 help="external contributor for patch (appended to " + | |
1689 "description and used as author for git). Should be " + | |
1690 "formatted as 'First Last <email@example.com>'") | |
1691 add_git_similarity(parser) | |
1692 (options, args) = parser.parse_args(args) | |
1693 cl = Changelist() | |
1694 | |
1695 if not args or cmd == 'push': | |
1696 # Default to merging against our best guess of the upstream branch. | |
1697 args = [cl.GetUpstreamBranch()] | |
1698 | |
1699 if options.contributor: | |
1700 if not re.match('^.*\s<\S+@\S+>$', options.contributor): | |
1701 print "Please provide contibutor as 'First Last <email@example.com>'" | |
1702 return 1 | |
1703 | |
1704 base_branch = args[0] | |
1705 base_has_submodules = IsSubmoduleMergeCommit(base_branch) | |
1706 | |
1707 if is_dirty_git_tree(cmd): | |
1708 return 1 | |
1709 | |
1710 # This rev-list syntax means "show all commits not in my branch that | |
1711 # are in base_branch". | |
1712 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(), | |
1713 base_branch]).splitlines() | |
1714 if upstream_commits: | |
1715 print ('Base branch "%s" has %d commits ' | |
1716 'not in this branch.' % (base_branch, len(upstream_commits))) | |
1717 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd) | |
1718 return 1 | |
1719 | |
1720 # This is the revision `svn dcommit` will commit on top of. | |
1721 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1', | |
1722 '--pretty=format:%H']) | |
1723 | |
1724 if cmd == 'dcommit': | |
1725 # If the base_head is a submodule merge commit, the first parent of the | |
1726 # base_head should be a git-svn commit, which is what we're interested in. | |
1727 base_svn_head = base_branch | |
1728 if base_has_submodules: | |
1729 base_svn_head += '^1' | |
1730 | |
1731 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head]) | |
1732 if extra_commits: | |
1733 print ('This branch has %d additional commits not upstreamed yet.' | |
1734 % len(extra_commits.splitlines())) | |
1735 print ('Upstream "%s" or rebase this branch on top of the upstream trunk ' | |
1736 'before attempting to %s.' % (base_branch, cmd)) | |
1737 return 1 | |
1738 | |
1739 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip() | |
1740 if not options.bypass_hooks: | |
1741 author = None | |
1742 if options.contributor: | |
1743 author = re.search(r'\<(.*)\>', options.contributor).group(1) | |
1744 hook_results = cl.RunHook( | |
1745 committing=True, | |
1746 may_prompt=not options.force, | |
1747 verbose=options.verbose, | |
1748 change=cl.GetChange(base_branch, author)) | |
1749 if not hook_results.should_continue(): | |
1750 return 1 | |
1751 | |
1752 if cmd == 'dcommit': | |
1753 # Check the tree status if the tree status URL is set. | |
1754 status = GetTreeStatus() | |
1755 if 'closed' == status: | |
1756 print('The tree is closed. Please wait for it to reopen. Use ' | |
1757 '"git cl dcommit --bypass-hooks" to commit on a closed tree.') | |
1758 return 1 | |
1759 elif 'unknown' == status: | |
1760 print('Unable to determine tree status. Please verify manually and ' | |
1761 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.') | |
1762 else: | |
1763 breakpad.SendStack( | |
1764 'GitClHooksBypassedCommit', | |
1765 'Issue %s/%s bypassed hook when committing (tree status was "%s")' % | |
1766 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()), | |
1767 verbose=False) | |
1768 | |
1769 change_desc = ChangeDescription(options.message) | |
1770 if not change_desc.description and cl.GetIssue(): | |
1771 change_desc = ChangeDescription(cl.GetDescription()) | |
1772 | |
1773 if not change_desc.description: | |
1774 if not cl.GetIssue() and options.bypass_hooks: | |
1775 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch])) | |
1776 else: | |
1777 print 'No description set.' | |
1778 print 'Visit %s/edit to set it.' % (cl.GetIssueURL()) | |
1779 return 1 | |
1780 | |
1781 # Keep a separate copy for the commit message, because the commit message | |
1782 # contains the link to the Rietveld issue, while the Rietveld message contains | |
1783 # the commit viewvc url. | |
1784 # Keep a separate copy for the commit message. | |
1785 if cl.GetIssue(): | |
1786 change_desc.update_reviewers(cl.GetApprovingReviewers()) | |
1787 | |
1788 commit_desc = ChangeDescription(change_desc.description) | |
1789 if cl.GetIssue(): | |
1790 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL()) | |
1791 if options.contributor: | |
1792 commit_desc.append_footer('Patch from %s.' % options.contributor) | |
1793 | |
1794 print('Description:') | |
1795 print(commit_desc.description) | |
1796 | |
1797 branches = [base_branch, cl.GetBranchRef()] | |
1798 if not options.force: | |
1799 print_stats(options.similarity, options.find_copies, branches) | |
1800 ask_for_data('About to commit; enter to confirm.') | |
1801 | |
1802 # We want to squash all this branch's commits into one commit with the proper | |
1803 # description. We do this by doing a "reset --soft" to the base branch (which | |
1804 # keeps the working copy the same), then dcommitting that. If origin/master | |
1805 # has a submodule merge commit, we'll also need to cherry-pick the squashed | |
1806 # commit onto a branch based on the git-svn head. | |
1807 MERGE_BRANCH = 'git-cl-commit' | |
1808 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick' | |
1809 # Delete the branches if they exist. | |
1810 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]: | |
1811 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch] | |
1812 result = RunGitWithCode(showref_cmd) | |
1813 if result[0] == 0: | |
1814 RunGit(['branch', '-D', branch]) | |
1815 | |
1816 # We might be in a directory that's present in this branch but not in the | |
1817 # trunk. Move up to the top of the tree so that git commands that expect a | |
1818 # valid CWD won't fail after we check out the merge branch. | |
1819 rel_base_path = settings.GetRelativeRoot() | |
1820 if rel_base_path: | |
1821 os.chdir(rel_base_path) | |
1822 | |
1823 # Stuff our change into the merge branch. | |
1824 # We wrap in a try...finally block so if anything goes wrong, | |
1825 # we clean up the branches. | |
1826 retcode = -1 | |
1827 try: | |
1828 RunGit(['checkout', '-q', '-b', MERGE_BRANCH]) | |
1829 RunGit(['reset', '--soft', base_branch]) | |
1830 if options.contributor: | |
1831 RunGit( | |
1832 [ | |
1833 'commit', '--author', options.contributor, | |
1834 '-m', commit_desc.description, | |
1835 ]) | |
1836 else: | |
1837 RunGit(['commit', '-m', commit_desc.description]) | |
1838 if base_has_submodules: | |
1839 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip() | |
1840 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head]) | |
1841 RunGit(['checkout', CHERRY_PICK_BRANCH]) | |
1842 RunGit(['cherry-pick', cherry_pick_commit]) | |
1843 if cmd == 'push': | |
1844 # push the merge branch. | |
1845 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch()) | |
1846 retcode, output = RunGitWithCode( | |
1847 ['push', '--porcelain', remote, 'HEAD:%s' % branch]) | |
1848 logging.debug(output) | |
1849 else: | |
1850 # dcommit the merge branch. | |
1851 retcode, output = RunGitWithCode(['svn', 'dcommit', | |
1852 '-C%s' % options.similarity, | |
1853 '--no-rebase', '--rmdir']) | |
1854 finally: | |
1855 # And then swap back to the original branch and clean up. | |
1856 RunGit(['checkout', '-q', cl.GetBranch()]) | |
1857 RunGit(['branch', '-D', MERGE_BRANCH]) | |
1858 if base_has_submodules: | |
1859 RunGit(['branch', '-D', CHERRY_PICK_BRANCH]) | |
1860 | |
1861 if cl.GetIssue(): | |
1862 if cmd == 'dcommit' and 'Committed r' in output: | |
1863 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1) | |
1864 elif cmd == 'push' and retcode == 0: | |
1865 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l) | |
1866 for l in output.splitlines(False)) | |
1867 match = filter(None, match) | |
1868 if len(match) != 1: | |
1869 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" % | |
1870 output) | |
1871 revision = match[0].group(2) | |
1872 else: | |
1873 return 1 | |
1874 viewvc_url = settings.GetViewVCUrl() | |
1875 if viewvc_url and revision: | |
1876 change_desc.append_footer('Committed: ' + viewvc_url + revision) | |
1877 elif revision: | |
1878 change_desc.append_footer('Committed: ' + revision) | |
1879 print ('Closing issue ' | |
1880 '(you may be prompted for your codereview password)...') | |
1881 cl.UpdateDescription(change_desc.description) | |
1882 cl.CloseIssue() | |
1883 props = cl.GetIssueProperties() | |
1884 patch_num = len(props['patchsets']) | |
1885 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision) | |
1886 if options.bypass_hooks: | |
1887 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.' | |
1888 else: | |
1889 comment += ' (presubmit successful).' | |
1890 cl.RpcServer().add_comment(cl.GetIssue(), comment) | |
1891 cl.SetIssue(None) | |
1892 | |
1893 if retcode == 0: | |
1894 hook = POSTUPSTREAM_HOOK_PATTERN % cmd | |
1895 if os.path.isfile(hook): | |
1896 RunCommand([hook, base_branch], error_ok=True) | |
1897 | |
1898 return 0 | |
1899 | |
1900 | |
1901 @subcommand.usage('[upstream branch to apply against]') | |
1902 def CMDdcommit(parser, args): | |
1903 """Commits the current changelist via git-svn.""" | |
1904 if not settings.GetIsGitSvn(): | |
1905 message = """This doesn't appear to be an SVN repository. | |
1906 If your project has a git mirror with an upstream SVN master, you probably need | |
1907 to run 'git svn init', see your project's git mirror documentation. | |
1908 If your project has a true writeable upstream repository, you probably want | |
1909 to run 'git cl push' instead. | |
1910 Choose wisely, if you get this wrong, your commit might appear to succeed but | |
1911 will instead be silently ignored.""" | |
1912 print(message) | |
1913 ask_for_data('[Press enter to dcommit or ctrl-C to quit]') | |
1914 return SendUpstream(parser, args, 'dcommit') | |
1915 | |
1916 | |
1917 @subcommand.usage('[upstream branch to apply against]') | |
1918 def CMDpush(parser, args): | |
1919 """Commits the current changelist via git.""" | |
1920 if settings.GetIsGitSvn(): | |
1921 print('This appears to be an SVN repository.') | |
1922 print('Are you sure you didn\'t mean \'git cl dcommit\'?') | |
1923 ask_for_data('[Press enter to push or ctrl-C to quit]') | |
1924 return SendUpstream(parser, args, 'push') | |
1925 | |
1926 | |
1927 @subcommand.usage('<patch url or issue id>') | |
1928 def CMDpatch(parser, args): | |
1929 """Patches in a code review.""" | |
1930 parser.add_option('-b', dest='newbranch', | |
1931 help='create a new branch off trunk for the patch') | |
1932 parser.add_option('-f', '--force', action='store_true', | |
1933 help='with -b, clobber any existing branch') | |
1934 parser.add_option('-d', '--directory', action='store', metavar='DIR', | |
1935 help='Change to the directory DIR immediately, ' | |
1936 'before doing anything else.') | |
1937 parser.add_option('--reject', action='store_true', | |
1938 help='failed patches spew .rej files rather than ' | |
1939 'attempting a 3-way merge') | |
1940 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit', | |
1941 help="don't commit after patch applies") | |
1942 (options, args) = parser.parse_args(args) | |
1943 if len(args) != 1: | |
1944 parser.print_help() | |
1945 return 1 | |
1946 issue_arg = args[0] | |
1947 | |
1948 # TODO(maruel): Use apply_issue.py | |
1949 # TODO(ukai): use gerrit-cherry-pick for gerrit repository? | |
1950 | |
1951 if options.newbranch: | |
1952 if options.force: | |
1953 RunGit(['branch', '-D', options.newbranch], | |
1954 stderr=subprocess2.PIPE, error_ok=True) | |
1955 RunGit(['checkout', '-b', options.newbranch, | |
1956 Changelist().GetUpstreamBranch()]) | |
1957 | |
1958 return PatchIssue(issue_arg, options.reject, options.nocommit, | |
1959 options.directory) | |
1960 | |
1961 | |
1962 def PatchIssue(issue_arg, reject, nocommit, directory): | |
1963 if type(issue_arg) is int or issue_arg.isdigit(): | |
1964 # Input is an issue id. Figure out the URL. | |
1965 issue = int(issue_arg) | |
1966 cl = Changelist(issue=issue) | |
1967 patchset = cl.GetMostRecentPatchset() | |
1968 patch_data = cl.GetPatchSetDiff(issue, patchset) | |
1969 else: | |
1970 # Assume it's a URL to the patch. Default to https. | |
1971 issue_url = gclient_utils.UpgradeToHttps(issue_arg) | |
1972 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url) | |
1973 if not match: | |
1974 DieWithError('Must pass an issue ID or full URL for ' | |
1975 '\'Download raw patch set\'') | |
1976 issue = int(match.group(1)) | |
1977 patchset = int(match.group(2)) | |
1978 patch_data = urllib2.urlopen(issue_arg).read() | |
1979 | |
1980 # Switch up to the top-level directory, if necessary, in preparation for | |
1981 # applying the patch. | |
1982 top = settings.GetRelativeRoot() | |
1983 if top: | |
1984 os.chdir(top) | |
1985 | |
1986 # Git patches have a/ at the beginning of source paths. We strip that out | |
1987 # with a sed script rather than the -p flag to patch so we can feed either | |
1988 # Git or svn-style patches into the same apply command. | |
1989 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7. | |
1990 try: | |
1991 patch_data = subprocess2.check_output( | |
1992 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data) | |
1993 except subprocess2.CalledProcessError: | |
1994 DieWithError('Git patch mungling failed.') | |
1995 logging.info(patch_data) | |
1996 | |
1997 # We use "git apply" to apply the patch instead of "patch" so that we can | |
1998 # pick up file adds. | |
1999 # The --index flag means: also insert into the index (so we catch adds). | |
2000 cmd = ['git', 'apply', '--index', '-p0'] | |
2001 if directory: | |
2002 cmd.extend(('--directory', directory)) | |
2003 if reject: | |
2004 cmd.append('--reject') | |
2005 elif IsGitVersionAtLeast('1.7.12'): | |
2006 cmd.append('--3way') | |
2007 try: | |
2008 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(), | |
2009 stdin=patch_data, stdout=subprocess2.VOID) | |
2010 except subprocess2.CalledProcessError: | |
2011 DieWithError('Failed to apply the patch') | |
2012 | |
2013 # If we had an issue, commit the current state and register the issue. | |
2014 if not nocommit: | |
2015 RunGit(['commit', '-m', 'patch from issue %s' % issue]) | |
2016 cl = Changelist() | |
2017 cl.SetIssue(issue) | |
2018 cl.SetPatchset(patchset) | |
2019 print "Committed patch locally." | |
2020 else: | |
2021 print "Patch applied to index." | |
2022 return 0 | |
2023 | |
2024 | |
2025 def CMDrebase(parser, args): | |
2026 """Rebases current branch on top of svn repo.""" | |
2027 # Provide a wrapper for git svn rebase to help avoid accidental | |
2028 # git svn dcommit. | |
2029 # It's the only command that doesn't use parser at all since we just defer | |
2030 # execution to git-svn. | |
2031 | |
2032 return RunGitWithCode(['svn', 'rebase'] + args)[1] | |
2033 | |
2034 | |
2035 def GetTreeStatus(url=None): | |
2036 """Fetches the tree status and returns either 'open', 'closed', | |
2037 'unknown' or 'unset'.""" | |
2038 url = url or settings.GetTreeStatusUrl(error_ok=True) | |
2039 if url: | |
2040 status = urllib2.urlopen(url).read().lower() | |
2041 if status.find('closed') != -1 or status == '0': | |
2042 return 'closed' | |
2043 elif status.find('open') != -1 or status == '1': | |
2044 return 'open' | |
2045 return 'unknown' | |
2046 return 'unset' | |
2047 | |
2048 | |
2049 def GetTreeStatusReason(): | |
2050 """Fetches the tree status from a json url and returns the message | |
2051 with the reason for the tree to be opened or closed.""" | |
2052 url = settings.GetTreeStatusUrl() | |
2053 json_url = urlparse.urljoin(url, '/current?format=json') | |
2054 connection = urllib2.urlopen(json_url) | |
2055 status = json.loads(connection.read()) | |
2056 connection.close() | |
2057 return status['message'] | |
2058 | |
2059 | |
2060 def CMDtree(parser, args): | |
2061 """Shows the status of the tree.""" | |
2062 _, args = parser.parse_args(args) | |
2063 status = GetTreeStatus() | |
2064 if 'unset' == status: | |
2065 print 'You must configure your tree status URL by running "git cl config".' | |
2066 return 2 | |
2067 | |
2068 print "The tree is %s" % status | |
2069 print | |
2070 print GetTreeStatusReason() | |
2071 if status != 'open': | |
2072 return 1 | |
2073 return 0 | |
2074 | |
2075 | |
2076 def CMDtry(parser, args): | |
2077 """Triggers a try job through Rietveld.""" | |
2078 group = optparse.OptionGroup(parser, "Try job options") | |
2079 group.add_option( | |
2080 "-b", "--bot", action="append", | |
2081 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple " | |
2082 "times to specify multiple builders. ex: " | |
2083 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See " | |
2084 "the try server waterfall for the builders name and the tests " | |
2085 "available. Can also be used to specify gtest_filter, e.g. " | |
2086 "-bwin_rel:base_unittests:ValuesTest.*Value")) | |
2087 group.add_option( | |
2088 "-m", "--master", default='', | |
2089 help=("Specify a try master where to run the tries.")) | |
2090 group.add_option( | |
2091 "-r", "--revision", | |
2092 help="Revision to use for the try job; default: the " | |
2093 "revision will be determined by the try server; see " | |
2094 "its waterfall for more info") | |
2095 group.add_option( | |
2096 "-c", "--clobber", action="store_true", default=False, | |
2097 help="Force a clobber before building; e.g. don't do an " | |
2098 "incremental build") | |
2099 group.add_option( | |
2100 "--project", | |
2101 help="Override which project to use. Projects are defined " | |
2102 "server-side to define what default bot set to use") | |
2103 group.add_option( | |
2104 "-t", "--testfilter", action="append", default=[], | |
2105 help=("Apply a testfilter to all the selected builders. Unless the " | |
2106 "builders configurations are similar, use multiple " | |
2107 "--bot <builder>:<test> arguments.")) | |
2108 group.add_option( | |
2109 "-n", "--name", help="Try job name; default to current branch name") | |
2110 parser.add_option_group(group) | |
2111 options, args = parser.parse_args(args) | |
2112 | |
2113 if args: | |
2114 parser.error('Unknown arguments: %s' % args) | |
2115 | |
2116 cl = Changelist() | |
2117 if not cl.GetIssue(): | |
2118 parser.error('Need to upload first') | |
2119 | |
2120 if not options.name: | |
2121 options.name = cl.GetBranch() | |
2122 | |
2123 def GetMasterMap(): | |
2124 # Process --bot and --testfilter. | |
2125 if not options.bot: | |
2126 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None) | |
2127 | |
2128 # Get try masters from PRESUBMIT.py files. | |
2129 masters = presubmit_support.DoGetTryMasters( | |
2130 change, | |
2131 change.LocalPaths(), | |
2132 settings.GetRoot(), | |
2133 None, | |
2134 None, | |
2135 options.verbose, | |
2136 sys.stdout) | |
2137 if masters: | |
2138 return masters | |
2139 | |
2140 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files. | |
2141 options.bot = presubmit_support.DoGetTrySlaves( | |
2142 change, | |
2143 change.LocalPaths(), | |
2144 settings.GetRoot(), | |
2145 None, | |
2146 None, | |
2147 options.verbose, | |
2148 sys.stdout) | |
2149 if not options.bot: | |
2150 parser.error('No default try builder to try, use --bot') | |
2151 | |
2152 builders_and_tests = {} | |
2153 # TODO(machenbach): The old style command-line options don't support | |
2154 # multiple try masters yet. | |
2155 old_style = filter(lambda x: isinstance(x, basestring), options.bot) | |
2156 new_style = filter(lambda x: isinstance(x, tuple), options.bot) | |
2157 | |
2158 for bot in old_style: | |
2159 if ':' in bot: | |
2160 builder, tests = bot.split(':', 1) | |
2161 builders_and_tests.setdefault(builder, []).extend(tests.split(',')) | |
2162 elif ',' in bot: | |
2163 parser.error('Specify one bot per --bot flag') | |
2164 else: | |
2165 builders_and_tests.setdefault(bot, []).append('defaulttests') | |
2166 | |
2167 for bot, tests in new_style: | |
2168 builders_and_tests.setdefault(bot, []).extend(tests) | |
2169 | |
2170 # Return a master map with one master to be backwards compatible. The | |
2171 # master name defaults to an empty string, which will cause the master | |
2172 # not to be set on rietveld (deprecated). | |
2173 return {options.master: builders_and_tests} | |
2174 | |
2175 masters = GetMasterMap() | |
2176 | |
2177 if options.testfilter: | |
2178 forced_tests = sum((t.split(',') for t in options.testfilter), []) | |
2179 masters = dict((master, dict( | |
2180 (b, forced_tests) for b, t in slaves.iteritems() | |
2181 if t != ['compile'])) for master, slaves in masters.iteritems()) | |
2182 | |
2183 for builders in masters.itervalues(): | |
2184 if any('triggered' in b for b in builders): | |
2185 print >> sys.stderr, ( | |
2186 'ERROR You are trying to send a job to a triggered bot. This type of' | |
2187 ' bot requires an\ninitial job from a parent (usually a builder). ' | |
2188 'Instead send your job to the parent.\n' | |
2189 'Bot list: %s' % builders) | |
2190 return 1 | |
2191 | |
2192 patchset = cl.GetMostRecentPatchset() | |
2193 if patchset and patchset != cl.GetPatchset(): | |
2194 print( | |
2195 '\nWARNING Mismatch between local config and server. Did a previous ' | |
2196 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' | |
2197 'Continuing using\npatchset %s.\n' % patchset) | |
2198 try: | |
2199 cl.RpcServer().trigger_distributed_try_jobs( | |
2200 cl.GetIssue(), patchset, options.name, options.clobber, | |
2201 options.revision, masters) | |
2202 except urllib2.HTTPError, e: | |
2203 if e.code == 404: | |
2204 print('404 from rietveld; ' | |
2205 'did you mean to use "git try" instead of "git cl try"?') | |
2206 return 1 | |
2207 print('Tried jobs on:') | |
2208 | |
2209 for (master, builders) in masters.iteritems(): | |
2210 if master: | |
2211 print 'Master: %s' % master | |
2212 length = max(len(builder) for builder in builders) | |
2213 for builder in sorted(builders): | |
2214 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | |
2215 return 0 | |
2216 | |
2217 | |
2218 @subcommand.usage('[new upstream branch]') | |
2219 def CMDupstream(parser, args): | |
2220 """Prints or sets the name of the upstream branch, if any.""" | |
2221 _, args = parser.parse_args(args) | |
2222 if len(args) > 1: | |
2223 parser.error('Unrecognized args: %s' % ' '.join(args)) | |
2224 | |
2225 cl = Changelist() | |
2226 if args: | |
2227 # One arg means set upstream branch. | |
2228 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]]) | |
2229 cl = Changelist() | |
2230 print "Upstream branch set to " + cl.GetUpstreamBranch() | |
2231 else: | |
2232 print cl.GetUpstreamBranch() | |
2233 return 0 | |
2234 | |
2235 | |
2236 def CMDweb(parser, args): | |
2237 """Opens the current CL in the web browser.""" | |
2238 _, args = parser.parse_args(args) | |
2239 if args: | |
2240 parser.error('Unrecognized args: %s' % ' '.join(args)) | |
2241 | |
2242 issue_url = Changelist().GetIssueURL() | |
2243 if not issue_url: | |
2244 print >> sys.stderr, 'ERROR No issue to open' | |
2245 return 1 | |
2246 | |
2247 webbrowser.open(issue_url) | |
2248 return 0 | |
2249 | |
2250 | |
2251 def CMDset_commit(parser, args): | |
2252 """Sets the commit bit to trigger the Commit Queue.""" | |
2253 _, args = parser.parse_args(args) | |
2254 if args: | |
2255 parser.error('Unrecognized args: %s' % ' '.join(args)) | |
2256 cl = Changelist() | |
2257 cl.SetFlag('commit', '1') | |
2258 return 0 | |
2259 | |
2260 | |
2261 def CMDset_close(parser, args): | |
2262 """Closes the issue.""" | |
2263 _, args = parser.parse_args(args) | |
2264 if args: | |
2265 parser.error('Unrecognized args: %s' % ' '.join(args)) | |
2266 cl = Changelist() | |
2267 # Ensure there actually is an issue to close. | |
2268 cl.GetDescription() | |
2269 cl.CloseIssue() | |
2270 return 0 | |
2271 | |
2272 | |
2273 def CMDdiff(parser, args): | |
2274 """shows differences between local tree and last upload.""" | |
2275 cl = Changelist() | |
2276 issue = cl.GetIssue() | |
2277 branch = cl.GetBranch() | |
2278 if not issue: | |
2279 DieWithError('No issue found for current branch (%s)' % branch) | |
2280 TMP_BRANCH = 'git-cl-diff' | |
2281 base_branch = cl.GetCommonAncestorWithUpstream() | |
2282 | |
2283 # Create a new branch based on the merge-base | |
2284 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch]) | |
2285 try: | |
2286 # Patch in the latest changes from rietveld. | |
2287 rtn = PatchIssue(issue, False, False, None) | |
2288 if rtn != 0: | |
2289 return rtn | |
2290 | |
2291 # Switch back to starting brand and diff against the temporary | |
2292 # branch containing the latest rietveld patch. | |
2293 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch]) | |
2294 finally: | |
2295 RunGit(['checkout', '-q', branch]) | |
2296 RunGit(['branch', '-D', TMP_BRANCH]) | |
2297 | |
2298 return 0 | |
2299 | |
2300 | |
2301 def CMDowners(parser, args): | |
2302 """interactively find the owners for reviewing""" | |
2303 parser.add_option( | |
2304 '--no-color', | |
2305 action='store_true', | |
2306 help='Use this option to disable color output') | |
2307 options, args = parser.parse_args(args) | |
2308 | |
2309 author = RunGit(['config', 'user.email']).strip() or None | |
2310 | |
2311 cl = Changelist() | |
2312 | |
2313 if args: | |
2314 if len(args) > 1: | |
2315 parser.error('Unknown args') | |
2316 base_branch = args[0] | |
2317 else: | |
2318 # Default to diffing against the common ancestor of the upstream branch. | |
2319 base_branch = cl.GetCommonAncestorWithUpstream() | |
2320 | |
2321 change = cl.GetChange(base_branch, None) | |
2322 return owners_finder.OwnersFinder( | |
2323 [f.LocalPath() for f in | |
2324 cl.GetChange(base_branch, None).AffectedFiles()], | |
2325 change.RepositoryRoot(), author, | |
2326 fopen=file, os_path=os.path, glob=glob.glob, | |
2327 disable_color=options.no_color).run() | |
2328 | |
2329 | |
2330 @subcommand.usage('[files or directories to diff]') | |
2331 def CMDformat(parser, args): | |
2332 """Runs clang-format on the diff.""" | |
2333 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto'] | |
2334 parser.add_option('--full', action='store_true', | |
2335 help='Reformat the full content of all touched files') | |
2336 parser.add_option('--dry-run', action='store_true', | |
2337 help='Don\'t modify any file on disk.') | |
2338 opts, args = parser.parse_args(args) | |
2339 | |
2340 # git diff generates paths against the root of the repository. Change | |
2341 # to that directory so clang-format can find files even within subdirs. | |
2342 rel_base_path = settings.GetRelativeRoot() | |
2343 if rel_base_path: | |
2344 os.chdir(rel_base_path) | |
2345 | |
2346 # Generate diff for the current branch's changes. | |
2347 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix'] | |
2348 if opts.full: | |
2349 # Only list the names of modified files. | |
2350 diff_cmd.append('--name-only') | |
2351 else: | |
2352 # Only generate context-less patches. | |
2353 diff_cmd.append('-U0') | |
2354 | |
2355 # Grab the merge-base commit, i.e. the upstream commit of the current | |
2356 # branch when it was created or the last time it was rebased. This is | |
2357 # to cover the case where the user may have called "git fetch origin", | |
2358 # moving the origin branch to a newer commit, but hasn't rebased yet. | |
2359 upstream_commit = None | |
2360 cl = Changelist() | |
2361 upstream_branch = cl.GetUpstreamBranch() | |
2362 if upstream_branch: | |
2363 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch]) | |
2364 upstream_commit = upstream_commit.strip() | |
2365 | |
2366 if not upstream_commit: | |
2367 DieWithError('Could not find base commit for this branch. ' | |
2368 'Are you in detached state?') | |
2369 | |
2370 diff_cmd.append(upstream_commit) | |
2371 | |
2372 # Handle source file filtering. | |
2373 diff_cmd.append('--') | |
2374 if args: | |
2375 for arg in args: | |
2376 if os.path.isdir(arg): | |
2377 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS] | |
2378 elif os.path.isfile(arg): | |
2379 diff_cmd.append(arg) | |
2380 else: | |
2381 DieWithError('Argument "%s" is not a file or a directory' % arg) | |
2382 else: | |
2383 diff_cmd += ['*' + ext for ext in CLANG_EXTS] | |
2384 diff_output = RunGit(diff_cmd) | |
2385 | |
2386 top_dir = os.path.normpath( | |
2387 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')) | |
2388 | |
2389 # Locate the clang-format binary in the checkout | |
2390 try: | |
2391 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree() | |
2392 except clang_format.NotFoundError, e: | |
2393 DieWithError(e) | |
2394 | |
2395 if opts.full: | |
2396 # diff_output is a list of files to send to clang-format. | |
2397 files = diff_output.splitlines() | |
2398 if not files: | |
2399 print "Nothing to format." | |
2400 return 0 | |
2401 cmd = [clang_format_tool] | |
2402 if not opts.dry_run: | |
2403 cmd.append('-i') | |
2404 stdout = RunCommand(cmd + files, cwd=top_dir) | |
2405 else: | |
2406 env = os.environ.copy() | |
2407 env['PATH'] = os.path.dirname(clang_format_tool) | |
2408 # diff_output is a patch to send to clang-format-diff.py | |
2409 try: | |
2410 script = clang_format.FindClangFormatScriptInChromiumTree( | |
2411 'clang-format-diff.py') | |
2412 except clang_format.NotFoundError, e: | |
2413 DieWithError(e) | |
2414 | |
2415 cmd = [sys.executable, script, '-p0'] | |
2416 if not opts.dry_run: | |
2417 cmd.append('-i') | |
2418 | |
2419 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env) | |
2420 if opts.dry_run and len(stdout) > 0: | |
2421 return 2 | |
2422 | |
2423 return 0 | |
2424 | |
2425 | |
2426 class OptionParser(optparse.OptionParser): | |
2427 """Creates the option parse and add --verbose support.""" | |
2428 def __init__(self, *args, **kwargs): | |
2429 optparse.OptionParser.__init__( | |
2430 self, *args, prog='git cl', version=__version__, **kwargs) | |
2431 self.add_option( | |
2432 '-v', '--verbose', action='count', default=0, | |
2433 help='Use 2 times for more debugging info') | |
2434 | |
2435 def parse_args(self, args=None, values=None): | |
2436 options, args = optparse.OptionParser.parse_args(self, args, values) | |
2437 levels = [logging.WARNING, logging.INFO, logging.DEBUG] | |
2438 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)]) | |
2439 return options, args | |
2440 | |
2441 | |
2442 def main(argv): | |
2443 if sys.hexversion < 0x02060000: | |
2444 print >> sys.stderr, ( | |
2445 '\nYour python version %s is unsupported, please upgrade.\n' % | |
2446 sys.version.split(' ', 1)[0]) | |
2447 return 2 | |
2448 | |
2449 # Reload settings. | |
2450 global settings | |
2451 settings = Settings() | |
2452 | |
2453 colorize_CMDstatus_doc() | |
2454 dispatcher = subcommand.CommandDispatcher(__name__) | |
2455 try: | |
2456 return dispatcher.execute(OptionParser(), argv) | |
2457 except urllib2.HTTPError, e: | |
2458 if e.code != 500: | |
2459 raise | |
2460 DieWithError( | |
2461 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' | |
2462 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) | |
2463 | |
2464 | |
2465 if __name__ == '__main__': | |
2466 # These affect sys.stdout so do it outside of main() to simplify mocks in | |
2467 # unit testing. | |
2468 fix_encoding.fix_encoding() | |
2469 colorama.init() | |
2470 sys.exit(main(sys.argv[1:])) | |
OLD | NEW |