blob: caa9b604b2d54800f8b4d0a0ebc5851d91b930bf [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
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
33try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000034 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035except ImportError:
36 pass
37
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000044import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000045import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000046import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000048from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000049import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000052import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000054import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000055import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000060DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000061POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000063GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000064CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000065REFS_THAT_ALIAS_TO_OTHER_REFS = {
66 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
67 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
68}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
thestig@chromium.org44202a22014-03-11 19:22:18 +000070# Valid extensions for files we want to lint.
71DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
72DEFAULT_LINT_IGNORE_REGEX = r"$^"
73
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000074# Shortcut since it quickly becomes redundant.
75Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000076
maruel@chromium.orgddd59412011-11-30 14:20:38 +000077# Initialized in main()
78settings = None
79
80
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000082 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083 sys.exit(1)
84
85
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000086def GetNoGitPagerEnv():
87 env = os.environ.copy()
88 # 'cat' is a magical git string that disables pagers on all platforms.
89 env['GIT_PAGER'] = 'cat'
90 return env
91
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000092
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000093def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000095 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000096 except subprocess2.CalledProcessError as e:
97 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 'Command "%s" failed.\n%s' % (
101 ' '.join(args), error_message or e.stdout or ''))
102 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103
104
105def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000107 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000110def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000112 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113 if suppress_stderr:
114 stderr = subprocess2.VOID
115 else:
116 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000117 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000118 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 stdout=subprocess2.PIPE,
120 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 return code, out[0]
122 except ValueError:
123 # When the subprocess fails, it returns None. That triggers a ValueError
124 # when trying to unpack the return value into (out, code).
125 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126
127
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000128def RunGitSilent(args):
129 """Returns stdout, suppresses stderr and ingores the return code."""
130 return RunGitWithCode(args, suppress_stderr=True)[1]
131
132
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000133def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000134 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 return (version.startswith(prefix) and
137 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138
139
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000140def BranchExists(branch):
141 """Return True if specified branch exists."""
142 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
143 suppress_stderr=True)
144 return not code
145
146
maruel@chromium.org90541732011-04-01 17:54:18 +0000147def ask_for_data(prompt):
148 try:
149 return raw_input(prompt)
150 except KeyboardInterrupt:
151 # Hide the exception.
152 sys.exit(1)
153
154
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155def git_set_branch_value(key, value):
156 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000157 if not branch:
158 return
159
160 cmd = ['config']
161 if isinstance(value, int):
162 cmd.append('--int')
163 git_key = 'branch.%s.%s' % (branch, key)
164 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000165
166
167def git_get_branch_default(key, default):
168 branch = Changelist().GetBranch()
169 if branch:
170 git_key = 'branch.%s.%s' % (branch, key)
171 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
172 try:
173 return int(stdout.strip())
174 except ValueError:
175 pass
176 return default
177
178
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000179def add_git_similarity(parser):
180 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000181 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182 help='Sets the percentage that a pair of files need to match in order to'
183 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 parser.add_option(
185 '--find-copies', action='store_true',
186 help='Allows git to look for copies.')
187 parser.add_option(
188 '--no-find-copies', action='store_false', dest='find_copies',
189 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000190
191 old_parser_args = parser.parse_args
192 def Parse(args):
193 options, args = old_parser_args(args)
194
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000196 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 print('Note: Saving similarity of %d%% in git config.'
199 % options.similarity)
200 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = max(0, min(options.similarity, 100))
203
204 if options.find_copies is None:
205 options.find_copies = bool(
206 git_get_branch_default('git-find-copies', True))
207 else:
208 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000209
210 print('Using %d%% similarity for rename/copy detection. '
211 'Override with --similarity.' % options.similarity)
212
213 return options, args
214 parser.parse_args = Parse
215
216
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000217def _prefix_master(master):
218 """Convert user-specified master name to full master name.
219
220 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
221 name, while the developers always use shortened master name
222 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
223 function does the conversion for buildbucket migration.
224 """
225 prefix = 'master.'
226 if master.startswith(prefix):
227 return master
228 return '%s%s' % (prefix, master)
229
230
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000231def trigger_try_jobs(auth_config, changelist, options, masters, category,
232 override_properties=None):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000233 rietveld_url = settings.GetDefaultServerUrl()
234 rietveld_host = urlparse.urlparse(rietveld_url).hostname
235 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
236 http = authenticator.authorize(httplib2.Http())
237 http.force_exception_to_status_code = True
238 issue_props = changelist.GetIssueProperties()
239 issue = changelist.GetIssue()
240 patchset = changelist.GetMostRecentPatchset()
241
242 buildbucket_put_url = (
243 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000244 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000245 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
246 hostname=rietveld_host,
247 issue=issue,
248 patch=patchset)
249
250 batch_req_body = {'builds': []}
251 print_text = []
252 print_text.append('Tried jobs on:')
253 for master, builders_and_tests in sorted(masters.iteritems()):
254 print_text.append('Master: %s' % master)
255 bucket = _prefix_master(master)
256 for builder, tests in sorted(builders_and_tests.iteritems()):
257 print_text.append(' %s: %s' % (builder, tests))
258 parameters = {
259 'builder_name': builder,
260 'changes': [
261 {'author': {'email': issue_props['owner_email']}},
262 ],
263 'properties': {
264 'category': category,
265 'issue': issue,
266 'master': master,
267 'patch_project': issue_props['project'],
268 'patch_storage': 'rietveld',
269 'patchset': patchset,
270 'reason': options.name,
271 'revision': options.revision,
272 'rietveld': rietveld_url,
273 'testfilter': tests,
274 },
275 }
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000276 if override_properties:
277 parameters['properties'].update(override_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000278 if options.clobber:
279 parameters['properties']['clobber'] = True
280 batch_req_body['builds'].append(
281 {
282 'bucket': bucket,
283 'parameters_json': json.dumps(parameters),
284 'tags': ['builder:%s' % builder,
285 'buildset:%s' % buildset,
286 'master:%s' % master,
287 'user_agent:git_cl_try']
288 }
289 )
290
291 for try_count in xrange(3):
292 response, content = http.request(
293 buildbucket_put_url,
294 'PUT',
295 body=json.dumps(batch_req_body),
296 headers={'Content-Type': 'application/json'},
297 )
298 content_json = None
299 try:
300 content_json = json.loads(content)
301 except ValueError:
302 pass
303
304 # Buildbucket could return an error even if status==200.
305 if content_json and content_json.get('error'):
306 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
307 content_json['error'].get('code', ''),
308 content_json['error'].get('reason', ''),
309 content_json['error'].get('message', ''))
310 raise BuildbucketResponseException(msg)
311
312 if response.status == 200:
313 if not content_json:
314 raise BuildbucketResponseException(
315 'Buildbucket returns invalid json content: %s.\n'
316 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
317 content)
318 break
319 if response.status < 500 or try_count >= 2:
320 raise httplib2.HttpLib2Error(content)
321
322 # status >= 500 means transient failures.
323 logging.debug('Transient errors when triggering tryjobs. Will retry.')
324 time.sleep(0.5 + 1.5*try_count)
325
326 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000327
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000329def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
330 """Return the corresponding git ref if |base_url| together with |glob_spec|
331 matches the full |url|.
332
333 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
334 """
335 fetch_suburl, as_ref = glob_spec.split(':')
336 if allow_wildcards:
337 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
338 if glob_match:
339 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
340 # "branches/{472,597,648}/src:refs/remotes/svn/*".
341 branch_re = re.escape(base_url)
342 if glob_match.group(1):
343 branch_re += '/' + re.escape(glob_match.group(1))
344 wildcard = glob_match.group(2)
345 if wildcard == '*':
346 branch_re += '([^/]*)'
347 else:
348 # Escape and replace surrounding braces with parentheses and commas
349 # with pipe symbols.
350 wildcard = re.escape(wildcard)
351 wildcard = re.sub('^\\\\{', '(', wildcard)
352 wildcard = re.sub('\\\\,', '|', wildcard)
353 wildcard = re.sub('\\\\}$', ')', wildcard)
354 branch_re += wildcard
355 if glob_match.group(3):
356 branch_re += re.escape(glob_match.group(3))
357 match = re.match(branch_re, url)
358 if match:
359 return re.sub('\*$', match.group(1), as_ref)
360
361 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
362 if fetch_suburl:
363 full_url = base_url + '/' + fetch_suburl
364 else:
365 full_url = base_url
366 if full_url == url:
367 return as_ref
368 return None
369
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000370
iannucci@chromium.org79540052012-10-19 23:15:26 +0000371def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000372 """Prints statistics about the change to the user."""
373 # --no-ext-diff is broken in some versions of Git, so try to work around
374 # this by overriding the environment (but there is still a problem if the
375 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000376 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000377 if 'GIT_EXTERNAL_DIFF' in env:
378 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000379
380 if find_copies:
381 similarity_options = ['--find-copies-harder', '-l100000',
382 '-C%s' % similarity]
383 else:
384 similarity_options = ['-M%s' % similarity]
385
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000386 try:
387 stdout = sys.stdout.fileno()
388 except AttributeError:
389 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000390 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000391 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000392 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000393 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000394
395
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000396class BuildbucketResponseException(Exception):
397 pass
398
399
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000400class Settings(object):
401 def __init__(self):
402 self.default_server = None
403 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000404 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405 self.is_git_svn = None
406 self.svn_branch = None
407 self.tree_status_url = None
408 self.viewvc_url = None
409 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000410 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000411 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000412 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000413 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000414 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415
416 def LazyUpdateIfNeeded(self):
417 """Updates the settings from a codereview.settings file, if available."""
418 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000419 # The only value that actually changes the behavior is
420 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000421 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000422 error_ok=True
423 ).strip().lower()
424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000426 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000427 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000428 # set updated to True to avoid infinite calling loop
429 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000430 self.updated = True
431 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000432 self.updated = True
433
434 def GetDefaultServerUrl(self, error_ok=False):
435 if not self.default_server:
436 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000437 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000438 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000439 if error_ok:
440 return self.default_server
441 if not self.default_server:
442 error_message = ('Could not find settings file. You must configure '
443 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000444 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000445 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 return self.default_server
447
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000448 @staticmethod
449 def GetRelativeRoot():
450 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000453 if self.root is None:
454 self.root = os.path.abspath(self.GetRelativeRoot())
455 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456
457 def GetIsGitSvn(self):
458 """Return true if this repo looks like it's using git-svn."""
459 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000460 if self.GetPendingRefPrefix():
461 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
462 self.is_git_svn = False
463 else:
464 # If you have any "svn-remote.*" config keys, we think you're using svn.
465 self.is_git_svn = RunGitWithCode(
466 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000467 return self.is_git_svn
468
469 def GetSVNBranch(self):
470 if self.svn_branch is None:
471 if not self.GetIsGitSvn():
472 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
473
474 # Try to figure out which remote branch we're based on.
475 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000476 # 1) iterate through our branch history and find the svn URL.
477 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478
479 # regexp matching the git-svn line that contains the URL.
480 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
481
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000482 # We don't want to go through all of history, so read a line from the
483 # pipe at a time.
484 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000485 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000486 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
487 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000488 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000489 for line in proc.stdout:
490 match = git_svn_re.match(line)
491 if match:
492 url = match.group(1)
493 proc.stdout.close() # Cut pipe.
494 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000495
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000496 if url:
497 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
498 remotes = RunGit(['config', '--get-regexp',
499 r'^svn-remote\..*\.url']).splitlines()
500 for remote in remotes:
501 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000502 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000503 remote = match.group(1)
504 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000505 rewrite_root = RunGit(
506 ['config', 'svn-remote.%s.rewriteRoot' % remote],
507 error_ok=True).strip()
508 if rewrite_root:
509 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000510 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000511 ['config', 'svn-remote.%s.fetch' % remote],
512 error_ok=True).strip()
513 if fetch_spec:
514 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
515 if self.svn_branch:
516 break
517 branch_spec = RunGit(
518 ['config', 'svn-remote.%s.branches' % remote],
519 error_ok=True).strip()
520 if branch_spec:
521 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
522 if self.svn_branch:
523 break
524 tag_spec = RunGit(
525 ['config', 'svn-remote.%s.tags' % remote],
526 error_ok=True).strip()
527 if tag_spec:
528 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
529 if self.svn_branch:
530 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000531
532 if not self.svn_branch:
533 DieWithError('Can\'t guess svn branch -- try specifying it on the '
534 'command line')
535
536 return self.svn_branch
537
538 def GetTreeStatusUrl(self, error_ok=False):
539 if not self.tree_status_url:
540 error_message = ('You must configure your tree status URL by running '
541 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000542 self.tree_status_url = self._GetRietveldConfig(
543 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000544 return self.tree_status_url
545
546 def GetViewVCUrl(self):
547 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000548 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549 return self.viewvc_url
550
rmistry@google.com90752582014-01-14 21:04:50 +0000551 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000552 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000553
rmistry@google.com78948ed2015-07-08 23:09:57 +0000554 def GetIsSkipDependencyUpload(self, branch_name):
555 """Returns true if specified branch should skip dep uploads."""
556 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
557 error_ok=True)
558
rmistry@google.com5626a922015-02-26 14:03:30 +0000559 def GetRunPostUploadHook(self):
560 run_post_upload_hook = self._GetRietveldConfig(
561 'run-post-upload-hook', error_ok=True)
562 return run_post_upload_hook == "True"
563
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000564 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000565 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000566
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000567 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000568 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000569
ukai@chromium.orge8077812012-02-03 03:41:46 +0000570 def GetIsGerrit(self):
571 """Return true if this repo is assosiated with gerrit code review system."""
572 if self.is_gerrit is None:
573 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
574 return self.is_gerrit
575
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000576 def GetGitEditor(self):
577 """Return the editor specified in the git config, or None if none is."""
578 if self.git_editor is None:
579 self.git_editor = self._GetConfig('core.editor', error_ok=True)
580 return self.git_editor or None
581
thestig@chromium.org44202a22014-03-11 19:22:18 +0000582 def GetLintRegex(self):
583 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
584 DEFAULT_LINT_REGEX)
585
586 def GetLintIgnoreRegex(self):
587 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
588 DEFAULT_LINT_IGNORE_REGEX)
589
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000590 def GetProject(self):
591 if not self.project:
592 self.project = self._GetRietveldConfig('project', error_ok=True)
593 return self.project
594
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000595 def GetForceHttpsCommitUrl(self):
596 if not self.force_https_commit_url:
597 self.force_https_commit_url = self._GetRietveldConfig(
598 'force-https-commit-url', error_ok=True)
599 return self.force_https_commit_url
600
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000601 def GetPendingRefPrefix(self):
602 if not self.pending_ref_prefix:
603 self.pending_ref_prefix = self._GetRietveldConfig(
604 'pending-ref-prefix', error_ok=True)
605 return self.pending_ref_prefix
606
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000607 def _GetRietveldConfig(self, param, **kwargs):
608 return self._GetConfig('rietveld.' + param, **kwargs)
609
rmistry@google.com78948ed2015-07-08 23:09:57 +0000610 def _GetBranchConfig(self, branch_name, param, **kwargs):
611 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 def _GetConfig(self, param, **kwargs):
614 self.LazyUpdateIfNeeded()
615 return RunGit(['config', param], **kwargs).strip()
616
617
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618def ShortBranchName(branch):
619 """Convert a name like 'refs/heads/foo' to just 'foo'."""
620 return branch.replace('refs/heads/', '')
621
622
623class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000624 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000626 global settings
627 if not settings:
628 # Happens when git_cl.py is used as a utility library.
629 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 settings.GetDefaultServerUrl()
631 self.branchref = branchref
632 if self.branchref:
633 self.branch = ShortBranchName(self.branchref)
634 else:
635 self.branch = None
636 self.rietveld_server = None
637 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000638 self.lookedup_issue = False
639 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640 self.has_description = False
641 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000642 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000644 self.cc = None
645 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000646 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000647 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000648 self._remote = None
649 self._rpc_server = None
650
651 @property
652 def auth_config(self):
653 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000654
655 def GetCCList(self):
656 """Return the users cc'd on this CL.
657
658 Return is a string suitable for passing to gcl with the --cc flag.
659 """
660 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000661 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000662 more_cc = ','.join(self.watchers)
663 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
664 return self.cc
665
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000666 def GetCCListWithoutDefault(self):
667 """Return the users cc'd on this CL excluding default ones."""
668 if self.cc is None:
669 self.cc = ','.join(self.watchers)
670 return self.cc
671
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000672 def SetWatchers(self, watchers):
673 """Set the list of email addresses that should be cc'd based on the changed
674 files in this CL.
675 """
676 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677
678 def GetBranch(self):
679 """Returns the short branch name, e.g. 'master'."""
680 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000681 branchref = RunGit(['symbolic-ref', 'HEAD'],
682 stderr=subprocess2.VOID, error_ok=True).strip()
683 if not branchref:
684 return None
685 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 self.branch = ShortBranchName(self.branchref)
687 return self.branch
688
689 def GetBranchRef(self):
690 """Returns the full branch name, e.g. 'refs/heads/master'."""
691 self.GetBranch() # Poke the lazy loader.
692 return self.branchref
693
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000694 @staticmethod
695 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000696 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 e.g. 'origin', 'refs/heads/master'
698 """
699 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
701 error_ok=True).strip()
702 if upstream_branch:
703 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
704 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000705 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
706 error_ok=True).strip()
707 if upstream_branch:
708 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000710 # Fall back on trying a git-svn upstream branch.
711 if settings.GetIsGitSvn():
712 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000714 # Else, try to guess the origin remote.
715 remote_branches = RunGit(['branch', '-r']).split()
716 if 'origin/master' in remote_branches:
717 # Fall back on origin/master if it exits.
718 remote = 'origin'
719 upstream_branch = 'refs/heads/master'
720 elif 'origin/trunk' in remote_branches:
721 # Fall back on origin/trunk if it exists. Generally a shared
722 # git-svn clone
723 remote = 'origin'
724 upstream_branch = 'refs/heads/trunk'
725 else:
726 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727Either pass complete "git diff"-style arguments, like
728 git cl upload origin/master
729or verify this branch is set up to track another (via the --track argument to
730"git checkout -b ...").""")
731
732 return remote, upstream_branch
733
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000734 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000735 upstream_branch = self.GetUpstreamBranch()
736 if not BranchExists(upstream_branch):
737 DieWithError('The upstream for the current branch (%s) does not exist '
738 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000739 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000740 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 def GetUpstreamBranch(self):
743 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000744 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000746 upstream_branch = upstream_branch.replace('refs/heads/',
747 'refs/remotes/%s/' % remote)
748 upstream_branch = upstream_branch.replace('refs/branch-heads/',
749 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750 self.upstream_branch = upstream_branch
751 return self.upstream_branch
752
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000753 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000754 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000755 remote, branch = None, self.GetBranch()
756 seen_branches = set()
757 while branch not in seen_branches:
758 seen_branches.add(branch)
759 remote, branch = self.FetchUpstreamTuple(branch)
760 branch = ShortBranchName(branch)
761 if remote != '.' or branch.startswith('refs/remotes'):
762 break
763 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000764 remotes = RunGit(['remote'], error_ok=True).split()
765 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000766 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000767 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000768 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000769 logging.warning('Could not determine which remote this change is '
770 'associated with, so defaulting to "%s". This may '
771 'not be what you want. You may prevent this message '
772 'by running "git svn info" as documented here: %s',
773 self._remote,
774 GIT_INSTRUCTIONS_URL)
775 else:
776 logging.warn('Could not determine which remote this change is '
777 'associated with. You may prevent this message by '
778 'running "git svn info" as documented here: %s',
779 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000780 branch = 'HEAD'
781 if branch.startswith('refs/remotes'):
782 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000783 elif branch.startswith('refs/branch-heads/'):
784 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000785 else:
786 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000787 return self._remote
788
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000789 def GitSanityChecks(self, upstream_git_obj):
790 """Checks git repo status and ensures diff is from local commits."""
791
sbc@chromium.org79706062015-01-14 21:18:12 +0000792 if upstream_git_obj is None:
793 if self.GetBranch() is None:
794 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000795 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000796 else:
797 print >> sys.stderr, (
798 'ERROR: no upstream branch')
799 return False
800
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000801 # Verify the commit we're diffing against is in our current branch.
802 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
803 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
804 if upstream_sha != common_ancestor:
805 print >> sys.stderr, (
806 'ERROR: %s is not in the current branch. You may need to rebase '
807 'your tracking branch' % upstream_sha)
808 return False
809
810 # List the commits inside the diff, and verify they are all local.
811 commits_in_diff = RunGit(
812 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
813 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
814 remote_branch = remote_branch.strip()
815 if code != 0:
816 _, remote_branch = self.GetRemoteBranch()
817
818 commits_in_remote = RunGit(
819 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
820
821 common_commits = set(commits_in_diff) & set(commits_in_remote)
822 if common_commits:
823 print >> sys.stderr, (
824 'ERROR: Your diff contains %d commits already in %s.\n'
825 'Run "git log --oneline %s..HEAD" to get a list of commits in '
826 'the diff. If you are using a custom git flow, you can override'
827 ' the reference used for this check with "git config '
828 'gitcl.remotebranch <git-ref>".' % (
829 len(common_commits), remote_branch, upstream_git_obj))
830 return False
831 return True
832
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000833 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000834 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000835
836 Returns None if it is not set.
837 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000838 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
839 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000840
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000841 def GetGitSvnRemoteUrl(self):
842 """Return the configured git-svn remote URL parsed from git svn info.
843
844 Returns None if it is not set.
845 """
846 # URL is dependent on the current directory.
847 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
848 if data:
849 keys = dict(line.split(': ', 1) for line in data.splitlines()
850 if ': ' in line)
851 return keys.get('URL', None)
852 return None
853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 def GetRemoteUrl(self):
855 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
856
857 Returns None if there is no remote.
858 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000859 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000860 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
861
862 # If URL is pointing to a local directory, it is probably a git cache.
863 if os.path.isdir(url):
864 url = RunGit(['config', 'remote.%s.url' % remote],
865 error_ok=True,
866 cwd=url).strip()
867 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868
869 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000870 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000871 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000873 self.issue = int(issue) or None if issue else None
874 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 return self.issue
876
877 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000878 if not self.rietveld_server:
879 # If we're on a branch then get the server potentially associated
880 # with that branch.
881 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000882 rietveld_server_config = self._RietveldServer()
883 if rietveld_server_config:
884 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
885 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000886 if not self.rietveld_server:
887 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888 return self.rietveld_server
889
890 def GetIssueURL(self):
891 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000892 if not self.GetIssue():
893 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
895
896 def GetDescription(self, pretty=False):
897 if not self.has_description:
898 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000899 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000900 try:
901 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000902 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000903 if e.code == 404:
904 DieWithError(
905 ('\nWhile fetching the description for issue %d, received a '
906 '404 (not found)\n'
907 'error. It is likely that you deleted this '
908 'issue on the server. If this is the\n'
909 'case, please run\n\n'
910 ' git cl issue 0\n\n'
911 'to clear the association with the deleted issue. Then run '
912 'this command again.') % issue)
913 else:
914 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000915 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000916 except urllib2.URLError as e:
917 print >> sys.stderr, (
918 'Warning: Failed to retrieve CL description due to network '
919 'failure.')
920 self.description = ''
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 self.has_description = True
923 if pretty:
924 wrapper = textwrap.TextWrapper()
925 wrapper.initial_indent = wrapper.subsequent_indent = ' '
926 return wrapper.fill(self.description)
927 return self.description
928
929 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000930 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000931 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 patchset = RunGit(['config', self._PatchsetSetting()],
933 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000934 self.patchset = int(patchset) or None if patchset else None
935 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 return self.patchset
937
938 def SetPatchset(self, patchset):
939 """Set this branch's patchset. If patchset=0, clears the patchset."""
940 if patchset:
941 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000942 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943 else:
944 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000945 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000946 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000948 def GetMostRecentPatchset(self):
949 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000950
951 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000952 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000953 '/download/issue%s_%s.diff' % (issue, patchset))
954
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000955 def GetIssueProperties(self):
956 if self._props is None:
957 issue = self.GetIssue()
958 if not issue:
959 self._props = {}
960 else:
961 self._props = self.RpcServer().get_issue_properties(issue, True)
962 return self._props
963
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000964 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000965 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000966
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000967 def AddComment(self, message):
968 return self.RpcServer().add_comment(self.GetIssue(), message)
969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 def SetIssue(self, issue):
971 """Set this branch's issue. If issue=0, clears the issue."""
972 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000973 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 RunGit(['config', self._IssueSetting(), str(issue)])
975 if self.rietveld_server:
976 RunGit(['config', self._RietveldServer(), self.rietveld_server])
977 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000978 current_issue = self.GetIssue()
979 if current_issue:
980 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000981 self.issue = None
982 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000984 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 if not self.GitSanityChecks(upstream_branch):
986 DieWithError('\nGit sanity check failure')
987
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000988 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000989 if not root:
990 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000991 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000992
993 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000994 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000995 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000996 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000997 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000998 except subprocess2.CalledProcessError:
999 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001000 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001001 'This branch probably doesn\'t exist anymore. To reset the\n'
1002 'tracking branch, please run\n'
1003 ' git branch --set-upstream %s trunk\n'
1004 'replacing trunk with origin/master or the relevant branch') %
1005 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001006
maruel@chromium.org52424302012-08-29 15:14:30 +00001007 issue = self.GetIssue()
1008 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001009 if issue:
1010 description = self.GetDescription()
1011 else:
1012 # If the change was never uploaded, use the log messages of all commits
1013 # up to the branch point, as git cl upload will prefill the description
1014 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001015 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1016 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001017
1018 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001019 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001020 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001021 name,
1022 description,
1023 absroot,
1024 files,
1025 issue,
1026 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001027 author,
1028 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001029
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001030 def GetStatus(self):
1031 """Apply a rough heuristic to give a simple summary of an issue's review
1032 or CQ status, assuming adherence to a common workflow.
1033
1034 Returns None if no issue for this branch, or one of the following keywords:
1035 * 'error' - error from review tool (including deleted issues)
1036 * 'unsent' - not sent for review
1037 * 'waiting' - waiting for review
1038 * 'reply' - waiting for owner to reply to review
1039 * 'lgtm' - LGTM from at least one approved reviewer
1040 * 'commit' - in the commit queue
1041 * 'closed' - closed
1042 """
1043 if not self.GetIssue():
1044 return None
1045
1046 try:
1047 props = self.GetIssueProperties()
1048 except urllib2.HTTPError:
1049 return 'error'
1050
1051 if props.get('closed'):
1052 # Issue is closed.
1053 return 'closed'
1054 if props.get('commit'):
1055 # Issue is in the commit queue.
1056 return 'commit'
1057
1058 try:
1059 reviewers = self.GetApprovingReviewers()
1060 except urllib2.HTTPError:
1061 return 'error'
1062
1063 if reviewers:
1064 # Was LGTM'ed.
1065 return 'lgtm'
1066
1067 messages = props.get('messages') or []
1068
1069 if not messages:
1070 # No message was sent.
1071 return 'unsent'
1072 if messages[-1]['sender'] != props.get('owner_email'):
1073 # Non-LGTM reply from non-owner
1074 return 'reply'
1075 return 'waiting'
1076
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001077 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001078 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001079
1080 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001081 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001082 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001083 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001084 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001085 except presubmit_support.PresubmitFailure, e:
1086 DieWithError(
1087 ('%s\nMaybe your depot_tools is out of date?\n'
1088 'If all fails, contact maruel@') % e)
1089
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001090 def UpdateDescription(self, description):
1091 self.description = description
1092 return self.RpcServer().update_description(
1093 self.GetIssue(), self.description)
1094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001096 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001097 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001099 def SetFlag(self, flag, value):
1100 """Patchset must match."""
1101 if not self.GetPatchset():
1102 DieWithError('The patchset needs to match. Send another patchset.')
1103 try:
1104 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001105 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001106 except urllib2.HTTPError, e:
1107 if e.code == 404:
1108 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1109 if e.code == 403:
1110 DieWithError(
1111 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1112 'match?') % (self.GetIssue(), self.GetPatchset()))
1113 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001115 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 """Returns an upload.RpcServer() to access this review's rietveld instance.
1117 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001118 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001119 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001120 self.GetRietveldServer(),
1121 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001122 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123
1124 def _IssueSetting(self):
1125 """Return the git setting that stores this change's issue."""
1126 return 'branch.%s.rietveldissue' % self.GetBranch()
1127
1128 def _PatchsetSetting(self):
1129 """Return the git setting that stores this change's most recent patchset."""
1130 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1131
1132 def _RietveldServer(self):
1133 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001134 branch = self.GetBranch()
1135 if branch:
1136 return 'branch.%s.rietveldserver' % branch
1137 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138
1139
1140def GetCodereviewSettingsInteractively():
1141 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001142 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 server = settings.GetDefaultServerUrl(error_ok=True)
1144 prompt = 'Rietveld server (host[:port])'
1145 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001146 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 if not server and not newserver:
1148 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001149 if newserver:
1150 newserver = gclient_utils.UpgradeToHttps(newserver)
1151 if newserver != server:
1152 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001154 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 prompt = caption
1156 if initial:
1157 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001158 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 if new_val == 'x':
1160 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001161 elif new_val:
1162 if is_url:
1163 new_val = gclient_utils.UpgradeToHttps(new_val)
1164 if new_val != initial:
1165 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001167 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001168 SetProperty(settings.GetDefaultPrivateFlag(),
1169 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001171 'tree-status-url', False)
1172 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001173 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001174 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1175 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
1177 # TODO: configure a default branch to diff against, rather than this
1178 # svn-based hackery.
1179
1180
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001181class ChangeDescription(object):
1182 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001183 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001184 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001185
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001186 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001187 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001188
agable@chromium.org42c20792013-09-12 17:34:49 +00001189 @property # www.logilab.org/ticket/89786
1190 def description(self): # pylint: disable=E0202
1191 return '\n'.join(self._description_lines)
1192
1193 def set_description(self, desc):
1194 if isinstance(desc, basestring):
1195 lines = desc.splitlines()
1196 else:
1197 lines = [line.rstrip() for line in desc]
1198 while lines and not lines[0]:
1199 lines.pop(0)
1200 while lines and not lines[-1]:
1201 lines.pop(-1)
1202 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001203
piman@chromium.org336f9122014-09-04 02:16:55 +00001204 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001205 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001206 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001207 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001208 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001209 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001210
agable@chromium.org42c20792013-09-12 17:34:49 +00001211 # Get the set of R= and TBR= lines and remove them from the desciption.
1212 regexp = re.compile(self.R_LINE)
1213 matches = [regexp.match(line) for line in self._description_lines]
1214 new_desc = [l for i, l in enumerate(self._description_lines)
1215 if not matches[i]]
1216 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001217
agable@chromium.org42c20792013-09-12 17:34:49 +00001218 # Construct new unified R= and TBR= lines.
1219 r_names = []
1220 tbr_names = []
1221 for match in matches:
1222 if not match:
1223 continue
1224 people = cleanup_list([match.group(2).strip()])
1225 if match.group(1) == 'TBR':
1226 tbr_names.extend(people)
1227 else:
1228 r_names.extend(people)
1229 for name in r_names:
1230 if name not in reviewers:
1231 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001232 if add_owners_tbr:
1233 owners_db = owners.Database(change.RepositoryRoot(),
1234 fopen=file, os_path=os.path, glob=glob.glob)
1235 all_reviewers = set(tbr_names + reviewers)
1236 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1237 all_reviewers)
1238 tbr_names.extend(owners_db.reviewers_for(missing_files,
1239 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001240 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1241 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1242
1243 # Put the new lines in the description where the old first R= line was.
1244 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1245 if 0 <= line_loc < len(self._description_lines):
1246 if new_tbr_line:
1247 self._description_lines.insert(line_loc, new_tbr_line)
1248 if new_r_line:
1249 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001250 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001251 if new_r_line:
1252 self.append_footer(new_r_line)
1253 if new_tbr_line:
1254 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001255
1256 def prompt(self):
1257 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001258 self.set_description([
1259 '# Enter a description of the change.',
1260 '# This will be displayed on the codereview site.',
1261 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001262 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001263 '--------------------',
1264 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001265
agable@chromium.org42c20792013-09-12 17:34:49 +00001266 regexp = re.compile(self.BUG_LINE)
1267 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001268 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001269 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001270 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001271 if not content:
1272 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001273 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001274
1275 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001276 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1277 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001278 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001279 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001280
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001281 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001282 if self._description_lines:
1283 # Add an empty line if either the last line or the new line isn't a tag.
1284 last_line = self._description_lines[-1]
1285 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1286 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1287 self._description_lines.append('')
1288 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001289
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001290 def get_reviewers(self):
1291 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001292 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1293 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001294 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001295
1296
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001297def get_approving_reviewers(props):
1298 """Retrieves the reviewers that approved a CL from the issue properties with
1299 messages.
1300
1301 Note that the list may contain reviewers that are not committer, thus are not
1302 considered by the CQ.
1303 """
1304 return sorted(
1305 set(
1306 message['sender']
1307 for message in props['messages']
1308 if message['approval'] and message['sender'] in props['reviewers']
1309 )
1310 )
1311
1312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313def FindCodereviewSettingsFile(filename='codereview.settings'):
1314 """Finds the given file starting in the cwd and going up.
1315
1316 Only looks up to the top of the repository unless an
1317 'inherit-review-settings-ok' file exists in the root of the repository.
1318 """
1319 inherit_ok_file = 'inherit-review-settings-ok'
1320 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001321 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1323 root = '/'
1324 while True:
1325 if filename in os.listdir(cwd):
1326 if os.path.isfile(os.path.join(cwd, filename)):
1327 return open(os.path.join(cwd, filename))
1328 if cwd == root:
1329 break
1330 cwd = os.path.dirname(cwd)
1331
1332
1333def LoadCodereviewSettingsFromFile(fileobj):
1334 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001335 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 def SetProperty(name, setting, unset_error_ok=False):
1338 fullname = 'rietveld.' + name
1339 if setting in keyvals:
1340 RunGit(['config', fullname, keyvals[setting]])
1341 else:
1342 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1343
1344 SetProperty('server', 'CODE_REVIEW_SERVER')
1345 # Only server setting is required. Other settings can be absent.
1346 # In that case, we ignore errors raised during option deletion attempt.
1347 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001348 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1350 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001351 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001352 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001353 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1354 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001355 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001356 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001357 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001358 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1359 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001361 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001362 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001363
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1365 #should be of the form
1366 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1367 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1368 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1369 keyvals['ORIGIN_URL_CONFIG']])
1370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001372def urlretrieve(source, destination):
1373 """urllib is broken for SSL connections via a proxy therefore we
1374 can't use urllib.urlretrieve()."""
1375 with open(destination, 'w') as f:
1376 f.write(urllib2.urlopen(source).read())
1377
1378
ukai@chromium.org712d6102013-11-27 00:52:58 +00001379def hasSheBang(fname):
1380 """Checks fname is a #! script."""
1381 with open(fname) as f:
1382 return f.read(2).startswith('#!')
1383
1384
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001385def DownloadHooks(force):
1386 """downloads hooks
1387
1388 Args:
1389 force: True to update hooks. False to install hooks if not present.
1390 """
1391 if not settings.GetIsGerrit():
1392 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001393 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001394 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1395 if not os.access(dst, os.X_OK):
1396 if os.path.exists(dst):
1397 if not force:
1398 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001399 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001400 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001401 if not hasSheBang(dst):
1402 DieWithError('Not a script: %s\n'
1403 'You need to download from\n%s\n'
1404 'into .git/hooks/commit-msg and '
1405 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001406 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1407 except Exception:
1408 if os.path.exists(dst):
1409 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001410 DieWithError('\nFailed to download hooks.\n'
1411 'You need to download from\n%s\n'
1412 'into .git/hooks/commit-msg and '
1413 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001414
1415
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001416@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001418 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001420 parser.add_option('--activate-update', action='store_true',
1421 help='activate auto-updating [rietveld] section in '
1422 '.git/config')
1423 parser.add_option('--deactivate-update', action='store_true',
1424 help='deactivate auto-updating [rietveld] section in '
1425 '.git/config')
1426 options, args = parser.parse_args(args)
1427
1428 if options.deactivate_update:
1429 RunGit(['config', 'rietveld.autoupdate', 'false'])
1430 return
1431
1432 if options.activate_update:
1433 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1434 return
1435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 if len(args) == 0:
1437 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001438 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 return 0
1440
1441 url = args[0]
1442 if not url.endswith('codereview.settings'):
1443 url = os.path.join(url, 'codereview.settings')
1444
1445 # Load code review settings and download hooks (if available).
1446 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001447 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448 return 0
1449
1450
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001451def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001452 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001453 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1454 branch = ShortBranchName(branchref)
1455 _, args = parser.parse_args(args)
1456 if not args:
1457 print("Current base-url:")
1458 return RunGit(['config', 'branch.%s.base-url' % branch],
1459 error_ok=False).strip()
1460 else:
1461 print("Setting base-url to %s" % args[0])
1462 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1463 error_ok=False).strip()
1464
1465
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001466def color_for_status(status):
1467 """Maps a Changelist status to color, for CMDstatus and other tools."""
1468 return {
1469 'unsent': Fore.RED,
1470 'waiting': Fore.BLUE,
1471 'reply': Fore.YELLOW,
1472 'lgtm': Fore.GREEN,
1473 'commit': Fore.MAGENTA,
1474 'closed': Fore.CYAN,
1475 'error': Fore.WHITE,
1476 }.get(status, Fore.WHITE)
1477
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001478def fetch_cl_status(branch, auth_config=None):
1479 """Fetches information for an issue and returns (branch, issue, status)."""
1480 cl = Changelist(branchref=branch, auth_config=auth_config)
1481 url = cl.GetIssueURL()
1482 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001483
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001484 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001485 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001486 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001487
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001488 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001489
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001490def get_cl_statuses(
1491 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001492 """Returns a blocking iterable of (branch, issue, color) for given branches.
1493
1494 If fine_grained is true, this will fetch CL statuses from the server.
1495 Otherwise, simply indicate if there's a matching url for the given branches.
1496
1497 If max_processes is specified, it is used as the maximum number of processes
1498 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1499 spawned.
1500 """
1501 # Silence upload.py otherwise it becomes unwieldly.
1502 upload.verbosity = 0
1503
1504 if fine_grained:
1505 # Process one branch synchronously to work through authentication, then
1506 # spawn processes to process all the other branches in parallel.
1507 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001508 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1509 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001510
1511 branches_to_fetch = branches[1:]
1512 pool = ThreadPool(
1513 min(max_processes, len(branches_to_fetch))
1514 if max_processes is not None
1515 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001516 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001517 yield x
1518 else:
1519 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1520 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001521 cl = Changelist(branchref=b, auth_config=auth_config)
1522 url = cl.GetIssueURL()
1523 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001524
rmistry@google.com2dd99862015-06-22 12:22:18 +00001525
1526def upload_branch_deps(cl, args):
1527 """Uploads CLs of local branches that are dependents of the current branch.
1528
1529 If the local branch dependency tree looks like:
1530 test1 -> test2.1 -> test3.1
1531 -> test3.2
1532 -> test2.2 -> test3.3
1533
1534 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1535 run on the dependent branches in this order:
1536 test2.1, test3.1, test3.2, test2.2, test3.3
1537
1538 Note: This function does not rebase your local dependent branches. Use it when
1539 you make a change to the parent branch that will not conflict with its
1540 dependent branches, and you would like their dependencies updated in
1541 Rietveld.
1542 """
1543 if git_common.is_dirty_git_tree('upload-branch-deps'):
1544 return 1
1545
1546 root_branch = cl.GetBranch()
1547 if root_branch is None:
1548 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1549 'Get on a branch!')
1550 if not cl.GetIssue() or not cl.GetPatchset():
1551 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1552 'patchset dependencies without an uploaded CL.')
1553
1554 branches = RunGit(['for-each-ref',
1555 '--format=%(refname:short) %(upstream:short)',
1556 'refs/heads'])
1557 if not branches:
1558 print('No local branches found.')
1559 return 0
1560
1561 # Create a dictionary of all local branches to the branches that are dependent
1562 # on it.
1563 tracked_to_dependents = collections.defaultdict(list)
1564 for b in branches.splitlines():
1565 tokens = b.split()
1566 if len(tokens) == 2:
1567 branch_name, tracked = tokens
1568 tracked_to_dependents[tracked].append(branch_name)
1569
1570 print
1571 print 'The dependent local branches of %s are:' % root_branch
1572 dependents = []
1573 def traverse_dependents_preorder(branch, padding=''):
1574 dependents_to_process = tracked_to_dependents.get(branch, [])
1575 padding += ' '
1576 for dependent in dependents_to_process:
1577 print '%s%s' % (padding, dependent)
1578 dependents.append(dependent)
1579 traverse_dependents_preorder(dependent, padding)
1580 traverse_dependents_preorder(root_branch)
1581 print
1582
1583 if not dependents:
1584 print 'There are no dependent local branches for %s' % root_branch
1585 return 0
1586
1587 print ('This command will checkout all dependent branches and run '
1588 '"git cl upload".')
1589 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1590
1591 # Add a default patchset title to all upload calls.
1592 args.extend(['-t', 'Updated patchset dependency'])
1593 # Record all dependents that failed to upload.
1594 failures = {}
1595 # Go through all dependents, checkout the branch and upload.
1596 try:
1597 for dependent_branch in dependents:
1598 print
1599 print '--------------------------------------'
1600 print 'Running "git cl upload" from %s:' % dependent_branch
1601 RunGit(['checkout', '-q', dependent_branch])
1602 print
1603 try:
1604 if CMDupload(OptionParser(), args) != 0:
1605 print 'Upload failed for %s!' % dependent_branch
1606 failures[dependent_branch] = 1
1607 except: # pylint: disable=W0702
1608 failures[dependent_branch] = 1
1609 print
1610 finally:
1611 # Swap back to the original root branch.
1612 RunGit(['checkout', '-q', root_branch])
1613
1614 print
1615 print 'Upload complete for dependent branches!'
1616 for dependent_branch in dependents:
1617 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1618 print ' %s : %s' % (dependent_branch, upload_status)
1619 print
1620
1621 return 0
1622
1623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001625 """Show status of changelists.
1626
1627 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001628 - Red not sent for review or broken
1629 - Blue waiting for review
1630 - Yellow waiting for you to reply to review
1631 - Green LGTM'ed
1632 - Magenta in the commit queue
1633 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001634
1635 Also see 'git cl comments'.
1636 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001637 parser.add_option('--field',
1638 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001639 parser.add_option('-f', '--fast', action='store_true',
1640 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001641 parser.add_option(
1642 '-j', '--maxjobs', action='store', type=int,
1643 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001644
1645 auth.add_auth_options(parser)
1646 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001647 if args:
1648 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001649 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001652 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 if options.field.startswith('desc'):
1654 print cl.GetDescription()
1655 elif options.field == 'id':
1656 issueid = cl.GetIssue()
1657 if issueid:
1658 print issueid
1659 elif options.field == 'patch':
1660 patchset = cl.GetPatchset()
1661 if patchset:
1662 print patchset
1663 elif options.field == 'url':
1664 url = cl.GetIssueURL()
1665 if url:
1666 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001667 return 0
1668
1669 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1670 if not branches:
1671 print('No local branch found.')
1672 return 0
1673
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 changes = (
1675 Changelist(branchref=b, auth_config=auth_config)
1676 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001677 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001678 alignment = max(5, max(len(b) for b in branches))
1679 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680 output = get_cl_statuses(branches,
1681 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001682 max_processes=options.maxjobs,
1683 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001684
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001685 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001686 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001687 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001688 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001689 b, i, status = output.next()
1690 branch_statuses[b] = (i, status)
1691 issue_url, status = branch_statuses.pop(branch)
1692 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001693 reset = Fore.RESET
1694 if not sys.stdout.isatty():
1695 color = ''
1696 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001697 status_str = '(%s)' % status if status else ''
1698 print ' %*s : %s%s %s%s' % (
1699 alignment, ShortBranchName(branch), color, issue_url, status_str,
1700 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001701
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001702 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001703 print
1704 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001705 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001706 if not cl.GetIssue():
1707 print 'No issue assigned.'
1708 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001709 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001710 if not options.fast:
1711 print 'Issue description:'
1712 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 return 0
1714
1715
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001716def colorize_CMDstatus_doc():
1717 """To be called once in main() to add colors to git cl status help."""
1718 colors = [i for i in dir(Fore) if i[0].isupper()]
1719
1720 def colorize_line(line):
1721 for color in colors:
1722 if color in line.upper():
1723 # Extract whitespaces first and the leading '-'.
1724 indent = len(line) - len(line.lstrip(' ')) + 1
1725 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1726 return line
1727
1728 lines = CMDstatus.__doc__.splitlines()
1729 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1730
1731
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001732@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001734 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735
1736 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001737 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001738 parser.add_option('-r', '--reverse', action='store_true',
1739 help='Lookup the branch(es) for the specified issues. If '
1740 'no issues are specified, all branches with mapped '
1741 'issues will be listed.')
1742 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743
dnj@chromium.org406c4402015-03-03 17:22:28 +00001744 if options.reverse:
1745 branches = RunGit(['for-each-ref', 'refs/heads',
1746 '--format=%(refname:short)']).splitlines()
1747
1748 # Reverse issue lookup.
1749 issue_branch_map = {}
1750 for branch in branches:
1751 cl = Changelist(branchref=branch)
1752 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1753 if not args:
1754 args = sorted(issue_branch_map.iterkeys())
1755 for issue in args:
1756 if not issue:
1757 continue
1758 print 'Branch for issue number %s: %s' % (
1759 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1760 else:
1761 cl = Changelist()
1762 if len(args) > 0:
1763 try:
1764 issue = int(args[0])
1765 except ValueError:
1766 DieWithError('Pass a number to set the issue or none to list it.\n'
1767 'Maybe you want to run git cl status?')
1768 cl.SetIssue(issue)
1769 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770 return 0
1771
1772
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001773def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001774 """Shows or posts review comments for any changelist."""
1775 parser.add_option('-a', '--add-comment', dest='comment',
1776 help='comment to add to an issue')
1777 parser.add_option('-i', dest='issue',
1778 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001779 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001780 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001781 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001782
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001783 issue = None
1784 if options.issue:
1785 try:
1786 issue = int(options.issue)
1787 except ValueError:
1788 DieWithError('A review issue id is expected to be a number')
1789
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001790 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001791
1792 if options.comment:
1793 cl.AddComment(options.comment)
1794 return 0
1795
1796 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001797 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001798 if message['disapproval']:
1799 color = Fore.RED
1800 elif message['approval']:
1801 color = Fore.GREEN
1802 elif message['sender'] == data['owner_email']:
1803 color = Fore.MAGENTA
1804 else:
1805 color = Fore.BLUE
1806 print '\n%s%s %s%s' % (
1807 color, message['date'].split('.', 1)[0], message['sender'],
1808 Fore.RESET)
1809 if message['text'].strip():
1810 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001811 return 0
1812
1813
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001814def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001815 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001816 parser.add_option('-d', '--display', action='store_true',
1817 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001818 auth.add_auth_options(parser)
1819 options, _ = parser.parse_args(args)
1820 auth_config = auth.extract_auth_config_from_options(options)
1821 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001822 if not cl.GetIssue():
1823 DieWithError('This branch has no associated changelist.')
1824 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001825 if options.display:
1826 print description.description
1827 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001828 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001829 if cl.GetDescription() != description.description:
1830 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001831 return 0
1832
1833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834def CreateDescriptionFromLog(args):
1835 """Pulls out the commit log to use as a base for the CL description."""
1836 log_args = []
1837 if len(args) == 1 and not args[0].endswith('.'):
1838 log_args = [args[0] + '..']
1839 elif len(args) == 1 and args[0].endswith('...'):
1840 log_args = [args[0][:-1]]
1841 elif len(args) == 2:
1842 log_args = [args[0] + '..' + args[1]]
1843 else:
1844 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001845 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846
1847
thestig@chromium.org44202a22014-03-11 19:22:18 +00001848def CMDlint(parser, args):
1849 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001850 parser.add_option('--filter', action='append', metavar='-x,+y',
1851 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001852 auth.add_auth_options(parser)
1853 options, args = parser.parse_args(args)
1854 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001855
1856 # Access to a protected member _XX of a client class
1857 # pylint: disable=W0212
1858 try:
1859 import cpplint
1860 import cpplint_chromium
1861 except ImportError:
1862 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1863 return 1
1864
1865 # Change the current working directory before calling lint so that it
1866 # shows the correct base.
1867 previous_cwd = os.getcwd()
1868 os.chdir(settings.GetRoot())
1869 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001870 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001871 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1872 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001873 if not files:
1874 print "Cannot lint an empty CL"
1875 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001876
1877 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001878 command = args + files
1879 if options.filter:
1880 command = ['--filter=' + ','.join(options.filter)] + command
1881 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001882
1883 white_regex = re.compile(settings.GetLintRegex())
1884 black_regex = re.compile(settings.GetLintIgnoreRegex())
1885 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1886 for filename in filenames:
1887 if white_regex.match(filename):
1888 if black_regex.match(filename):
1889 print "Ignoring file %s" % filename
1890 else:
1891 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1892 extra_check_functions)
1893 else:
1894 print "Skipping file %s" % filename
1895 finally:
1896 os.chdir(previous_cwd)
1897 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1898 if cpplint._cpplint_state.error_count != 0:
1899 return 1
1900 return 0
1901
1902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001904 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001905 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001907 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001908 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001909 auth.add_auth_options(parser)
1910 options, args = parser.parse_args(args)
1911 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912
sbc@chromium.org71437c02015-04-09 19:29:40 +00001913 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001914 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 return 1
1916
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001917 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 if args:
1919 base_branch = args[0]
1920 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001921 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001922 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001924 cl.RunHook(
1925 committing=not options.upload,
1926 may_prompt=False,
1927 verbose=options.verbose,
1928 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001929 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930
1931
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001932def AddChangeIdToCommitMessage(options, args):
1933 """Re-commits using the current message, assumes the commit hook is in
1934 place.
1935 """
1936 log_desc = options.message or CreateDescriptionFromLog(args)
1937 git_command = ['commit', '--amend', '-m', log_desc]
1938 RunGit(git_command)
1939 new_log_desc = CreateDescriptionFromLog(args)
1940 if CHANGE_ID in new_log_desc:
1941 print 'git-cl: Added Change-Id to commit message.'
1942 else:
1943 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1944
1945
piman@chromium.org336f9122014-09-04 02:16:55 +00001946def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001947 """upload the current branch to gerrit."""
1948 # We assume the remote called "origin" is the one we want.
1949 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001950 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001951
1952 remote, remote_branch = cl.GetRemoteBranch()
1953 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1954 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001956 change_desc = ChangeDescription(
1957 options.message or CreateDescriptionFromLog(args))
1958 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001959 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001961
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001962 if options.squash:
1963 # Try to get the message from a previous upload.
1964 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1965 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1966 if not message:
1967 if not options.force:
1968 change_desc.prompt()
1969
1970 if CHANGE_ID not in change_desc.description:
1971 # Run the commit-msg hook without modifying the head commit by writing
1972 # the commit message to a temporary file and running the hook over it,
1973 # then reading the file back in.
1974 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1975 'commit-msg')
1976 file_handle, msg_file = tempfile.mkstemp(text=True,
1977 prefix='commit_msg')
1978 try:
1979 try:
1980 with os.fdopen(file_handle, 'w') as fileobj:
1981 fileobj.write(change_desc.description)
1982 finally:
1983 os.close(file_handle)
1984 RunCommand([commit_msg_hook, msg_file])
1985 change_desc.set_description(gclient_utils.FileRead(msg_file))
1986 finally:
1987 os.remove(msg_file)
1988
1989 if not change_desc.description:
1990 print "Description is empty; aborting."
1991 return 1
1992
1993 message = change_desc.description
1994
1995 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1996 if remote is '.':
1997 # If our upstream branch is local, we base our squashed commit on its
1998 # squashed version.
1999 parent = ('refs/heads/git_cl_uploads/' +
2000 scm.GIT.ShortBranchName(upstream_branch))
2001
2002 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2003 # will create additional CLs when uploading.
2004 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2005 RunGitSilent(['rev-parse', parent + ':'])):
2006 print 'Upload upstream branch ' + upstream_branch + ' first.'
2007 return 1
2008 else:
2009 parent = cl.GetCommonAncestorWithUpstream()
2010
2011 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2012 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2013 '-m', message]).strip()
2014 else:
2015 if CHANGE_ID not in change_desc.description:
2016 AddChangeIdToCommitMessage(options, args)
2017 ref_to_push = 'HEAD'
2018 parent = '%s/%s' % (gerrit_remote, branch)
2019
2020 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2021 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002022 if len(commits) > 1:
2023 print('WARNING: This will upload %d commits. Run the following command '
2024 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002025 print('git log %s..%s' % (parent, ref_to_push))
2026 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002027 'commit.')
2028 ask_for_data('About to upload; enter to confirm.')
2029
piman@chromium.org336f9122014-09-04 02:16:55 +00002030 if options.reviewers or options.tbr_owners:
2031 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
ukai@chromium.orge8077812012-02-03 03:41:46 +00002033 receive_options = []
2034 cc = cl.GetCCList().split(',')
2035 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002036 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002037 cc = filter(None, cc)
2038 if cc:
2039 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002040 if change_desc.get_reviewers():
2041 receive_options.extend(
2042 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
ukai@chromium.orge8077812012-02-03 03:41:46 +00002044 git_command = ['push']
2045 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002046 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002047 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002048 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002049 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002050
2051 if options.squash:
2052 head = RunGit(['rev-parse', 'HEAD']).strip()
2053 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2054
ukai@chromium.orge8077812012-02-03 03:41:46 +00002055 # TODO(ukai): parse Change-Id: and set issue number?
2056 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058
wittman@chromium.org455dc922015-01-26 20:15:50 +00002059def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2060 """Computes the remote branch ref to use for the CL.
2061
2062 Args:
2063 remote (str): The git remote for the CL.
2064 remote_branch (str): The git remote branch for the CL.
2065 target_branch (str): The target branch specified by the user.
2066 pending_prefix (str): The pending prefix from the settings.
2067 """
2068 if not (remote and remote_branch):
2069 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002070
wittman@chromium.org455dc922015-01-26 20:15:50 +00002071 if target_branch:
2072 # Cannonicalize branch references to the equivalent local full symbolic
2073 # refs, which are then translated into the remote full symbolic refs
2074 # below.
2075 if '/' not in target_branch:
2076 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2077 else:
2078 prefix_replacements = (
2079 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2080 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2081 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2082 )
2083 match = None
2084 for regex, replacement in prefix_replacements:
2085 match = re.search(regex, target_branch)
2086 if match:
2087 remote_branch = target_branch.replace(match.group(0), replacement)
2088 break
2089 if not match:
2090 # This is a branch path but not one we recognize; use as-is.
2091 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002092 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2093 # Handle the refs that need to land in different refs.
2094 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002095
wittman@chromium.org455dc922015-01-26 20:15:50 +00002096 # Create the true path to the remote branch.
2097 # Does the following translation:
2098 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2099 # * refs/remotes/origin/master -> refs/heads/master
2100 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2101 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2102 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2103 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2104 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2105 'refs/heads/')
2106 elif remote_branch.startswith('refs/remotes/branch-heads'):
2107 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2108 # If a pending prefix exists then replace refs/ with it.
2109 if pending_prefix:
2110 remote_branch = remote_branch.replace('refs/', pending_prefix)
2111 return remote_branch
2112
2113
piman@chromium.org336f9122014-09-04 02:16:55 +00002114def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002115 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002116 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2117 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002118 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 if options.emulate_svn_auto_props:
2120 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121
2122 change_desc = None
2123
pgervais@chromium.org91141372014-01-09 23:27:20 +00002124 if options.email is not None:
2125 upload_args.extend(['--email', options.email])
2126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002128 if options.title:
2129 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002130 if options.message:
2131 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002132 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133 print ("This branch is associated with issue %s. "
2134 "Adding patch to that issue." % cl.GetIssue())
2135 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002136 if options.title:
2137 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002138 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002139 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002140 if options.reviewers or options.tbr_owners:
2141 change_desc.update_reviewers(options.reviewers,
2142 options.tbr_owners,
2143 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002144 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002145 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002146
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002147 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 print "Description is empty; aborting."
2149 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002150
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002151 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002152 if change_desc.get_reviewers():
2153 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002154 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002155 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002156 DieWithError("Must specify reviewers to send email.")
2157 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002158
2159 # We check this before applying rietveld.private assuming that in
2160 # rietveld.cc only addresses which we can send private CLs to are listed
2161 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2162 # --private is specified explicitly on the command line.
2163 if options.private:
2164 logging.warn('rietveld.cc is ignored since private flag is specified. '
2165 'You need to review and add them manually if necessary.')
2166 cc = cl.GetCCListWithoutDefault()
2167 else:
2168 cc = cl.GetCCList()
2169 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002170 if cc:
2171 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002172
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002173 if options.private or settings.GetDefaultPrivateFlag() == "True":
2174 upload_args.append('--private')
2175
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002176 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002177 if not options.find_copies:
2178 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002179
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180 # Include the upstream repo's URL in the change -- this is useful for
2181 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002182 remote_url = cl.GetGitBaseUrlFromConfig()
2183 if not remote_url:
2184 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002185 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002186 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002187 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2188 remote_url = (cl.GetRemoteUrl() + '@'
2189 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 if remote_url:
2191 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002192 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002193 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2194 settings.GetPendingRefPrefix())
2195 if target_ref:
2196 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002198 # Look for dependent patchsets. See crbug.com/480453 for more details.
2199 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2200 upstream_branch = ShortBranchName(upstream_branch)
2201 if remote is '.':
2202 # A local branch is being tracked.
2203 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002204 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002205 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002206 print ('Skipping dependency patchset upload because git config '
2207 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002208 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002209 else:
2210 auth_config = auth.extract_auth_config_from_options(options)
2211 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2212 branch_cl_issue_url = branch_cl.GetIssueURL()
2213 branch_cl_issue = branch_cl.GetIssue()
2214 branch_cl_patchset = branch_cl.GetPatchset()
2215 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2216 upload_args.extend(
2217 ['--depends_on_patchset', '%s:%s' % (
2218 branch_cl_issue, branch_cl_patchset)])
2219 print
2220 print ('The current branch (%s) is tracking a local branch (%s) with '
2221 'an associated CL.') % (cl.GetBranch(), local_branch)
2222 print 'Adding %s/#ps%s as a dependency patchset.' % (
2223 branch_cl_issue_url, branch_cl_patchset)
2224 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002225
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002226 project = settings.GetProject()
2227 if project:
2228 upload_args.extend(['--project', project])
2229
rmistry@google.comef966222015-04-07 11:15:01 +00002230 if options.cq_dry_run:
2231 upload_args.extend(['--cq_dry_run'])
2232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002234 upload_args = ['upload'] + upload_args + args
2235 logging.info('upload.RealMain(%s)', upload_args)
2236 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002237 issue = int(issue)
2238 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002239 except KeyboardInterrupt:
2240 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002241 except:
2242 # If we got an exception after the user typed a description for their
2243 # change, back up the description before re-raising.
2244 if change_desc:
2245 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2246 print '\nGot exception while uploading -- saving description to %s\n' \
2247 % backup_path
2248 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002249 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002250 backup_file.close()
2251 raise
2252
2253 if not cl.GetIssue():
2254 cl.SetIssue(issue)
2255 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002256
2257 if options.use_commit_queue:
2258 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002259 return 0
2260
2261
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002262def cleanup_list(l):
2263 """Fixes a list so that comma separated items are put as individual items.
2264
2265 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2266 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2267 """
2268 items = sum((i.split(',') for i in l), [])
2269 stripped_items = (i.strip() for i in items)
2270 return sorted(filter(None, stripped_items))
2271
2272
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002273@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002274def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002275 """Uploads the current changelist to codereview.
2276
2277 Can skip dependency patchset uploads for a branch by running:
2278 git config branch.branch_name.skip-deps-uploads True
2279 To unset run:
2280 git config --unset branch.branch_name.skip-deps-uploads
2281 Can also set the above globally by using the --global flag.
2282 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002283 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2284 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002285 parser.add_option('--bypass-watchlists', action='store_true',
2286 dest='bypass_watchlists',
2287 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002288 parser.add_option('-f', action='store_true', dest='force',
2289 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002290 parser.add_option('-m', dest='message', help='message for patchset')
2291 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002292 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002293 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002294 help='reviewer email addresses')
2295 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002296 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002297 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002298 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002299 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002300 parser.add_option('--emulate_svn_auto_props',
2301 '--emulate-svn-auto-props',
2302 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002303 dest="emulate_svn_auto_props",
2304 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002305 parser.add_option('-c', '--use-commit-queue', action='store_true',
2306 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002307 parser.add_option('--private', action='store_true',
2308 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002309 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002310 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002311 metavar='TARGET',
2312 help='Apply CL to remote ref TARGET. ' +
2313 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002314 parser.add_option('--squash', action='store_true',
2315 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002316 parser.add_option('--email', default=None,
2317 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002318 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2319 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002320 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2321 help='Send the patchset to do a CQ dry run right after '
2322 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002323 parser.add_option('--dependencies', action='store_true',
2324 help='Uploads CLs of all the local branches that depend on '
2325 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002326
rmistry@google.com2dd99862015-06-22 12:22:18 +00002327 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002328 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002329 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002330 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002331 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002332
sbc@chromium.org71437c02015-04-09 19:29:40 +00002333 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002334 return 1
2335
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002336 options.reviewers = cleanup_list(options.reviewers)
2337 options.cc = cleanup_list(options.cc)
2338
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002339 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002340 if args:
2341 # TODO(ukai): is it ok for gerrit case?
2342 base_branch = args[0]
2343 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002344 if cl.GetBranch() is None:
2345 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2346
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002347 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002348 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002349 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002350
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002351 # Make sure authenticated to Rietveld before running expensive hooks. It is
2352 # a fast, best efforts check. Rietveld still can reject the authentication
2353 # during the actual upload.
2354 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2355 authenticator = auth.get_authenticator_for_host(
2356 cl.GetRietveldServer(), auth_config)
2357 if not authenticator.has_cached_credentials():
2358 raise auth.LoginRequiredError(cl.GetRietveldServer())
2359
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002360 # Apply watchlists on upload.
2361 change = cl.GetChange(base_branch, None)
2362 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2363 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002364 if not options.bypass_watchlists:
2365 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002366
ukai@chromium.orge8077812012-02-03 03:41:46 +00002367 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002368 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002369 # Set the reviewer list now so that presubmit checks can access it.
2370 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002371 change_description.update_reviewers(options.reviewers,
2372 options.tbr_owners,
2373 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002374 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002375 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002376 may_prompt=not options.force,
2377 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002378 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002379 if not hook_results.should_continue():
2380 return 1
2381 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002382 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002383
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002384 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002385 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002386 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002387 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002388 print ('The last upload made from this repository was patchset #%d but '
2389 'the most recent patchset on the server is #%d.'
2390 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002391 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2392 'from another machine or branch the patch you\'re uploading now '
2393 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002394 ask_for_data('About to upload; enter to confirm.')
2395
iannucci@chromium.org79540052012-10-19 23:15:26 +00002396 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002397 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002398 return GerritUpload(options, args, cl, change)
2399 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002400 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002401 git_set_branch_value('last-upload-hash',
2402 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002403 # Run post upload hooks, if specified.
2404 if settings.GetRunPostUploadHook():
2405 presubmit_support.DoPostUploadExecuter(
2406 change,
2407 cl,
2408 settings.GetRoot(),
2409 options.verbose,
2410 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002411
rmistry@google.com2dd99862015-06-22 12:22:18 +00002412 # Upload all dependencies if specified.
2413 if options.dependencies:
2414 print
2415 print '--dependencies has been specified.'
2416 print 'All dependent local branches will be re-uploaded.'
2417 print
2418 # Remove the dependencies flag from args so that we do not end up in a
2419 # loop.
2420 orig_args.remove('--dependencies')
2421 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002422 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002423
2424
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002425def IsSubmoduleMergeCommit(ref):
2426 # When submodules are added to the repo, we expect there to be a single
2427 # non-git-svn merge commit at remote HEAD with a signature comment.
2428 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002429 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002430 return RunGit(cmd) != ''
2431
2432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002433def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002434 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002436 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437 Updates changelog with metadata (e.g. pointer to review).
2438 Pushes/dcommits the code upstream.
2439 Updates review and closes.
2440 """
2441 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2442 help='bypass upload presubmit hook')
2443 parser.add_option('-m', dest='message',
2444 help="override review description")
2445 parser.add_option('-f', action='store_true', dest='force',
2446 help="force yes to questions (don't prompt)")
2447 parser.add_option('-c', dest='contributor',
2448 help="external contributor for patch (appended to " +
2449 "description and used as author for git). Should be " +
2450 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002451 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002452 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002453 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002454 auth_config = auth.extract_auth_config_from_options(options)
2455
2456 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002457
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002458 current = cl.GetBranch()
2459 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2460 if not settings.GetIsGitSvn() and remote == '.':
2461 print
2462 print 'Attempting to push branch %r into another local branch!' % current
2463 print
2464 print 'Either reparent this branch on top of origin/master:'
2465 print ' git reparent-branch --root'
2466 print
2467 print 'OR run `git rebase-update` if you think the parent branch is already'
2468 print 'committed.'
2469 print
2470 print ' Current parent: %r' % upstream_branch
2471 return 1
2472
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002473 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 # Default to merging against our best guess of the upstream branch.
2475 args = [cl.GetUpstreamBranch()]
2476
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002477 if options.contributor:
2478 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2479 print "Please provide contibutor as 'First Last <email@example.com>'"
2480 return 1
2481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002482 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002483 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002484
sbc@chromium.org71437c02015-04-09 19:29:40 +00002485 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002486 return 1
2487
2488 # This rev-list syntax means "show all commits not in my branch that
2489 # are in base_branch".
2490 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2491 base_branch]).splitlines()
2492 if upstream_commits:
2493 print ('Base branch "%s" has %d commits '
2494 'not in this branch.' % (base_branch, len(upstream_commits)))
2495 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2496 return 1
2497
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002498 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002499 svn_head = None
2500 if cmd == 'dcommit' or base_has_submodules:
2501 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2502 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002503
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002504 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002505 # If the base_head is a submodule merge commit, the first parent of the
2506 # base_head should be a git-svn commit, which is what we're interested in.
2507 base_svn_head = base_branch
2508 if base_has_submodules:
2509 base_svn_head += '^1'
2510
2511 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512 if extra_commits:
2513 print ('This branch has %d additional commits not upstreamed yet.'
2514 % len(extra_commits.splitlines()))
2515 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2516 'before attempting to %s.' % (base_branch, cmd))
2517 return 1
2518
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002519 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002520 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002521 author = None
2522 if options.contributor:
2523 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002524 hook_results = cl.RunHook(
2525 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002526 may_prompt=not options.force,
2527 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002528 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002529 if not hook_results.should_continue():
2530 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002531
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002532 # Check the tree status if the tree status URL is set.
2533 status = GetTreeStatus()
2534 if 'closed' == status:
2535 print('The tree is closed. Please wait for it to reopen. Use '
2536 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2537 return 1
2538 elif 'unknown' == status:
2539 print('Unable to determine tree status. Please verify manually and '
2540 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2541 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002542 else:
2543 breakpad.SendStack(
2544 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002545 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2546 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002547 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002548
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002549 change_desc = ChangeDescription(options.message)
2550 if not change_desc.description and cl.GetIssue():
2551 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002553 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002554 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002555 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002556 else:
2557 print 'No description set.'
2558 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2559 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002560
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002561 # Keep a separate copy for the commit message, because the commit message
2562 # contains the link to the Rietveld issue, while the Rietveld message contains
2563 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002564 # Keep a separate copy for the commit message.
2565 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002566 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002567
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002568 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002569 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002570 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002571 # after it. Add a period on a new line to circumvent this. Also add a space
2572 # before the period to make sure that Gitiles continues to correctly resolve
2573 # the URL.
2574 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002576 commit_desc.append_footer('Patch from %s.' % options.contributor)
2577
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002578 print('Description:')
2579 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002580
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002581 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002582 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002583 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002584
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002585 # We want to squash all this branch's commits into one commit with the proper
2586 # description. We do this by doing a "reset --soft" to the base branch (which
2587 # keeps the working copy the same), then dcommitting that. If origin/master
2588 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2589 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002591 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2592 # Delete the branches if they exist.
2593 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2594 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2595 result = RunGitWithCode(showref_cmd)
2596 if result[0] == 0:
2597 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002598
2599 # We might be in a directory that's present in this branch but not in the
2600 # trunk. Move up to the top of the tree so that git commands that expect a
2601 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002602 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002603 if rel_base_path:
2604 os.chdir(rel_base_path)
2605
2606 # Stuff our change into the merge branch.
2607 # We wrap in a try...finally block so if anything goes wrong,
2608 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002609 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002610 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002611 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002612 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002613 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002614 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002615 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002616 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002617 RunGit(
2618 [
2619 'commit', '--author', options.contributor,
2620 '-m', commit_desc.description,
2621 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002623 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002624 if base_has_submodules:
2625 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2626 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2627 RunGit(['checkout', CHERRY_PICK_BRANCH])
2628 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002629 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002630 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002631 pending_prefix = settings.GetPendingRefPrefix()
2632 if not pending_prefix or branch.startswith(pending_prefix):
2633 # If not using refs/pending/heads/* at all, or target ref is already set
2634 # to pending, then push to the target ref directly.
2635 retcode, output = RunGitWithCode(
2636 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002637 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002638 else:
2639 # Cherry-pick the change on top of pending ref and then push it.
2640 assert branch.startswith('refs/'), branch
2641 assert pending_prefix[-1] == '/', pending_prefix
2642 pending_ref = pending_prefix + branch[len('refs/'):]
2643 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002644 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002645 if retcode == 0:
2646 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002647 else:
2648 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002649 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002650 'svn', 'dcommit',
2651 '-C%s' % options.similarity,
2652 '--no-rebase', '--rmdir',
2653 ]
2654 if settings.GetForceHttpsCommitUrl():
2655 # Allow forcing https commit URLs for some projects that don't allow
2656 # committing to http URLs (like Google Code).
2657 remote_url = cl.GetGitSvnRemoteUrl()
2658 if urlparse.urlparse(remote_url).scheme == 'http':
2659 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002660 cmd_args.append('--commit-url=%s' % remote_url)
2661 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002662 if 'Committed r' in output:
2663 revision = re.match(
2664 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2665 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002666 finally:
2667 # And then swap back to the original branch and clean up.
2668 RunGit(['checkout', '-q', cl.GetBranch()])
2669 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002670 if base_has_submodules:
2671 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002672
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002673 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002674 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002675 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002676
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002677 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002678 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002679 try:
2680 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2681 # We set pushed_to_pending to False, since it made it all the way to the
2682 # real ref.
2683 pushed_to_pending = False
2684 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002685 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002686
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002688 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002690 if not to_pending:
2691 if viewvc_url and revision:
2692 change_desc.append_footer(
2693 'Committed: %s%s' % (viewvc_url, revision))
2694 elif revision:
2695 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696 print ('Closing issue '
2697 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002698 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002699 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002700 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002701 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002702 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002703 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002704 if options.bypass_hooks:
2705 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2706 else:
2707 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002708 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002709 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002710
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002711 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002712 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2713 print 'The commit is in the pending queue (%s).' % pending_ref
2714 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002715 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002716 'footer.' % branch)
2717
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002718 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2719 if os.path.isfile(hook):
2720 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002721
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002722 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723
2724
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002725def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2726 print
2727 print 'Waiting for commit to be landed on %s...' % real_ref
2728 print '(If you are impatient, you may Ctrl-C once without harm)'
2729 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2730 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2731
2732 loop = 0
2733 while True:
2734 sys.stdout.write('fetching (%d)... \r' % loop)
2735 sys.stdout.flush()
2736 loop += 1
2737
2738 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2739 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2740 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2741 for commit in commits.splitlines():
2742 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2743 print 'Found commit on %s' % real_ref
2744 return commit
2745
2746 current_rev = to_rev
2747
2748
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002749def PushToGitPending(remote, pending_ref, upstream_ref):
2750 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2751
2752 Returns:
2753 (retcode of last operation, output log of last operation).
2754 """
2755 assert pending_ref.startswith('refs/'), pending_ref
2756 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2757 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2758 code = 0
2759 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002760 max_attempts = 3
2761 attempts_left = max_attempts
2762 while attempts_left:
2763 if attempts_left != max_attempts:
2764 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2765 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002766
2767 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002768 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002769 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002770 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002771 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002772 print 'Fetch failed with exit code %d.' % code
2773 if out.strip():
2774 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002775 continue
2776
2777 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002778 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002779 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002780 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002781 if code:
2782 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002783 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2784 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002785 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2786 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002787 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002788 return code, out
2789
2790 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002791 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002792 code, out = RunGitWithCode(
2793 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2794 if code == 0:
2795 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002796 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002797 return code, out
2798
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002799 print 'Push failed with exit code %d.' % code
2800 if out.strip():
2801 print out.strip()
2802 if IsFatalPushFailure(out):
2803 print (
2804 'Fatal push error. Make sure your .netrc credentials and git '
2805 'user.email are correct and you have push access to the repo.')
2806 return code, out
2807
2808 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002809 return code, out
2810
2811
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002812def IsFatalPushFailure(push_stdout):
2813 """True if retrying push won't help."""
2814 return '(prohibited by Gerrit)' in push_stdout
2815
2816
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002817@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002818def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002819 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002821 if get_footer_svn_id():
2822 # If it looks like previous commits were mirrored with git-svn.
2823 message = """This repository appears to be a git-svn mirror, but no
2824upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2825 else:
2826 message = """This doesn't appear to be an SVN repository.
2827If your project has a true, writeable git repository, you probably want to run
2828'git cl land' instead.
2829If your project has a git mirror of an upstream SVN master, you probably need
2830to run 'git svn init'.
2831
2832Using the wrong command might cause your commit to appear to succeed, and the
2833review to be closed, without actually landing upstream. If you choose to
2834proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002835 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002836 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002837 return SendUpstream(parser, args, 'dcommit')
2838
2839
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002840@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002841def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002842 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002843 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002844 print('This appears to be an SVN repository.')
2845 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002846 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002847 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002848 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849
2850
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002851@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002853 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002854 parser.add_option('-b', dest='newbranch',
2855 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002856 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002858 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2859 help='Change to the directory DIR immediately, '
2860 'before doing anything else.')
2861 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002862 help='failed patches spew .rej files rather than '
2863 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2865 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002866 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002868 auth_config = auth.extract_auth_config_from_options(options)
2869
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870 if len(args) != 1:
2871 parser.print_help()
2872 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002873 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002875 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002876 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002877 return 1
2878
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002879 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002880 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002881
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002882 if options.newbranch:
2883 if options.force:
2884 RunGit(['branch', '-D', options.newbranch],
2885 stderr=subprocess2.PIPE, error_ok=True)
2886 RunGit(['checkout', '-b', options.newbranch,
2887 Changelist().GetUpstreamBranch()])
2888
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002889 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002890 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002891
2892
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002893def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002894 # PatchIssue should never be called with a dirty tree. It is up to the
2895 # caller to check this, but just in case we assert here since the
2896 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002897 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002898
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002899 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002900 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002901 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002902 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002903 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002904 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002905 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002906 # Assume it's a URL to the patch. Default to https.
2907 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002908 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002909 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002910 DieWithError('Must pass an issue ID or full URL for '
2911 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002912 issue = int(match.group(2))
2913 cl = Changelist(issue=issue, auth_config=auth_config)
2914 cl.rietveld_server = match.group(1)
2915 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002916 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002917
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002918 # Switch up to the top-level directory, if necessary, in preparation for
2919 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002920 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 if top:
2922 os.chdir(top)
2923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 # Git patches have a/ at the beginning of source paths. We strip that out
2925 # with a sed script rather than the -p flag to patch so we can feed either
2926 # Git or svn-style patches into the same apply command.
2927 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002928 try:
2929 patch_data = subprocess2.check_output(
2930 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2931 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002932 DieWithError('Git patch mungling failed.')
2933 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002934
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935 # We use "git apply" to apply the patch instead of "patch" so that we can
2936 # pick up file adds.
2937 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002938 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002939 if directory:
2940 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002941 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002943 elif IsGitVersionAtLeast('1.7.12'):
2944 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002945 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002946 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002947 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002948 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002949 print 'Failed to apply the patch'
2950 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002951
2952 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002953 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002954 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2955 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002956 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2957 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002958 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002960 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002961 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002962 else:
2963 print "Patch applied to index."
2964 return 0
2965
2966
2967def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002968 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969 # Provide a wrapper for git svn rebase to help avoid accidental
2970 # git svn dcommit.
2971 # It's the only command that doesn't use parser at all since we just defer
2972 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002973
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002974 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002975
2976
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002977def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002978 """Fetches the tree status and returns either 'open', 'closed',
2979 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002980 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981 if url:
2982 status = urllib2.urlopen(url).read().lower()
2983 if status.find('closed') != -1 or status == '0':
2984 return 'closed'
2985 elif status.find('open') != -1 or status == '1':
2986 return 'open'
2987 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988 return 'unset'
2989
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991def GetTreeStatusReason():
2992 """Fetches the tree status from a json url and returns the message
2993 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002994 url = settings.GetTreeStatusUrl()
2995 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002996 connection = urllib2.urlopen(json_url)
2997 status = json.loads(connection.read())
2998 connection.close()
2999 return status['message']
3000
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003001
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003002def GetBuilderMaster(bot_list):
3003 """For a given builder, fetch the master from AE if available."""
3004 map_url = 'https://builders-map.appspot.com/'
3005 try:
3006 master_map = json.load(urllib2.urlopen(map_url))
3007 except urllib2.URLError as e:
3008 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3009 (map_url, e))
3010 except ValueError as e:
3011 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3012 if not master_map:
3013 return None, 'Failed to build master map.'
3014
3015 result_master = ''
3016 for bot in bot_list:
3017 builder = bot.split(':', 1)[0]
3018 master_list = master_map.get(builder, [])
3019 if not master_list:
3020 return None, ('No matching master for builder %s.' % builder)
3021 elif len(master_list) > 1:
3022 return None, ('The builder name %s exists in multiple masters %s.' %
3023 (builder, master_list))
3024 else:
3025 cur_master = master_list[0]
3026 if not result_master:
3027 result_master = cur_master
3028 elif result_master != cur_master:
3029 return None, 'The builders do not belong to the same master.'
3030 return result_master, None
3031
3032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003034 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003035 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003036 status = GetTreeStatus()
3037 if 'unset' == status:
3038 print 'You must configure your tree status URL by running "git cl config".'
3039 return 2
3040
3041 print "The tree is %s" % status
3042 print
3043 print GetTreeStatusReason()
3044 if status != 'open':
3045 return 1
3046 return 0
3047
3048
maruel@chromium.org15192402012-09-06 12:38:29 +00003049def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003050 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003051 group = optparse.OptionGroup(parser, "Try job options")
3052 group.add_option(
3053 "-b", "--bot", action="append",
3054 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3055 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003056 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003057 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003058 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003059 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003060 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003061 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003062 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003063 "-r", "--revision",
3064 help="Revision to use for the try job; default: the "
3065 "revision will be determined by the try server; see "
3066 "its waterfall for more info")
3067 group.add_option(
3068 "-c", "--clobber", action="store_true", default=False,
3069 help="Force a clobber before building; e.g. don't do an "
3070 "incremental build")
3071 group.add_option(
3072 "--project",
3073 help="Override which project to use. Projects are defined "
3074 "server-side to define what default bot set to use")
3075 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003076 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003077 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003078 "--use-rietveld", action="store_true", default=False,
3079 help="Use Rietveld to trigger try jobs.")
3080 group.add_option(
3081 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3082 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003083 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003084 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003085 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003086 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003087
3088 if args:
3089 parser.error('Unknown arguments: %s' % args)
3090
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003091 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003092 if not cl.GetIssue():
3093 parser.error('Need to upload first')
3094
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003095 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003096 if props.get('closed'):
3097 parser.error('Cannot send tryjobs for a closed CL')
3098
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003099 if props.get('private'):
3100 parser.error('Cannot use trybots with private issue')
3101
maruel@chromium.org15192402012-09-06 12:38:29 +00003102 if not options.name:
3103 options.name = cl.GetBranch()
3104
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003105 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003106 options.master, err_msg = GetBuilderMaster(options.bot)
3107 if err_msg:
3108 parser.error('Tryserver master cannot be found because: %s\n'
3109 'Please manually specify the tryserver master'
3110 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003111
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003112 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003113 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003114 if not options.bot:
3115 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003116
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003117 # Get try masters from PRESUBMIT.py files.
3118 masters = presubmit_support.DoGetTryMasters(
3119 change,
3120 change.LocalPaths(),
3121 settings.GetRoot(),
3122 None,
3123 None,
3124 options.verbose,
3125 sys.stdout)
3126 if masters:
3127 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003128
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003129 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3130 options.bot = presubmit_support.DoGetTrySlaves(
3131 change,
3132 change.LocalPaths(),
3133 settings.GetRoot(),
3134 None,
3135 None,
3136 options.verbose,
3137 sys.stdout)
3138 if not options.bot:
3139 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003140
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003141 builders_and_tests = {}
3142 # TODO(machenbach): The old style command-line options don't support
3143 # multiple try masters yet.
3144 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3145 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3146
3147 for bot in old_style:
3148 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003149 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003150 elif ',' in bot:
3151 parser.error('Specify one bot per --bot flag')
3152 else:
3153 builders_and_tests.setdefault(bot, []).append('defaulttests')
3154
3155 for bot, tests in new_style:
3156 builders_and_tests.setdefault(bot, []).extend(tests)
3157
3158 # Return a master map with one master to be backwards compatible. The
3159 # master name defaults to an empty string, which will cause the master
3160 # not to be set on rietveld (deprecated).
3161 return {options.master: builders_and_tests}
3162
3163 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003164
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003165 for builders in masters.itervalues():
3166 if any('triggered' in b for b in builders):
3167 print >> sys.stderr, (
3168 'ERROR You are trying to send a job to a triggered bot. This type of'
3169 ' bot requires an\ninitial job from a parent (usually a builder). '
3170 'Instead send your job to the parent.\n'
3171 'Bot list: %s' % builders)
3172 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003173
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003174 patchset = cl.GetMostRecentPatchset()
3175 if patchset and patchset != cl.GetPatchset():
3176 print(
3177 '\nWARNING Mismatch between local config and server. Did a previous '
3178 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3179 'Continuing using\npatchset %s.\n' % patchset)
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003180 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003181 try:
3182 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3183 except BuildbucketResponseException as ex:
3184 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003185 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003186 except Exception as e:
3187 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3188 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3189 e, stacktrace)
3190 return 1
3191 else:
3192 try:
3193 cl.RpcServer().trigger_distributed_try_jobs(
3194 cl.GetIssue(), patchset, options.name, options.clobber,
3195 options.revision, masters)
3196 except urllib2.HTTPError as e:
3197 if e.code == 404:
3198 print('404 from rietveld; '
3199 'did you mean to use "git try" instead of "git cl try"?')
3200 return 1
3201 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003202
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003203 for (master, builders) in sorted(masters.iteritems()):
3204 if master:
3205 print 'Master: %s' % master
3206 length = max(len(builder) for builder in builders)
3207 for builder in sorted(builders):
3208 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003209 return 0
3210
3211
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003212@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003214 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003215 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003216 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003217 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003220 if args:
3221 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003222 branch = cl.GetBranch()
3223 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003224 cl = Changelist()
3225 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003226
3227 # Clear configured merge-base, if there is one.
3228 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003229 else:
3230 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 return 0
3232
3233
thestig@chromium.org00858c82013-12-02 23:08:03 +00003234def CMDweb(parser, args):
3235 """Opens the current CL in the web browser."""
3236 _, args = parser.parse_args(args)
3237 if args:
3238 parser.error('Unrecognized args: %s' % ' '.join(args))
3239
3240 issue_url = Changelist().GetIssueURL()
3241 if not issue_url:
3242 print >> sys.stderr, 'ERROR No issue to open'
3243 return 1
3244
3245 webbrowser.open(issue_url)
3246 return 0
3247
3248
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003249def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003250 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003251 auth.add_auth_options(parser)
3252 options, args = parser.parse_args(args)
3253 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003254 if args:
3255 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003256 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003257 props = cl.GetIssueProperties()
3258 if props.get('private'):
3259 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003260 cl.SetFlag('commit', '1')
3261 return 0
3262
3263
groby@chromium.org411034a2013-02-26 15:12:01 +00003264def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003265 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003266 auth.add_auth_options(parser)
3267 options, args = parser.parse_args(args)
3268 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003269 if args:
3270 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003271 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003272 # Ensure there actually is an issue to close.
3273 cl.GetDescription()
3274 cl.CloseIssue()
3275 return 0
3276
3277
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003278def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003279 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003280 auth.add_auth_options(parser)
3281 options, args = parser.parse_args(args)
3282 auth_config = auth.extract_auth_config_from_options(options)
3283 if args:
3284 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003285
3286 # Uncommitted (staged and unstaged) changes will be destroyed by
3287 # "git reset --hard" if there are merging conflicts in PatchIssue().
3288 # Staged changes would be committed along with the patch from last
3289 # upload, hence counted toward the "last upload" side in the final
3290 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003291 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003292 return 1
3293
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003294 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003295 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003296 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003297 if not issue:
3298 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003299 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003300 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003301
3302 # Create a new branch based on the merge-base
3303 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3304 try:
3305 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003306 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003307 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003308 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003309 return rtn
3310
wychen@chromium.org06928532015-02-03 02:11:29 +00003311 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003312 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003313 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003314 finally:
3315 RunGit(['checkout', '-q', branch])
3316 RunGit(['branch', '-D', TMP_BRANCH])
3317
3318 return 0
3319
3320
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003321def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003322 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003323 parser.add_option(
3324 '--no-color',
3325 action='store_true',
3326 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003327 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003328 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003329 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003330
3331 author = RunGit(['config', 'user.email']).strip() or None
3332
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003333 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003334
3335 if args:
3336 if len(args) > 1:
3337 parser.error('Unknown args')
3338 base_branch = args[0]
3339 else:
3340 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003341 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003342
3343 change = cl.GetChange(base_branch, None)
3344 return owners_finder.OwnersFinder(
3345 [f.LocalPath() for f in
3346 cl.GetChange(base_branch, None).AffectedFiles()],
3347 change.RepositoryRoot(), author,
3348 fopen=file, os_path=os.path, glob=glob.glob,
3349 disable_color=options.no_color).run()
3350
3351
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003352def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3353 """Generates a diff command."""
3354 # Generate diff for the current branch's changes.
3355 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3356 upstream_commit, '--' ]
3357
3358 if args:
3359 for arg in args:
3360 if os.path.isdir(arg):
3361 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3362 elif os.path.isfile(arg):
3363 diff_cmd.append(arg)
3364 else:
3365 DieWithError('Argument "%s" is not a file or a directory' % arg)
3366 else:
3367 diff_cmd.extend('*' + ext for ext in extensions)
3368
3369 return diff_cmd
3370
3371
enne@chromium.org555cfe42014-01-29 18:21:39 +00003372@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003373def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003374 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003375 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003376 parser.add_option('--full', action='store_true',
3377 help='Reformat the full content of all touched files')
3378 parser.add_option('--dry-run', action='store_true',
3379 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003380 parser.add_option('--python', action='store_true',
3381 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003382 parser.add_option('--diff', action='store_true',
3383 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003384 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003385
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003386 # git diff generates paths against the root of the repository. Change
3387 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003388 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003389 if rel_base_path:
3390 os.chdir(rel_base_path)
3391
digit@chromium.org29e47272013-05-17 17:01:46 +00003392 # Grab the merge-base commit, i.e. the upstream commit of the current
3393 # branch when it was created or the last time it was rebased. This is
3394 # to cover the case where the user may have called "git fetch origin",
3395 # moving the origin branch to a newer commit, but hasn't rebased yet.
3396 upstream_commit = None
3397 cl = Changelist()
3398 upstream_branch = cl.GetUpstreamBranch()
3399 if upstream_branch:
3400 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3401 upstream_commit = upstream_commit.strip()
3402
3403 if not upstream_commit:
3404 DieWithError('Could not find base commit for this branch. '
3405 'Are you in detached state?')
3406
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003407 if opts.full:
3408 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003409 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003410 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003411 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003412 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003413
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003414 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003415 diff_output = RunGit(diff_cmd)
3416
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003417 top_dir = os.path.normpath(
3418 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3419
3420 # Locate the clang-format binary in the checkout
3421 try:
3422 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3423 except clang_format.NotFoundError, e:
3424 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003425
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003426 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3427 # formatted. This is used to block during the presubmit.
3428 return_value = 0
3429
digit@chromium.org29e47272013-05-17 17:01:46 +00003430 if opts.full:
3431 # diff_output is a list of files to send to clang-format.
3432 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003433 if files:
3434 cmd = [clang_format_tool]
3435 if not opts.dry_run and not opts.diff:
3436 cmd.append('-i')
3437 stdout = RunCommand(cmd + files, cwd=top_dir)
3438 if opts.diff:
3439 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003440 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003441 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003442 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003443 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003444 try:
3445 script = clang_format.FindClangFormatScriptInChromiumTree(
3446 'clang-format-diff.py')
3447 except clang_format.NotFoundError, e:
3448 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003449
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003450 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003451 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003452 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003453
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003454 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003455 if opts.diff:
3456 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003457 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003458 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003459
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003460 # Similar code to above, but using yapf on .py files rather than clang-format
3461 # on C/C++ files
3462 if opts.python:
3463 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3464 diff_output = RunGit(diff_cmd)
3465 yapf_tool = gclient_utils.FindExecutable('yapf')
3466 if yapf_tool is None:
3467 DieWithError('yapf not found in PATH')
3468
3469 if opts.full:
3470 files = diff_output.splitlines()
3471 if files:
3472 cmd = [yapf_tool]
3473 if not opts.dry_run and not opts.diff:
3474 cmd.append('-i')
3475 stdout = RunCommand(cmd + files, cwd=top_dir)
3476 if opts.diff:
3477 sys.stdout.write(stdout)
3478 else:
3479 # TODO(sbc): yapf --lines mode still has some issues.
3480 # https://github.com/google/yapf/issues/154
3481 DieWithError('--python currently only works with --full')
3482
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003483 # Build a diff command that only operates on dart files. dart's formatter
3484 # does not have the nice property of only operating on modified chunks, so
3485 # hard code full.
3486 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3487 args, ['.dart'])
3488 dart_diff_output = RunGit(dart_diff_cmd)
3489 if dart_diff_output:
3490 try:
3491 command = [dart_format.FindDartFmtToolInChromiumTree()]
3492 if not opts.dry_run and not opts.diff:
3493 command.append('-w')
3494 command.extend(dart_diff_output.splitlines())
3495
3496 stdout = RunCommand(command, cwd=top_dir, env=env)
3497 if opts.dry_run and stdout:
3498 return_value = 2
3499 except dart_format.NotFoundError as e:
3500 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3501 'this checkout.')
3502
3503 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003504
3505
maruel@chromium.org29404b52014-09-08 22:58:00 +00003506def CMDlol(parser, args):
3507 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003508 print zlib.decompress(base64.b64decode(
3509 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3510 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3511 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3512 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003513 return 0
3514
3515
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003516class OptionParser(optparse.OptionParser):
3517 """Creates the option parse and add --verbose support."""
3518 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003519 optparse.OptionParser.__init__(
3520 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003521 self.add_option(
3522 '-v', '--verbose', action='count', default=0,
3523 help='Use 2 times for more debugging info')
3524
3525 def parse_args(self, args=None, values=None):
3526 options, args = optparse.OptionParser.parse_args(self, args, values)
3527 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3528 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3529 return options, args
3530
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003531
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003533 if sys.hexversion < 0x02060000:
3534 print >> sys.stderr, (
3535 '\nYour python version %s is unsupported, please upgrade.\n' %
3536 sys.version.split(' ', 1)[0])
3537 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003538
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003539 # Reload settings.
3540 global settings
3541 settings = Settings()
3542
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003543 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003544 dispatcher = subcommand.CommandDispatcher(__name__)
3545 try:
3546 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003547 except auth.AuthenticationError as e:
3548 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003549 except urllib2.HTTPError, e:
3550 if e.code != 500:
3551 raise
3552 DieWithError(
3553 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3554 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003555 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003556
3557
3558if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003559 # These affect sys.stdout so do it outside of main() to simplify mocks in
3560 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003561 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003562 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003563 try:
3564 sys.exit(main(sys.argv[1:]))
3565 except KeyboardInterrupt:
3566 sys.stderr.write('interrupted\n')
3567 sys.exit(1)