blob: 01a12a82c816287504037fcbb19ee55047dfab2b [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000038import git_common
39import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000041import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000043import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000049DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000050POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000052GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000053CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054
thestig@chromium.org44202a22014-03-11 19:22:18 +000055# Valid extensions for files we want to lint.
56DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
57DEFAULT_LINT_IGNORE_REGEX = r"$^"
58
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000059# Shortcut since it quickly becomes redundant.
60Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000061
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062# Initialized in main()
63settings = None
64
65
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000067 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 sys.exit(1)
69
70
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000071def GetNoGitPagerEnv():
72 env = os.environ.copy()
73 # 'cat' is a magical git string that disables pagers on all platforms.
74 env['GIT_PAGER'] = 'cat'
75 return env
76
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000077
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000080 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000081 except subprocess2.CalledProcessError as e:
82 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000085 'Command "%s" failed.\n%s' % (
86 ' '.join(args), error_message or e.stdout or ''))
87 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088
89
90def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000091 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000092 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000095def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000097 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098 if suppress_stderr:
99 stderr = subprocess2.VOID
100 else:
101 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000102 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000103 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000104 stdout=subprocess2.PIPE,
105 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000106 return code, out[0]
107 except ValueError:
108 # When the subprocess fails, it returns None. That triggers a ValueError
109 # when trying to unpack the return value into (out, code).
110 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000113def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000114 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000115 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000116 return (version.startswith(prefix) and
117 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000118
119
maruel@chromium.org90541732011-04-01 17:54:18 +0000120def ask_for_data(prompt):
121 try:
122 return raw_input(prompt)
123 except KeyboardInterrupt:
124 # Hide the exception.
125 sys.exit(1)
126
127
iannucci@chromium.org79540052012-10-19 23:15:26 +0000128def git_set_branch_value(key, value):
129 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000130 if not branch:
131 return
132
133 cmd = ['config']
134 if isinstance(value, int):
135 cmd.append('--int')
136 git_key = 'branch.%s.%s' % (branch, key)
137 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138
139
140def git_get_branch_default(key, default):
141 branch = Changelist().GetBranch()
142 if branch:
143 git_key = 'branch.%s.%s' % (branch, key)
144 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
145 try:
146 return int(stdout.strip())
147 except ValueError:
148 pass
149 return default
150
151
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152def add_git_similarity(parser):
153 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 help='Sets the percentage that a pair of files need to match in order to'
156 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 parser.add_option(
158 '--find-copies', action='store_true',
159 help='Allows git to look for copies.')
160 parser.add_option(
161 '--no-find-copies', action='store_false', dest='find_copies',
162 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000163
164 old_parser_args = parser.parse_args
165 def Parse(args):
166 options, args = old_parser_args(args)
167
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000170 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171 print('Note: Saving similarity of %d%% in git config.'
172 % options.similarity)
173 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000174
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 options.similarity = max(0, min(options.similarity, 100))
176
177 if options.find_copies is None:
178 options.find_copies = bool(
179 git_get_branch_default('git-find-copies', True))
180 else:
181 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182
183 print('Using %d%% similarity for rename/copy detection. '
184 'Override with --similarity.' % options.similarity)
185
186 return options, args
187 parser.parse_args = Parse
188
189
ukai@chromium.org259e4682012-10-25 07:36:33 +0000190def is_dirty_git_tree(cmd):
191 # Make sure index is up-to-date before running diff-index.
192 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
193 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
194 if dirty:
195 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
196 print 'Uncommitted files: (git diff-index --name-status HEAD)'
197 print dirty[:4096]
198 if len(dirty) > 4096:
199 print '... (run "git diff-index --name-status HEAD" to see full output).'
200 return True
201 return False
202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000203
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000204def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
205 """Return the corresponding git ref if |base_url| together with |glob_spec|
206 matches the full |url|.
207
208 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
209 """
210 fetch_suburl, as_ref = glob_spec.split(':')
211 if allow_wildcards:
212 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
213 if glob_match:
214 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
215 # "branches/{472,597,648}/src:refs/remotes/svn/*".
216 branch_re = re.escape(base_url)
217 if glob_match.group(1):
218 branch_re += '/' + re.escape(glob_match.group(1))
219 wildcard = glob_match.group(2)
220 if wildcard == '*':
221 branch_re += '([^/]*)'
222 else:
223 # Escape and replace surrounding braces with parentheses and commas
224 # with pipe symbols.
225 wildcard = re.escape(wildcard)
226 wildcard = re.sub('^\\\\{', '(', wildcard)
227 wildcard = re.sub('\\\\,', '|', wildcard)
228 wildcard = re.sub('\\\\}$', ')', wildcard)
229 branch_re += wildcard
230 if glob_match.group(3):
231 branch_re += re.escape(glob_match.group(3))
232 match = re.match(branch_re, url)
233 if match:
234 return re.sub('\*$', match.group(1), as_ref)
235
236 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
237 if fetch_suburl:
238 full_url = base_url + '/' + fetch_suburl
239 else:
240 full_url = base_url
241 if full_url == url:
242 return as_ref
243 return None
244
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000245
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000247 """Prints statistics about the change to the user."""
248 # --no-ext-diff is broken in some versions of Git, so try to work around
249 # this by overriding the environment (but there is still a problem if the
250 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000251 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252 if 'GIT_EXTERNAL_DIFF' in env:
253 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254
255 if find_copies:
256 similarity_options = ['--find-copies-harder', '-l100000',
257 '-C%s' % similarity]
258 else:
259 similarity_options = ['-M%s' % similarity]
260
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000261 try:
262 stdout = sys.stdout.fileno()
263 except AttributeError:
264 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000265 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000266 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000267 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000268 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000269
270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271class Settings(object):
272 def __init__(self):
273 self.default_server = None
274 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000275 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 self.is_git_svn = None
277 self.svn_branch = None
278 self.tree_status_url = None
279 self.viewvc_url = None
280 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000281 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000282 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000283 self.project = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000284 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285
286 def LazyUpdateIfNeeded(self):
287 """Updates the settings from a codereview.settings file, if available."""
288 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 # The only value that actually changes the behavior is
290 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000291 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 error_ok=True
293 ).strip().lower()
294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000298 # set updated to True to avoid infinite calling loop
299 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000300 self.updated = True
301 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 self.updated = True
303
304 def GetDefaultServerUrl(self, error_ok=False):
305 if not self.default_server:
306 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000307 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000308 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if error_ok:
310 return self.default_server
311 if not self.default_server:
312 error_message = ('Could not find settings file. You must configure '
313 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000314 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000315 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.default_server
317
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 @staticmethod
319 def GetRelativeRoot():
320 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000323 if self.root is None:
324 self.root = os.path.abspath(self.GetRelativeRoot())
325 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetIsGitSvn(self):
328 """Return true if this repo looks like it's using git-svn."""
329 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000330 if self.GetPendingRefPrefix():
331 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
332 self.is_git_svn = False
333 else:
334 # If you have any "svn-remote.*" config keys, we think you're using svn.
335 self.is_git_svn = RunGitWithCode(
336 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 return self.is_git_svn
338
339 def GetSVNBranch(self):
340 if self.svn_branch is None:
341 if not self.GetIsGitSvn():
342 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
343
344 # Try to figure out which remote branch we're based on.
345 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000346 # 1) iterate through our branch history and find the svn URL.
347 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
349 # regexp matching the git-svn line that contains the URL.
350 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
351
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000352 # We don't want to go through all of history, so read a line from the
353 # pipe at a time.
354 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000355 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000356 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
357 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000358 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 for line in proc.stdout:
360 match = git_svn_re.match(line)
361 if match:
362 url = match.group(1)
363 proc.stdout.close() # Cut pipe.
364 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000366 if url:
367 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
368 remotes = RunGit(['config', '--get-regexp',
369 r'^svn-remote\..*\.url']).splitlines()
370 for remote in remotes:
371 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 remote = match.group(1)
374 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000375 rewrite_root = RunGit(
376 ['config', 'svn-remote.%s.rewriteRoot' % remote],
377 error_ok=True).strip()
378 if rewrite_root:
379 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000380 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000381 ['config', 'svn-remote.%s.fetch' % remote],
382 error_ok=True).strip()
383 if fetch_spec:
384 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
385 if self.svn_branch:
386 break
387 branch_spec = RunGit(
388 ['config', 'svn-remote.%s.branches' % remote],
389 error_ok=True).strip()
390 if branch_spec:
391 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
392 if self.svn_branch:
393 break
394 tag_spec = RunGit(
395 ['config', 'svn-remote.%s.tags' % remote],
396 error_ok=True).strip()
397 if tag_spec:
398 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
399 if self.svn_branch:
400 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401
402 if not self.svn_branch:
403 DieWithError('Can\'t guess svn branch -- try specifying it on the '
404 'command line')
405
406 return self.svn_branch
407
408 def GetTreeStatusUrl(self, error_ok=False):
409 if not self.tree_status_url:
410 error_message = ('You must configure your tree status URL by running '
411 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 self.tree_status_url = self._GetRietveldConfig(
413 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.tree_status_url
415
416 def GetViewVCUrl(self):
417 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 return self.viewvc_url
420
rmistry@google.com90752582014-01-14 21:04:50 +0000421 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000423
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000424 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000426
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000427 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000429
ukai@chromium.orge8077812012-02-03 03:41:46 +0000430 def GetIsGerrit(self):
431 """Return true if this repo is assosiated with gerrit code review system."""
432 if self.is_gerrit is None:
433 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
434 return self.is_gerrit
435
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 def GetGitEditor(self):
437 """Return the editor specified in the git config, or None if none is."""
438 if self.git_editor is None:
439 self.git_editor = self._GetConfig('core.editor', error_ok=True)
440 return self.git_editor or None
441
thestig@chromium.org44202a22014-03-11 19:22:18 +0000442 def GetLintRegex(self):
443 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
444 DEFAULT_LINT_REGEX)
445
446 def GetLintIgnoreRegex(self):
447 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
448 DEFAULT_LINT_IGNORE_REGEX)
449
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000450 def GetProject(self):
451 if not self.project:
452 self.project = self._GetRietveldConfig('project', error_ok=True)
453 return self.project
454
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000455 def GetPendingRefPrefix(self):
456 if not self.pending_ref_prefix:
457 self.pending_ref_prefix = self._GetRietveldConfig(
458 'pending-ref-prefix', error_ok=True)
459 return self.pending_ref_prefix
460
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000461 def _GetRietveldConfig(self, param, **kwargs):
462 return self._GetConfig('rietveld.' + param, **kwargs)
463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 def _GetConfig(self, param, **kwargs):
465 self.LazyUpdateIfNeeded()
466 return RunGit(['config', param], **kwargs).strip()
467
468
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469def ShortBranchName(branch):
470 """Convert a name like 'refs/heads/foo' to just 'foo'."""
471 return branch.replace('refs/heads/', '')
472
473
474class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000475 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000477 global settings
478 if not settings:
479 # Happens when git_cl.py is used as a utility library.
480 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 settings.GetDefaultServerUrl()
482 self.branchref = branchref
483 if self.branchref:
484 self.branch = ShortBranchName(self.branchref)
485 else:
486 self.branch = None
487 self.rietveld_server = None
488 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self.lookedup_issue = False
490 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 self.has_description = False
492 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000493 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000495 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000496 self.cc = None
497 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000498 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000499 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000500
501 def GetCCList(self):
502 """Return the users cc'd on this CL.
503
504 Return is a string suitable for passing to gcl with the --cc flag.
505 """
506 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000507 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 more_cc = ','.join(self.watchers)
509 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
510 return self.cc
511
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000512 def GetCCListWithoutDefault(self):
513 """Return the users cc'd on this CL excluding default ones."""
514 if self.cc is None:
515 self.cc = ','.join(self.watchers)
516 return self.cc
517
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000518 def SetWatchers(self, watchers):
519 """Set the list of email addresses that should be cc'd based on the changed
520 files in this CL.
521 """
522 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523
524 def GetBranch(self):
525 """Returns the short branch name, e.g. 'master'."""
526 if not self.branch:
527 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
528 self.branch = ShortBranchName(self.branchref)
529 return self.branch
530
531 def GetBranchRef(self):
532 """Returns the full branch name, e.g. 'refs/heads/master'."""
533 self.GetBranch() # Poke the lazy loader.
534 return self.branchref
535
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 @staticmethod
537 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000538 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 e.g. 'origin', 'refs/heads/master'
540 """
541 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000542 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
543 error_ok=True).strip()
544 if upstream_branch:
545 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
546 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000547 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
548 error_ok=True).strip()
549 if upstream_branch:
550 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000551 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000552 # Fall back on trying a git-svn upstream branch.
553 if settings.GetIsGitSvn():
554 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000556 # Else, try to guess the origin remote.
557 remote_branches = RunGit(['branch', '-r']).split()
558 if 'origin/master' in remote_branches:
559 # Fall back on origin/master if it exits.
560 remote = 'origin'
561 upstream_branch = 'refs/heads/master'
562 elif 'origin/trunk' in remote_branches:
563 # Fall back on origin/trunk if it exists. Generally a shared
564 # git-svn clone
565 remote = 'origin'
566 upstream_branch = 'refs/heads/trunk'
567 else:
568 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569Either pass complete "git diff"-style arguments, like
570 git cl upload origin/master
571or verify this branch is set up to track another (via the --track argument to
572"git checkout -b ...").""")
573
574 return remote, upstream_branch
575
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000576 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000577 return git_common.get_or_create_merge_base(self.GetBranch(),
578 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 def GetUpstreamBranch(self):
581 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000582 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000584 upstream_branch = upstream_branch.replace('refs/heads/',
585 'refs/remotes/%s/' % remote)
586 upstream_branch = upstream_branch.replace('refs/branch-heads/',
587 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.upstream_branch = upstream_branch
589 return self.upstream_branch
590
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000593 remote, branch = None, self.GetBranch()
594 seen_branches = set()
595 while branch not in seen_branches:
596 seen_branches.add(branch)
597 remote, branch = self.FetchUpstreamTuple(branch)
598 branch = ShortBranchName(branch)
599 if remote != '.' or branch.startswith('refs/remotes'):
600 break
601 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000602 remotes = RunGit(['remote'], error_ok=True).split()
603 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000604 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000605 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000606 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000607 logging.warning('Could not determine which remote this change is '
608 'associated with, so defaulting to "%s". This may '
609 'not be what you want. You may prevent this message '
610 'by running "git svn info" as documented here: %s',
611 self._remote,
612 GIT_INSTRUCTIONS_URL)
613 else:
614 logging.warn('Could not determine which remote this change is '
615 'associated with. You may prevent this message by '
616 'running "git svn info" as documented here: %s',
617 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000618 branch = 'HEAD'
619 if branch.startswith('refs/remotes'):
620 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000621 elif branch.startswith('refs/branch-heads/'):
622 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000623 else:
624 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000625 return self._remote
626
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000627 def GitSanityChecks(self, upstream_git_obj):
628 """Checks git repo status and ensures diff is from local commits."""
629
630 # Verify the commit we're diffing against is in our current branch.
631 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
632 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
633 if upstream_sha != common_ancestor:
634 print >> sys.stderr, (
635 'ERROR: %s is not in the current branch. You may need to rebase '
636 'your tracking branch' % upstream_sha)
637 return False
638
639 # List the commits inside the diff, and verify they are all local.
640 commits_in_diff = RunGit(
641 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
642 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
643 remote_branch = remote_branch.strip()
644 if code != 0:
645 _, remote_branch = self.GetRemoteBranch()
646
647 commits_in_remote = RunGit(
648 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
649
650 common_commits = set(commits_in_diff) & set(commits_in_remote)
651 if common_commits:
652 print >> sys.stderr, (
653 'ERROR: Your diff contains %d commits already in %s.\n'
654 'Run "git log --oneline %s..HEAD" to get a list of commits in '
655 'the diff. If you are using a custom git flow, you can override'
656 ' the reference used for this check with "git config '
657 'gitcl.remotebranch <git-ref>".' % (
658 len(common_commits), remote_branch, upstream_git_obj))
659 return False
660 return True
661
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000662 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000663 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000664
665 Returns None if it is not set.
666 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000667 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
668 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000669
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 def GetRemoteUrl(self):
671 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
672
673 Returns None if there is no remote.
674 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000675 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000676 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
677
678 # If URL is pointing to a local directory, it is probably a git cache.
679 if os.path.isdir(url):
680 url = RunGit(['config', 'remote.%s.url' % remote],
681 error_ok=True,
682 cwd=url).strip()
683 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
685 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000686 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000687 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000689 self.issue = int(issue) or None if issue else None
690 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 return self.issue
692
693 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000694 if not self.rietveld_server:
695 # If we're on a branch then get the server potentially associated
696 # with that branch.
697 if self.GetIssue():
698 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
699 ['config', self._RietveldServer()], error_ok=True).strip())
700 if not self.rietveld_server:
701 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 return self.rietveld_server
703
704 def GetIssueURL(self):
705 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000706 if not self.GetIssue():
707 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
709
710 def GetDescription(self, pretty=False):
711 if not self.has_description:
712 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000713 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000714 try:
715 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000716 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000717 if e.code == 404:
718 DieWithError(
719 ('\nWhile fetching the description for issue %d, received a '
720 '404 (not found)\n'
721 'error. It is likely that you deleted this '
722 'issue on the server. If this is the\n'
723 'case, please run\n\n'
724 ' git cl issue 0\n\n'
725 'to clear the association with the deleted issue. Then run '
726 'this command again.') % issue)
727 else:
728 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000729 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000730 except urllib2.URLError as e:
731 print >> sys.stderr, (
732 'Warning: Failed to retrieve CL description due to network '
733 'failure.')
734 self.description = ''
735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 self.has_description = True
737 if pretty:
738 wrapper = textwrap.TextWrapper()
739 wrapper.initial_indent = wrapper.subsequent_indent = ' '
740 return wrapper.fill(self.description)
741 return self.description
742
743 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000744 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000745 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 patchset = RunGit(['config', self._PatchsetSetting()],
747 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000748 self.patchset = int(patchset) or None if patchset else None
749 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750 return self.patchset
751
752 def SetPatchset(self, patchset):
753 """Set this branch's patchset. If patchset=0, clears the patchset."""
754 if patchset:
755 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000756 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 else:
758 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000759 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000760 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000762 def GetMostRecentPatchset(self):
763 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000764
765 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000766 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000767 '/download/issue%s_%s.diff' % (issue, patchset))
768
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000769 def GetIssueProperties(self):
770 if self._props is None:
771 issue = self.GetIssue()
772 if not issue:
773 self._props = {}
774 else:
775 self._props = self.RpcServer().get_issue_properties(issue, True)
776 return self._props
777
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000778 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000779 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 def SetIssue(self, issue):
782 """Set this branch's issue. If issue=0, clears the issue."""
783 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000784 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 RunGit(['config', self._IssueSetting(), str(issue)])
786 if self.rietveld_server:
787 RunGit(['config', self._RietveldServer(), self.rietveld_server])
788 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000789 current_issue = self.GetIssue()
790 if current_issue:
791 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000792 self.issue = None
793 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000795 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000796 if not self.GitSanityChecks(upstream_branch):
797 DieWithError('\nGit sanity check failure')
798
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000799 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000800 if not root:
801 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000802 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000803
804 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000806 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000807 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000808 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000809 except subprocess2.CalledProcessError:
810 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000811 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000812 'This branch probably doesn\'t exist anymore. To reset the\n'
813 'tracking branch, please run\n'
814 ' git branch --set-upstream %s trunk\n'
815 'replacing trunk with origin/master or the relevant branch') %
816 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000817
maruel@chromium.org52424302012-08-29 15:14:30 +0000818 issue = self.GetIssue()
819 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000820 if issue:
821 description = self.GetDescription()
822 else:
823 # If the change was never uploaded, use the log messages of all commits
824 # up to the branch point, as git cl upload will prefill the description
825 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000826 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
827 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000828
829 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000830 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000831 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000832 name,
833 description,
834 absroot,
835 files,
836 issue,
837 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000838 author,
839 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000840
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000841 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000842 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000843
844 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000845 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000846 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000847 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000848 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000849 except presubmit_support.PresubmitFailure, e:
850 DieWithError(
851 ('%s\nMaybe your depot_tools is out of date?\n'
852 'If all fails, contact maruel@') % e)
853
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000854 def UpdateDescription(self, description):
855 self.description = description
856 return self.RpcServer().update_description(
857 self.GetIssue(), self.description)
858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000860 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000861 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000863 def SetFlag(self, flag, value):
864 """Patchset must match."""
865 if not self.GetPatchset():
866 DieWithError('The patchset needs to match. Send another patchset.')
867 try:
868 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000869 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000870 except urllib2.HTTPError, e:
871 if e.code == 404:
872 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
873 if e.code == 403:
874 DieWithError(
875 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
876 'match?') % (self.GetIssue(), self.GetPatchset()))
877 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000879 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 """Returns an upload.RpcServer() to access this review's rietveld instance.
881 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000882 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000883 self._rpc_server = rietveld.CachingRietveld(
884 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000885 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886
887 def _IssueSetting(self):
888 """Return the git setting that stores this change's issue."""
889 return 'branch.%s.rietveldissue' % self.GetBranch()
890
891 def _PatchsetSetting(self):
892 """Return the git setting that stores this change's most recent patchset."""
893 return 'branch.%s.rietveldpatchset' % self.GetBranch()
894
895 def _RietveldServer(self):
896 """Returns the git setting that stores this change's rietveld server."""
897 return 'branch.%s.rietveldserver' % self.GetBranch()
898
899
900def GetCodereviewSettingsInteractively():
901 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 server = settings.GetDefaultServerUrl(error_ok=True)
904 prompt = 'Rietveld server (host[:port])'
905 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000906 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 if not server and not newserver:
908 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000909 if newserver:
910 newserver = gclient_utils.UpgradeToHttps(newserver)
911 if newserver != server:
912 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000914 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 prompt = caption
916 if initial:
917 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000918 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 if new_val == 'x':
920 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000921 elif new_val:
922 if is_url:
923 new_val = gclient_utils.UpgradeToHttps(new_val)
924 if new_val != initial:
925 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000927 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000928 SetProperty(settings.GetDefaultPrivateFlag(),
929 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000931 'tree-status-url', False)
932 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000933 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934
935 # TODO: configure a default branch to diff against, rather than this
936 # svn-based hackery.
937
938
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000939class ChangeDescription(object):
940 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000941 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000943
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000945 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000946
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 @property # www.logilab.org/ticket/89786
948 def description(self): # pylint: disable=E0202
949 return '\n'.join(self._description_lines)
950
951 def set_description(self, desc):
952 if isinstance(desc, basestring):
953 lines = desc.splitlines()
954 else:
955 lines = [line.rstrip() for line in desc]
956 while lines and not lines[0]:
957 lines.pop(0)
958 while lines and not lines[-1]:
959 lines.pop(-1)
960 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000961
962 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000964 assert isinstance(reviewers, list), reviewers
965 if not reviewers:
966 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000968
agable@chromium.org42c20792013-09-12 17:34:49 +0000969 # Get the set of R= and TBR= lines and remove them from the desciption.
970 regexp = re.compile(self.R_LINE)
971 matches = [regexp.match(line) for line in self._description_lines]
972 new_desc = [l for i, l in enumerate(self._description_lines)
973 if not matches[i]]
974 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000975
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 # Construct new unified R= and TBR= lines.
977 r_names = []
978 tbr_names = []
979 for match in matches:
980 if not match:
981 continue
982 people = cleanup_list([match.group(2).strip()])
983 if match.group(1) == 'TBR':
984 tbr_names.extend(people)
985 else:
986 r_names.extend(people)
987 for name in r_names:
988 if name not in reviewers:
989 reviewers.append(name)
990 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
991 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
992
993 # Put the new lines in the description where the old first R= line was.
994 line_loc = next((i for i, match in enumerate(matches) if match), -1)
995 if 0 <= line_loc < len(self._description_lines):
996 if new_tbr_line:
997 self._description_lines.insert(line_loc, new_tbr_line)
998 if new_r_line:
999 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001000 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001001 if new_r_line:
1002 self.append_footer(new_r_line)
1003 if new_tbr_line:
1004 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001005
1006 def prompt(self):
1007 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 self.set_description([
1009 '# Enter a description of the change.',
1010 '# This will be displayed on the codereview site.',
1011 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001012 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001013 '--------------------',
1014 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001015
agable@chromium.org42c20792013-09-12 17:34:49 +00001016 regexp = re.compile(self.BUG_LINE)
1017 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001018 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001019 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001020 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001021 if not content:
1022 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001023 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001024
1025 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001026 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1027 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001028 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001029 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001030
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001031 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001032 if self._description_lines:
1033 # Add an empty line if either the last line or the new line isn't a tag.
1034 last_line = self._description_lines[-1]
1035 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1036 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1037 self._description_lines.append('')
1038 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001039
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001040 def get_reviewers(self):
1041 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001042 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1043 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001044 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001045
1046
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001047def get_approving_reviewers(props):
1048 """Retrieves the reviewers that approved a CL from the issue properties with
1049 messages.
1050
1051 Note that the list may contain reviewers that are not committer, thus are not
1052 considered by the CQ.
1053 """
1054 return sorted(
1055 set(
1056 message['sender']
1057 for message in props['messages']
1058 if message['approval'] and message['sender'] in props['reviewers']
1059 )
1060 )
1061
1062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063def FindCodereviewSettingsFile(filename='codereview.settings'):
1064 """Finds the given file starting in the cwd and going up.
1065
1066 Only looks up to the top of the repository unless an
1067 'inherit-review-settings-ok' file exists in the root of the repository.
1068 """
1069 inherit_ok_file = 'inherit-review-settings-ok'
1070 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001071 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1073 root = '/'
1074 while True:
1075 if filename in os.listdir(cwd):
1076 if os.path.isfile(os.path.join(cwd, filename)):
1077 return open(os.path.join(cwd, filename))
1078 if cwd == root:
1079 break
1080 cwd = os.path.dirname(cwd)
1081
1082
1083def LoadCodereviewSettingsFromFile(fileobj):
1084 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001085 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 def SetProperty(name, setting, unset_error_ok=False):
1088 fullname = 'rietveld.' + name
1089 if setting in keyvals:
1090 RunGit(['config', fullname, keyvals[setting]])
1091 else:
1092 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1093
1094 SetProperty('server', 'CODE_REVIEW_SERVER')
1095 # Only server setting is required. Other settings can be absent.
1096 # In that case, we ignore errors raised during option deletion attempt.
1097 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001098 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1100 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001101 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001102 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1103 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001104 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001105 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001107 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001108 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1111 #should be of the form
1112 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1113 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1114 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1115 keyvals['ORIGIN_URL_CONFIG']])
1116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001118def urlretrieve(source, destination):
1119 """urllib is broken for SSL connections via a proxy therefore we
1120 can't use urllib.urlretrieve()."""
1121 with open(destination, 'w') as f:
1122 f.write(urllib2.urlopen(source).read())
1123
1124
ukai@chromium.org712d6102013-11-27 00:52:58 +00001125def hasSheBang(fname):
1126 """Checks fname is a #! script."""
1127 with open(fname) as f:
1128 return f.read(2).startswith('#!')
1129
1130
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001131def DownloadHooks(force):
1132 """downloads hooks
1133
1134 Args:
1135 force: True to update hooks. False to install hooks if not present.
1136 """
1137 if not settings.GetIsGerrit():
1138 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001139 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001140 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1141 if not os.access(dst, os.X_OK):
1142 if os.path.exists(dst):
1143 if not force:
1144 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001145 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001146 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001147 if not hasSheBang(dst):
1148 DieWithError('Not a script: %s\n'
1149 'You need to download from\n%s\n'
1150 'into .git/hooks/commit-msg and '
1151 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001152 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1153 except Exception:
1154 if os.path.exists(dst):
1155 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001156 DieWithError('\nFailed to download hooks.\n'
1157 'You need to download from\n%s\n'
1158 'into .git/hooks/commit-msg and '
1159 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001160
1161
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001162@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001164 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001166 parser.add_option('--activate-update', action='store_true',
1167 help='activate auto-updating [rietveld] section in '
1168 '.git/config')
1169 parser.add_option('--deactivate-update', action='store_true',
1170 help='deactivate auto-updating [rietveld] section in '
1171 '.git/config')
1172 options, args = parser.parse_args(args)
1173
1174 if options.deactivate_update:
1175 RunGit(['config', 'rietveld.autoupdate', 'false'])
1176 return
1177
1178 if options.activate_update:
1179 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1180 return
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if len(args) == 0:
1183 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001184 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 return 0
1186
1187 url = args[0]
1188 if not url.endswith('codereview.settings'):
1189 url = os.path.join(url, 'codereview.settings')
1190
1191 # Load code review settings and download hooks (if available).
1192 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001193 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 return 0
1195
1196
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001197def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001198 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001199 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1200 branch = ShortBranchName(branchref)
1201 _, args = parser.parse_args(args)
1202 if not args:
1203 print("Current base-url:")
1204 return RunGit(['config', 'branch.%s.base-url' % branch],
1205 error_ok=False).strip()
1206 else:
1207 print("Setting base-url to %s" % args[0])
1208 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1209 error_ok=False).strip()
1210
1211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001213 """Show status of changelists.
1214
1215 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001216 - Red not sent for review or broken
1217 - Blue waiting for review
1218 - Yellow waiting for you to reply to review
1219 - Green LGTM'ed
1220 - Magenta in the commit queue
1221 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001222
1223 Also see 'git cl comments'.
1224 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 parser.add_option('--field',
1226 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001227 parser.add_option('-f', '--fast', action='store_true',
1228 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001230 if args:
1231 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001234 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 if options.field.startswith('desc'):
1236 print cl.GetDescription()
1237 elif options.field == 'id':
1238 issueid = cl.GetIssue()
1239 if issueid:
1240 print issueid
1241 elif options.field == 'patch':
1242 patchset = cl.GetPatchset()
1243 if patchset:
1244 print patchset
1245 elif options.field == 'url':
1246 url = cl.GetIssueURL()
1247 if url:
1248 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001249 return 0
1250
1251 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1252 if not branches:
1253 print('No local branch found.')
1254 return 0
1255
1256 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001257 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001258 alignment = max(5, max(len(b) for b in branches))
1259 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001260 # Adhoc thread pool to request data concurrently.
1261 output = Queue.Queue()
1262
1263 # Silence upload.py otherwise it becomes unweldly.
1264 upload.verbosity = 0
1265
1266 if not options.fast:
1267 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001268 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001269 c = Changelist(branchref=b)
1270 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001271 props = {}
1272 r = None
1273 if i:
1274 try:
1275 props = c.GetIssueProperties()
1276 r = c.GetApprovingReviewers() if i else None
1277 except urllib2.HTTPError:
1278 # The issue probably doesn't exist anymore.
1279 i += ' (broken)'
1280
1281 msgs = props.get('messages') or []
1282
1283 if not i:
1284 color = Fore.WHITE
1285 elif props.get('closed'):
1286 # Issue is closed.
1287 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001288 elif props.get('commit'):
1289 # Issue is in the commit queue.
1290 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001291 elif r:
1292 # Was LGTM'ed.
1293 color = Fore.GREEN
1294 elif not msgs:
1295 # No message was sent.
1296 color = Fore.RED
1297 elif msgs[-1]['sender'] != props.get('owner_email'):
1298 color = Fore.YELLOW
1299 else:
1300 color = Fore.BLUE
1301 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001302
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001303 # Process one branch synchronously to work through authentication, then
1304 # spawn threads to process all the other branches in parallel.
1305 if branches:
1306 fetch(branches[0])
1307 threads = [
1308 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001309 for t in threads:
1310 t.daemon = True
1311 t.start()
1312 else:
1313 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1314 for b in branches:
1315 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001316 url = c.GetIssueURL()
1317 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001318
1319 tmp = {}
1320 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001321 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001322 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001323 b, i, color = output.get()
1324 tmp[b] = (i, color)
1325 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001326 reset = Fore.RESET
1327 if not sys.stdout.isatty():
1328 color = ''
1329 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001330 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001331 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001332
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001333 cl = Changelist()
1334 print
1335 print 'Current branch:',
1336 if not cl.GetIssue():
1337 print 'no issue assigned.'
1338 return 0
1339 print cl.GetBranch()
1340 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001341 if not options.fast:
1342 print 'Issue description:'
1343 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 return 0
1345
1346
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001347def colorize_CMDstatus_doc():
1348 """To be called once in main() to add colors to git cl status help."""
1349 colors = [i for i in dir(Fore) if i[0].isupper()]
1350
1351 def colorize_line(line):
1352 for color in colors:
1353 if color in line.upper():
1354 # Extract whitespaces first and the leading '-'.
1355 indent = len(line) - len(line.lstrip(' ')) + 1
1356 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1357 return line
1358
1359 lines = CMDstatus.__doc__.splitlines()
1360 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1361
1362
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001363@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001365 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
1367 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001368 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001369 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
1371 cl = Changelist()
1372 if len(args) > 0:
1373 try:
1374 issue = int(args[0])
1375 except ValueError:
1376 DieWithError('Pass a number to set the issue or none to list it.\n'
1377 'Maybe you want to run git cl status?')
1378 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001379 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 return 0
1381
1382
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001383def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001384 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001385 (_, args) = parser.parse_args(args)
1386 if args:
1387 parser.error('Unsupported argument: %s' % args)
1388
1389 cl = Changelist()
1390 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001391 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001392 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001393 if message['disapproval']:
1394 color = Fore.RED
1395 elif message['approval']:
1396 color = Fore.GREEN
1397 elif message['sender'] == data['owner_email']:
1398 color = Fore.MAGENTA
1399 else:
1400 color = Fore.BLUE
1401 print '\n%s%s %s%s' % (
1402 color, message['date'].split('.', 1)[0], message['sender'],
1403 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001404 if message['text'].strip():
1405 print '\n'.join(' ' + l for l in message['text'].splitlines())
1406 return 0
1407
1408
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001409def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001410 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001411 cl = Changelist()
1412 if not cl.GetIssue():
1413 DieWithError('This branch has no associated changelist.')
1414 description = ChangeDescription(cl.GetDescription())
1415 description.prompt()
1416 cl.UpdateDescription(description.description)
1417 return 0
1418
1419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420def CreateDescriptionFromLog(args):
1421 """Pulls out the commit log to use as a base for the CL description."""
1422 log_args = []
1423 if len(args) == 1 and not args[0].endswith('.'):
1424 log_args = [args[0] + '..']
1425 elif len(args) == 1 and args[0].endswith('...'):
1426 log_args = [args[0][:-1]]
1427 elif len(args) == 2:
1428 log_args = [args[0] + '..' + args[1]]
1429 else:
1430 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001431 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432
1433
thestig@chromium.org44202a22014-03-11 19:22:18 +00001434def CMDlint(parser, args):
1435 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001436 parser.add_option('--filter', action='append', metavar='-x,+y',
1437 help='Comma-separated list of cpplint\'s category-filters')
1438 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001439
1440 # Access to a protected member _XX of a client class
1441 # pylint: disable=W0212
1442 try:
1443 import cpplint
1444 import cpplint_chromium
1445 except ImportError:
1446 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1447 return 1
1448
1449 # Change the current working directory before calling lint so that it
1450 # shows the correct base.
1451 previous_cwd = os.getcwd()
1452 os.chdir(settings.GetRoot())
1453 try:
1454 cl = Changelist()
1455 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1456 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001457 if not files:
1458 print "Cannot lint an empty CL"
1459 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001460
1461 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001462 command = args + files
1463 if options.filter:
1464 command = ['--filter=' + ','.join(options.filter)] + command
1465 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001466
1467 white_regex = re.compile(settings.GetLintRegex())
1468 black_regex = re.compile(settings.GetLintIgnoreRegex())
1469 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1470 for filename in filenames:
1471 if white_regex.match(filename):
1472 if black_regex.match(filename):
1473 print "Ignoring file %s" % filename
1474 else:
1475 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1476 extra_check_functions)
1477 else:
1478 print "Skipping file %s" % filename
1479 finally:
1480 os.chdir(previous_cwd)
1481 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1482 if cpplint._cpplint_state.error_count != 0:
1483 return 1
1484 return 0
1485
1486
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001488 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001489 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001491 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001492 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493 (options, args) = parser.parse_args(args)
1494
ukai@chromium.org259e4682012-10-25 07:36:33 +00001495 if not options.force and is_dirty_git_tree('presubmit'):
1496 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497 return 1
1498
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001499 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 if args:
1501 base_branch = args[0]
1502 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001503 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001504 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001506 cl.RunHook(
1507 committing=not options.upload,
1508 may_prompt=False,
1509 verbose=options.verbose,
1510 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001511 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512
1513
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001514def AddChangeIdToCommitMessage(options, args):
1515 """Re-commits using the current message, assumes the commit hook is in
1516 place.
1517 """
1518 log_desc = options.message or CreateDescriptionFromLog(args)
1519 git_command = ['commit', '--amend', '-m', log_desc]
1520 RunGit(git_command)
1521 new_log_desc = CreateDescriptionFromLog(args)
1522 if CHANGE_ID in new_log_desc:
1523 print 'git-cl: Added Change-Id to commit message.'
1524 else:
1525 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1526
1527
ukai@chromium.orge8077812012-02-03 03:41:46 +00001528def GerritUpload(options, args, cl):
1529 """upload the current branch to gerrit."""
1530 # We assume the remote called "origin" is the one we want.
1531 # It is probably not worthwhile to support different workflows.
1532 remote = 'origin'
1533 branch = 'master'
1534 if options.target_branch:
1535 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 change_desc = ChangeDescription(
1538 options.message or CreateDescriptionFromLog(args))
1539 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001540 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001541 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001542 if CHANGE_ID not in change_desc.description:
1543 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001544
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001545 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001546 if len(commits) > 1:
1547 print('WARNING: This will upload %d commits. Run the following command '
1548 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001549 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001550 print('You can also use `git squash-branch` to squash these into a single'
1551 'commit.')
1552 ask_for_data('About to upload; enter to confirm.')
1553
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001554 if options.reviewers:
1555 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556
ukai@chromium.orge8077812012-02-03 03:41:46 +00001557 receive_options = []
1558 cc = cl.GetCCList().split(',')
1559 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001560 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001561 cc = filter(None, cc)
1562 if cc:
1563 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001564 if change_desc.get_reviewers():
1565 receive_options.extend(
1566 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567
ukai@chromium.orge8077812012-02-03 03:41:46 +00001568 git_command = ['push']
1569 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001570 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001571 ' '.join(receive_options))
1572 git_command += [remote, 'HEAD:refs/for/' + branch]
1573 RunGit(git_command)
1574 # TODO(ukai): parse Change-Id: and set issue number?
1575 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577
jam@chromium.org82114c02014-08-22 22:13:21 +00001578def RietveldUpload(options, args, cl):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001579 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1581 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 if options.emulate_svn_auto_props:
1583 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584
1585 change_desc = None
1586
pgervais@chromium.org91141372014-01-09 23:27:20 +00001587 if options.email is not None:
1588 upload_args.extend(['--email', options.email])
1589
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001591 if options.title:
1592 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001593 if options.message:
1594 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001595 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 print ("This branch is associated with issue %s. "
1597 "Adding patch to that issue." % cl.GetIssue())
1598 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001599 if options.title:
1600 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001601 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001602 change_desc = ChangeDescription(message)
1603 if options.reviewers:
1604 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001605 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001606 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001607
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001608 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 print "Description is empty; aborting."
1610 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001611
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001612 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001613 if change_desc.get_reviewers():
1614 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001615 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001616 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001617 DieWithError("Must specify reviewers to send email.")
1618 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001619
1620 # We check this before applying rietveld.private assuming that in
1621 # rietveld.cc only addresses which we can send private CLs to are listed
1622 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1623 # --private is specified explicitly on the command line.
1624 if options.private:
1625 logging.warn('rietveld.cc is ignored since private flag is specified. '
1626 'You need to review and add them manually if necessary.')
1627 cc = cl.GetCCListWithoutDefault()
1628 else:
1629 cc = cl.GetCCList()
1630 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001631 if cc:
1632 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001634 if options.private or settings.GetDefaultPrivateFlag() == "True":
1635 upload_args.append('--private')
1636
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001637 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001638 if not options.find_copies:
1639 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 # Include the upstream repo's URL in the change -- this is useful for
1642 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001643 remote_url = cl.GetGitBaseUrlFromConfig()
1644 if not remote_url:
1645 if settings.GetIsGitSvn():
1646 # URL is dependent on the current directory.
1647 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1648 if data:
1649 keys = dict(line.split(': ', 1) for line in data.splitlines()
1650 if ': ' in line)
1651 remote_url = keys.get('URL', None)
1652 else:
1653 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1654 remote_url = (cl.GetRemoteUrl() + '@'
1655 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 if remote_url:
1657 upload_args.extend(['--base_url', remote_url])
1658
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001659 project = settings.GetProject()
1660 if project:
1661 upload_args.extend(['--project', project])
1662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001664 upload_args = ['upload'] + upload_args + args
1665 logging.info('upload.RealMain(%s)', upload_args)
1666 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001667 issue = int(issue)
1668 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001669 except KeyboardInterrupt:
1670 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671 except:
1672 # If we got an exception after the user typed a description for their
1673 # change, back up the description before re-raising.
1674 if change_desc:
1675 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1676 print '\nGot exception while uploading -- saving description to %s\n' \
1677 % backup_path
1678 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001679 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 backup_file.close()
1681 raise
1682
1683 if not cl.GetIssue():
1684 cl.SetIssue(issue)
1685 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001686
1687 if options.use_commit_queue:
1688 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 return 0
1690
1691
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001692def cleanup_list(l):
1693 """Fixes a list so that comma separated items are put as individual items.
1694
1695 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1696 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1697 """
1698 items = sum((i.split(',') for i in l), [])
1699 stripped_items = (i.strip() for i in items)
1700 return sorted(filter(None, stripped_items))
1701
1702
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001703@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001704def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001705 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001706 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1707 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001708 parser.add_option('--bypass-watchlists', action='store_true',
1709 dest='bypass_watchlists',
1710 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001711 parser.add_option('-f', action='store_true', dest='force',
1712 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001713 parser.add_option('-m', dest='message', help='message for patchset')
1714 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001716 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717 help='reviewer email addresses')
1718 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001719 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001720 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001721 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001722 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001723 parser.add_option('--emulate_svn_auto_props',
1724 '--emulate-svn-auto-props',
1725 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001726 dest="emulate_svn_auto_props",
1727 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001728 parser.add_option('-c', '--use-commit-queue', action='store_true',
1729 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001730 parser.add_option('--private', action='store_true',
1731 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001732 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001733 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001734 help='When uploading to gerrit, remote branch to '
1735 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001736 parser.add_option('--email', default=None,
1737 help='email address to use to connect to Rietveld')
1738
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001739 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001740 (options, args) = parser.parse_args(args)
1741
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001742 if options.target_branch and not settings.GetIsGerrit():
1743 parser.error('Use --target_branch for non gerrit repository.')
1744
ukai@chromium.org259e4682012-10-25 07:36:33 +00001745 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001746 return 1
1747
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001748 options.reviewers = cleanup_list(options.reviewers)
1749 options.cc = cleanup_list(options.cc)
1750
ukai@chromium.orge8077812012-02-03 03:41:46 +00001751 cl = Changelist()
1752 if args:
1753 # TODO(ukai): is it ok for gerrit case?
1754 base_branch = args[0]
1755 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001756 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001757 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001758 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001760 # Apply watchlists on upload.
1761 change = cl.GetChange(base_branch, None)
1762 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1763 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001764 if not options.bypass_watchlists:
1765 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001766
ukai@chromium.orge8077812012-02-03 03:41:46 +00001767 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001768 if options.reviewers:
1769 # Set the reviewer list now so that presubmit checks can access it.
1770 change_description = ChangeDescription(change.FullDescriptionText())
1771 change_description.update_reviewers(options.reviewers)
1772 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001773 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 may_prompt=not options.force,
1775 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001776 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777 if not hook_results.should_continue():
1778 return 1
1779 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001780 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001782 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001783 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001784 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001785 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001786 print ('The last upload made from this repository was patchset #%d but '
1787 'the most recent patchset on the server is #%d.'
1788 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001789 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1790 'from another machine or branch the patch you\'re uploading now '
1791 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001792 ask_for_data('About to upload; enter to confirm.')
1793
iannucci@chromium.org79540052012-10-19 23:15:26 +00001794 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001795 if settings.GetIsGerrit():
1796 return GerritUpload(options, args, cl)
jam@chromium.org82114c02014-08-22 22:13:21 +00001797 ret = RietveldUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001798 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001799 git_set_branch_value('last-upload-hash',
1800 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001801
1802 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001803
1804
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001805def IsSubmoduleMergeCommit(ref):
1806 # When submodules are added to the repo, we expect there to be a single
1807 # non-git-svn merge commit at remote HEAD with a signature comment.
1808 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001809 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001810 return RunGit(cmd) != ''
1811
1812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001814 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001816 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 Updates changelog with metadata (e.g. pointer to review).
1818 Pushes/dcommits the code upstream.
1819 Updates review and closes.
1820 """
1821 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1822 help='bypass upload presubmit hook')
1823 parser.add_option('-m', dest='message',
1824 help="override review description")
1825 parser.add_option('-f', action='store_true', dest='force',
1826 help="force yes to questions (don't prompt)")
1827 parser.add_option('-c', dest='contributor',
1828 help="external contributor for patch (appended to " +
1829 "description and used as author for git). Should be " +
1830 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001831 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 (options, args) = parser.parse_args(args)
1833 cl = Changelist()
1834
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001835 current = cl.GetBranch()
1836 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1837 if not settings.GetIsGitSvn() and remote == '.':
1838 print
1839 print 'Attempting to push branch %r into another local branch!' % current
1840 print
1841 print 'Either reparent this branch on top of origin/master:'
1842 print ' git reparent-branch --root'
1843 print
1844 print 'OR run `git rebase-update` if you think the parent branch is already'
1845 print 'committed.'
1846 print
1847 print ' Current parent: %r' % upstream_branch
1848 return 1
1849
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001850 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 # Default to merging against our best guess of the upstream branch.
1852 args = [cl.GetUpstreamBranch()]
1853
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001854 if options.contributor:
1855 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1856 print "Please provide contibutor as 'First Last <email@example.com>'"
1857 return 1
1858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001860 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861
ukai@chromium.org259e4682012-10-25 07:36:33 +00001862 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 return 1
1864
1865 # This rev-list syntax means "show all commits not in my branch that
1866 # are in base_branch".
1867 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1868 base_branch]).splitlines()
1869 if upstream_commits:
1870 print ('Base branch "%s" has %d commits '
1871 'not in this branch.' % (base_branch, len(upstream_commits)))
1872 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1873 return 1
1874
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001875 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001876 svn_head = None
1877 if cmd == 'dcommit' or base_has_submodules:
1878 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1879 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001882 # If the base_head is a submodule merge commit, the first parent of the
1883 # base_head should be a git-svn commit, which is what we're interested in.
1884 base_svn_head = base_branch
1885 if base_has_submodules:
1886 base_svn_head += '^1'
1887
1888 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889 if extra_commits:
1890 print ('This branch has %d additional commits not upstreamed yet.'
1891 % len(extra_commits.splitlines()))
1892 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1893 'before attempting to %s.' % (base_branch, cmd))
1894 return 1
1895
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001896 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001897 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001898 author = None
1899 if options.contributor:
1900 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001901 hook_results = cl.RunHook(
1902 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001903 may_prompt=not options.force,
1904 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001905 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001906 if not hook_results.should_continue():
1907 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001909 # Check the tree status if the tree status URL is set.
1910 status = GetTreeStatus()
1911 if 'closed' == status:
1912 print('The tree is closed. Please wait for it to reopen. Use '
1913 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1914 return 1
1915 elif 'unknown' == status:
1916 print('Unable to determine tree status. Please verify manually and '
1917 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1918 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001919 else:
1920 breakpad.SendStack(
1921 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001922 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1923 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001924 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001926 change_desc = ChangeDescription(options.message)
1927 if not change_desc.description and cl.GetIssue():
1928 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001930 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001931 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001932 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001933 else:
1934 print 'No description set.'
1935 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1936 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001938 # Keep a separate copy for the commit message, because the commit message
1939 # contains the link to the Rietveld issue, while the Rietveld message contains
1940 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001941 # Keep a separate copy for the commit message.
1942 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001943 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001944
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001946 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001947 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001949 commit_desc.append_footer('Patch from %s.' % options.contributor)
1950
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001951 print('Description:')
1952 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953
1954 branches = [base_branch, cl.GetBranchRef()]
1955 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001956 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001958 # We want to squash all this branch's commits into one commit with the proper
1959 # description. We do this by doing a "reset --soft" to the base branch (which
1960 # keeps the working copy the same), then dcommitting that. If origin/master
1961 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1962 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001964 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1965 # Delete the branches if they exist.
1966 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1967 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1968 result = RunGitWithCode(showref_cmd)
1969 if result[0] == 0:
1970 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971
1972 # We might be in a directory that's present in this branch but not in the
1973 # trunk. Move up to the top of the tree so that git commands that expect a
1974 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001975 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 if rel_base_path:
1977 os.chdir(rel_base_path)
1978
1979 # Stuff our change into the merge branch.
1980 # We wrap in a try...finally block so if anything goes wrong,
1981 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001982 retcode = -1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001983 used_pending = False
1984 pending_ref = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001986 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1987 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001989 RunGit(
1990 [
1991 'commit', '--author', options.contributor,
1992 '-m', commit_desc.description,
1993 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001995 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001996 if base_has_submodules:
1997 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1998 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1999 RunGit(['checkout', CHERRY_PICK_BRANCH])
2000 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002001 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002002 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002003 pending_prefix = settings.GetPendingRefPrefix()
2004 if not pending_prefix or branch.startswith(pending_prefix):
2005 # If not using refs/pending/heads/* at all, or target ref is already set
2006 # to pending, then push to the target ref directly.
2007 retcode, output = RunGitWithCode(
2008 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
2009 used_pending = pending_prefix and branch.startswith(pending_prefix)
2010 else:
2011 # Cherry-pick the change on top of pending ref and then push it.
2012 assert branch.startswith('refs/'), branch
2013 assert pending_prefix[-1] == '/', pending_prefix
2014 pending_ref = pending_prefix + branch[len('refs/'):]
2015 retcode, output = PushToGitPending(remote, pending_ref, branch)
2016 used_pending = (retcode == 0)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017 logging.debug(output)
2018 else:
2019 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002020 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002021 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002022 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 finally:
2024 # And then swap back to the original branch and clean up.
2025 RunGit(['checkout', '-q', cl.GetBranch()])
2026 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002027 if base_has_submodules:
2028 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
2030 if cl.GetIssue():
2031 if cmd == 'dcommit' and 'Committed r' in output:
2032 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002033 elif cmd == 'land' and retcode == 0:
mark@chromium.org671c7a32014-07-31 17:09:36 +00002034 match = (re.match(r'.*?([a-f0-9]{7,})\.\.([a-f0-9]{7,})$', l)
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002035 for l in output.splitlines(False))
2036 match = filter(None, match)
2037 if len(match) != 1:
2038 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2039 output)
2040 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 else:
2042 return 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002043 to_pending = ' to pending queue' if used_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002044 viewvc_url = settings.GetViewVCUrl()
2045 if viewvc_url and revision:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002046 change_desc.append_footer(
2047 'Committed%s: %s%s' % (to_pending, viewvc_url, revision))
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002048 elif revision:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002049 change_desc.append_footer('Committed%s: %s' % (to_pending, revision))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050 print ('Closing issue '
2051 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002052 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002054 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002055 patch_num = len(props['patchsets'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002056 comment = "Committed patchset #%d%s manually as %s" % (
2057 patch_num, to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002058 if options.bypass_hooks:
2059 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2060 else:
2061 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002062 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002063 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002064
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002065 if used_pending and retcode == 0:
2066 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2067 print 'The commit is in the pending queue (%s).' % pending_ref
2068 print (
2069 'It will show up on %s in ~1 min, once it gets Cr-Commit-Position '
2070 'footer.' % branch)
2071
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002072 if retcode == 0:
2073 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2074 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002075 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077 return 0
2078
2079
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002080def PushToGitPending(remote, pending_ref, upstream_ref):
2081 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2082
2083 Returns:
2084 (retcode of last operation, output log of last operation).
2085 """
2086 assert pending_ref.startswith('refs/'), pending_ref
2087 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2088 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2089 code = 0
2090 out = ''
2091 attempt = 0
2092 while attempt < 5:
2093 attempt += 1
2094
2095 # Fetch. Retry fetch errors.
2096 code, out = RunGitWithCode(
2097 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)],
2098 suppress_stderr=True)
2099 if code:
2100 continue
2101
2102 # Try to cherry pick. Abort on merge conflicts.
2103 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
2104 code, out = RunGitWithCode(['cherry-pick', cherry], suppress_stderr=True)
2105 if code:
2106 print (
2107 'Your patch doesn\'t apply cleanly to upstream ref \'%s\', '
2108 'the following files have merge conflicts:' % upstream_ref)
2109 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2110 print 'Please rebase your patch and try again.'
2111 RunGitWithCode(['cherry-pick', '--abort'], suppress_stderr=True)
2112 return code, out
2113
2114 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
2115 code, out = RunGitWithCode(
2116 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2117 if code == 0:
2118 # Success.
2119 return code, out
2120
2121 return code, out
2122
2123
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002124@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002126 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002128 message = """This doesn't appear to be an SVN repository.
2129If your project has a git mirror with an upstream SVN master, you probably need
2130to run 'git svn init', see your project's git mirror documentation.
2131If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002132to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002133Choose wisely, if you get this wrong, your commit might appear to succeed but
2134will instead be silently ignored."""
2135 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002136 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 return SendUpstream(parser, args, 'dcommit')
2138
2139
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002140@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002141def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002142 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143 if settings.GetIsGitSvn():
2144 print('This appears to be an SVN repository.')
2145 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002146 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002147 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148
2149
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002150@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002152 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002153 parser.add_option('-b', dest='newbranch',
2154 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002155 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002157 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2158 help='Change to the directory DIR immediately, '
2159 'before doing anything else.')
2160 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002161 help='failed patches spew .rej files rather than '
2162 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2164 help="don't commit after patch applies")
2165 (options, args) = parser.parse_args(args)
2166 if len(args) != 1:
2167 parser.print_help()
2168 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002169 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002171 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002172 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002173
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002174 if options.newbranch:
2175 if options.force:
2176 RunGit(['branch', '-D', options.newbranch],
2177 stderr=subprocess2.PIPE, error_ok=True)
2178 RunGit(['checkout', '-b', options.newbranch,
2179 Changelist().GetUpstreamBranch()])
2180
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002181 return PatchIssue(issue_arg, options.reject, options.nocommit,
2182 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002183
2184
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002185def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002186 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002188 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002189 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002190 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002191 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002193 # Assume it's a URL to the patch. Default to https.
2194 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002195 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002196 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197 DieWithError('Must pass an issue ID or full URL for '
2198 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002199 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002200 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002201 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002202
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203 # Switch up to the top-level directory, if necessary, in preparation for
2204 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002205 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206 if top:
2207 os.chdir(top)
2208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209 # Git patches have a/ at the beginning of source paths. We strip that out
2210 # with a sed script rather than the -p flag to patch so we can feed either
2211 # Git or svn-style patches into the same apply command.
2212 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002213 try:
2214 patch_data = subprocess2.check_output(
2215 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2216 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002217 DieWithError('Git patch mungling failed.')
2218 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002220 # We use "git apply" to apply the patch instead of "patch" so that we can
2221 # pick up file adds.
2222 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002223 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002224 if directory:
2225 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002226 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002228 elif IsGitVersionAtLeast('1.7.12'):
2229 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002230 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002231 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002232 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002233 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234 DieWithError('Failed to apply the patch')
2235
2236 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002237 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002238 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2239 cl = Changelist()
2240 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002241 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002242 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002243 else:
2244 print "Patch applied to index."
2245 return 0
2246
2247
2248def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002249 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002250 # Provide a wrapper for git svn rebase to help avoid accidental
2251 # git svn dcommit.
2252 # It's the only command that doesn't use parser at all since we just defer
2253 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002254
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002255 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002256
2257
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002258def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002259 """Fetches the tree status and returns either 'open', 'closed',
2260 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002261 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262 if url:
2263 status = urllib2.urlopen(url).read().lower()
2264 if status.find('closed') != -1 or status == '0':
2265 return 'closed'
2266 elif status.find('open') != -1 or status == '1':
2267 return 'open'
2268 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002269 return 'unset'
2270
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002272def GetTreeStatusReason():
2273 """Fetches the tree status from a json url and returns the message
2274 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002275 url = settings.GetTreeStatusUrl()
2276 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002277 connection = urllib2.urlopen(json_url)
2278 status = json.loads(connection.read())
2279 connection.close()
2280 return status['message']
2281
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002282
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002283def GetBuilderMaster(bot_list):
2284 """For a given builder, fetch the master from AE if available."""
2285 map_url = 'https://builders-map.appspot.com/'
2286 try:
2287 master_map = json.load(urllib2.urlopen(map_url))
2288 except urllib2.URLError as e:
2289 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2290 (map_url, e))
2291 except ValueError as e:
2292 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2293 if not master_map:
2294 return None, 'Failed to build master map.'
2295
2296 result_master = ''
2297 for bot in bot_list:
2298 builder = bot.split(':', 1)[0]
2299 master_list = master_map.get(builder, [])
2300 if not master_list:
2301 return None, ('No matching master for builder %s.' % builder)
2302 elif len(master_list) > 1:
2303 return None, ('The builder name %s exists in multiple masters %s.' %
2304 (builder, master_list))
2305 else:
2306 cur_master = master_list[0]
2307 if not result_master:
2308 result_master = cur_master
2309 elif result_master != cur_master:
2310 return None, 'The builders do not belong to the same master.'
2311 return result_master, None
2312
2313
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002315 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002316 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002317 status = GetTreeStatus()
2318 if 'unset' == status:
2319 print 'You must configure your tree status URL by running "git cl config".'
2320 return 2
2321
2322 print "The tree is %s" % status
2323 print
2324 print GetTreeStatusReason()
2325 if status != 'open':
2326 return 1
2327 return 0
2328
2329
maruel@chromium.org15192402012-09-06 12:38:29 +00002330def CMDtry(parser, args):
2331 """Triggers a try job through Rietveld."""
2332 group = optparse.OptionGroup(parser, "Try job options")
2333 group.add_option(
2334 "-b", "--bot", action="append",
2335 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2336 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002337 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002338 "the try server waterfall for the builders name and the tests "
2339 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002340 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002341 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002342 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002343 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002344 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002345 "-r", "--revision",
2346 help="Revision to use for the try job; default: the "
2347 "revision will be determined by the try server; see "
2348 "its waterfall for more info")
2349 group.add_option(
2350 "-c", "--clobber", action="store_true", default=False,
2351 help="Force a clobber before building; e.g. don't do an "
2352 "incremental build")
2353 group.add_option(
2354 "--project",
2355 help="Override which project to use. Projects are defined "
2356 "server-side to define what default bot set to use")
2357 group.add_option(
2358 "-t", "--testfilter", action="append", default=[],
2359 help=("Apply a testfilter to all the selected builders. Unless the "
2360 "builders configurations are similar, use multiple "
2361 "--bot <builder>:<test> arguments."))
2362 group.add_option(
2363 "-n", "--name", help="Try job name; default to current branch name")
2364 parser.add_option_group(group)
2365 options, args = parser.parse_args(args)
2366
2367 if args:
2368 parser.error('Unknown arguments: %s' % args)
2369
2370 cl = Changelist()
2371 if not cl.GetIssue():
2372 parser.error('Need to upload first')
2373
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002374 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002375 if props.get('closed'):
2376 parser.error('Cannot send tryjobs for a closed CL')
2377
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002378 if props.get('private'):
2379 parser.error('Cannot use trybots with private issue')
2380
maruel@chromium.org15192402012-09-06 12:38:29 +00002381 if not options.name:
2382 options.name = cl.GetBranch()
2383
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002384 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002385 options.master, err_msg = GetBuilderMaster(options.bot)
2386 if err_msg:
2387 parser.error('Tryserver master cannot be found because: %s\n'
2388 'Please manually specify the tryserver master'
2389 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002390
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002391 def GetMasterMap():
2392 # Process --bot and --testfilter.
2393 if not options.bot:
2394 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002395
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002396 # Get try masters from PRESUBMIT.py files.
2397 masters = presubmit_support.DoGetTryMasters(
2398 change,
2399 change.LocalPaths(),
2400 settings.GetRoot(),
2401 None,
2402 None,
2403 options.verbose,
2404 sys.stdout)
2405 if masters:
2406 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002407
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002408 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2409 options.bot = presubmit_support.DoGetTrySlaves(
2410 change,
2411 change.LocalPaths(),
2412 settings.GetRoot(),
2413 None,
2414 None,
2415 options.verbose,
2416 sys.stdout)
2417 if not options.bot:
2418 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002419
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002420 builders_and_tests = {}
2421 # TODO(machenbach): The old style command-line options don't support
2422 # multiple try masters yet.
2423 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2424 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2425
2426 for bot in old_style:
2427 if ':' in bot:
2428 builder, tests = bot.split(':', 1)
2429 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2430 elif ',' in bot:
2431 parser.error('Specify one bot per --bot flag')
2432 else:
2433 builders_and_tests.setdefault(bot, []).append('defaulttests')
2434
2435 for bot, tests in new_style:
2436 builders_and_tests.setdefault(bot, []).extend(tests)
2437
2438 # Return a master map with one master to be backwards compatible. The
2439 # master name defaults to an empty string, which will cause the master
2440 # not to be set on rietveld (deprecated).
2441 return {options.master: builders_and_tests}
2442
2443 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002444
maruel@chromium.org15192402012-09-06 12:38:29 +00002445 if options.testfilter:
2446 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002447 masters = dict((master, dict(
2448 (b, forced_tests) for b, t in slaves.iteritems()
2449 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002450
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002451 for builders in masters.itervalues():
2452 if any('triggered' in b for b in builders):
2453 print >> sys.stderr, (
2454 'ERROR You are trying to send a job to a triggered bot. This type of'
2455 ' bot requires an\ninitial job from a parent (usually a builder). '
2456 'Instead send your job to the parent.\n'
2457 'Bot list: %s' % builders)
2458 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002459
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002460 patchset = cl.GetMostRecentPatchset()
2461 if patchset and patchset != cl.GetPatchset():
2462 print(
2463 '\nWARNING Mismatch between local config and server. Did a previous '
2464 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2465 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002466 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002467 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002468 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002469 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002470 except urllib2.HTTPError, e:
2471 if e.code == 404:
2472 print('404 from rietveld; '
2473 'did you mean to use "git try" instead of "git cl try"?')
2474 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002475 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002476
2477 for (master, builders) in masters.iteritems():
2478 if master:
2479 print 'Master: %s' % master
2480 length = max(len(builder) for builder in builders)
2481 for builder in sorted(builders):
2482 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002483 return 0
2484
2485
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002486@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002487def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002488 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002489 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002490 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002491 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002492
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002493 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002494 if args:
2495 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002496 branch = cl.GetBranch()
2497 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002498 cl = Changelist()
2499 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002500
2501 # Clear configured merge-base, if there is one.
2502 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002503 else:
2504 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002505 return 0
2506
2507
thestig@chromium.org00858c82013-12-02 23:08:03 +00002508def CMDweb(parser, args):
2509 """Opens the current CL in the web browser."""
2510 _, args = parser.parse_args(args)
2511 if args:
2512 parser.error('Unrecognized args: %s' % ' '.join(args))
2513
2514 issue_url = Changelist().GetIssueURL()
2515 if not issue_url:
2516 print >> sys.stderr, 'ERROR No issue to open'
2517 return 1
2518
2519 webbrowser.open(issue_url)
2520 return 0
2521
2522
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002523def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002524 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002525 _, args = parser.parse_args(args)
2526 if args:
2527 parser.error('Unrecognized args: %s' % ' '.join(args))
2528 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002529 props = cl.GetIssueProperties()
2530 if props.get('private'):
2531 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002532 cl.SetFlag('commit', '1')
2533 return 0
2534
2535
groby@chromium.org411034a2013-02-26 15:12:01 +00002536def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002537 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002538 _, args = parser.parse_args(args)
2539 if args:
2540 parser.error('Unrecognized args: %s' % ' '.join(args))
2541 cl = Changelist()
2542 # Ensure there actually is an issue to close.
2543 cl.GetDescription()
2544 cl.CloseIssue()
2545 return 0
2546
2547
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002548def CMDdiff(parser, args):
2549 """shows differences between local tree and last upload."""
2550 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002551 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002552 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002553 if not issue:
2554 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002555 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002556 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002557
2558 # Create a new branch based on the merge-base
2559 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2560 try:
2561 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002562 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002563 if rtn != 0:
2564 return rtn
2565
2566 # Switch back to starting brand and diff against the temporary
2567 # branch containing the latest rietveld patch.
2568 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2569 finally:
2570 RunGit(['checkout', '-q', branch])
2571 RunGit(['branch', '-D', TMP_BRANCH])
2572
2573 return 0
2574
2575
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002576def CMDowners(parser, args):
2577 """interactively find the owners for reviewing"""
2578 parser.add_option(
2579 '--no-color',
2580 action='store_true',
2581 help='Use this option to disable color output')
2582 options, args = parser.parse_args(args)
2583
2584 author = RunGit(['config', 'user.email']).strip() or None
2585
2586 cl = Changelist()
2587
2588 if args:
2589 if len(args) > 1:
2590 parser.error('Unknown args')
2591 base_branch = args[0]
2592 else:
2593 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002594 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002595
2596 change = cl.GetChange(base_branch, None)
2597 return owners_finder.OwnersFinder(
2598 [f.LocalPath() for f in
2599 cl.GetChange(base_branch, None).AffectedFiles()],
2600 change.RepositoryRoot(), author,
2601 fopen=file, os_path=os.path, glob=glob.glob,
2602 disable_color=options.no_color).run()
2603
2604
enne@chromium.org555cfe42014-01-29 18:21:39 +00002605@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002606def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002607 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002608 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002609 parser.add_option('--full', action='store_true',
2610 help='Reformat the full content of all touched files')
2611 parser.add_option('--dry-run', action='store_true',
2612 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002613 parser.add_option('--diff', action='store_true',
2614 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002615 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002616
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002617 # git diff generates paths against the root of the repository. Change
2618 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002619 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002620 if rel_base_path:
2621 os.chdir(rel_base_path)
2622
digit@chromium.org29e47272013-05-17 17:01:46 +00002623 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002624 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002625 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002626 # Only list the names of modified files.
2627 diff_cmd.append('--name-only')
2628 else:
2629 # Only generate context-less patches.
2630 diff_cmd.append('-U0')
2631
2632 # Grab the merge-base commit, i.e. the upstream commit of the current
2633 # branch when it was created or the last time it was rebased. This is
2634 # to cover the case where the user may have called "git fetch origin",
2635 # moving the origin branch to a newer commit, but hasn't rebased yet.
2636 upstream_commit = None
2637 cl = Changelist()
2638 upstream_branch = cl.GetUpstreamBranch()
2639 if upstream_branch:
2640 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2641 upstream_commit = upstream_commit.strip()
2642
2643 if not upstream_commit:
2644 DieWithError('Could not find base commit for this branch. '
2645 'Are you in detached state?')
2646
2647 diff_cmd.append(upstream_commit)
2648
2649 # Handle source file filtering.
2650 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002651 if args:
2652 for arg in args:
2653 if os.path.isdir(arg):
2654 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2655 elif os.path.isfile(arg):
2656 diff_cmd.append(arg)
2657 else:
2658 DieWithError('Argument "%s" is not a file or a directory' % arg)
2659 else:
2660 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002661 diff_output = RunGit(diff_cmd)
2662
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002663 top_dir = os.path.normpath(
2664 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2665
2666 # Locate the clang-format binary in the checkout
2667 try:
2668 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2669 except clang_format.NotFoundError, e:
2670 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002671
digit@chromium.org29e47272013-05-17 17:01:46 +00002672 if opts.full:
2673 # diff_output is a list of files to send to clang-format.
2674 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002675 if not files:
2676 print "Nothing to format."
2677 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002678 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002679 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002680 cmd.append('-i')
2681 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002682 if opts.diff:
2683 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002684 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002685 env = os.environ.copy()
2686 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002687 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002688 try:
2689 script = clang_format.FindClangFormatScriptInChromiumTree(
2690 'clang-format-diff.py')
2691 except clang_format.NotFoundError, e:
2692 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002693
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002694 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002695 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002696 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002697
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002698 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002699 if opts.diff:
2700 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002701 if opts.dry_run and len(stdout) > 0:
2702 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002703
2704 return 0
2705
2706
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002707class OptionParser(optparse.OptionParser):
2708 """Creates the option parse and add --verbose support."""
2709 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002710 optparse.OptionParser.__init__(
2711 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002712 self.add_option(
2713 '-v', '--verbose', action='count', default=0,
2714 help='Use 2 times for more debugging info')
2715
2716 def parse_args(self, args=None, values=None):
2717 options, args = optparse.OptionParser.parse_args(self, args, values)
2718 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2719 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2720 return options, args
2721
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002724 if sys.hexversion < 0x02060000:
2725 print >> sys.stderr, (
2726 '\nYour python version %s is unsupported, please upgrade.\n' %
2727 sys.version.split(' ', 1)[0])
2728 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002729
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002730 # Reload settings.
2731 global settings
2732 settings = Settings()
2733
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002734 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002735 dispatcher = subcommand.CommandDispatcher(__name__)
2736 try:
2737 return dispatcher.execute(OptionParser(), argv)
2738 except urllib2.HTTPError, e:
2739 if e.code != 500:
2740 raise
2741 DieWithError(
2742 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2743 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002744
2745
2746if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002747 # These affect sys.stdout so do it outside of main() to simplify mocks in
2748 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002749 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002750 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002751 sys.exit(main(sys.argv[1:]))