blob: dc6f4cdd9a623190b1492dbeaf3723458927e5ac [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 try:
tandrii5d48c322016-08-18 16:19:37 -0700122 (out, _), code = subprocess2.communicate(['git'] + args,
123 env=GetNoGitPagerEnv(),
124 stdout=subprocess2.PIPE,
125 stderr=stderr)
126 return code, out
127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
129 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
tandrii5d48c322016-08-18 16:19:37 -0700159def _git_branch_config_key(branch, key):
160 """Helper method to return Git config key for a branch."""
161 assert branch, 'branch name is required to set git config for it'
162 return 'branch.%s.%s' % (branch, key)
163
164
165def _git_get_branch_config_value(key, default=None, value_type=str,
166 branch=False):
167 """Returns git config value of given or current branch if any.
168
169 Returns default in all other cases.
170 """
171 assert value_type in (int, str, bool)
172 if branch is False: # Distinguishing default arg value from None.
173 branch = GetCurrentBranch()
174
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000175 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700176 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000177
tandrii5d48c322016-08-18 16:19:37 -0700178 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700179 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700180 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700181 # git config also has --int, but apparently git config suffers from integer
182 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700183 args.append(_git_branch_config_key(branch, key))
184 code, out = RunGitWithCode(args)
185 if code == 0:
186 value = out.strip()
187 if value_type == int:
188 return int(value)
189 if value_type == bool:
190 return bool(value.lower() == 'true')
191 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 return default
193
194
tandrii5d48c322016-08-18 16:19:37 -0700195def _git_set_branch_config_value(key, value, branch=None, **kwargs):
196 """Sets the value or unsets if it's None of a git branch config.
197
198 Valid, though not necessarily existing, branch must be provided,
199 otherwise currently checked out branch is used.
200 """
201 if not branch:
202 branch = GetCurrentBranch()
203 assert branch, 'a branch name OR currently checked out branch is required'
204 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700205 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700206 if value is None:
207 args.append('--unset')
208 elif isinstance(value, bool):
209 args.append('--bool')
210 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700211 else:
tandrii33a46ff2016-08-23 05:53:40 -0700212 # git config also has --int, but apparently git config suffers from integer
213 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700214 value = str(value)
215 args.append(_git_branch_config_key(branch, key))
216 if value is not None:
217 args.append(value)
218 RunGit(args, **kwargs)
219
220
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000221def add_git_similarity(parser):
222 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700223 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000224 help='Sets the percentage that a pair of files need to match in order to'
225 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000226 parser.add_option(
227 '--find-copies', action='store_true',
228 help='Allows git to look for copies.')
229 parser.add_option(
230 '--no-find-copies', action='store_false', dest='find_copies',
231 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000232
233 old_parser_args = parser.parse_args
234 def Parse(args):
235 options, args = old_parser_args(args)
236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000237 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700238 options.similarity = _git_get_branch_config_value(
239 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000240 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241 print('Note: Saving similarity of %d%% in git config.'
242 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700243 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 options.similarity = max(0, min(options.similarity, 100))
246
247 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.find_copies = _git_get_branch_config_value(
249 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 else:
tandrii5d48c322016-08-18 16:19:37 -0700251 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000252
253 print('Using %d%% similarity for rename/copy detection. '
254 'Override with --similarity.' % options.similarity)
255
256 return options, args
257 parser.parse_args = Parse
258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
261 properties = dict(x.split('=', 1) for x in options.properties)
262 for key, val in properties.iteritems():
263 try:
264 properties[key] = json.loads(val)
265 except ValueError:
266 pass # If a value couldn't be evaluated, treat it as a string.
267 return properties
268
269
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270def _prefix_master(master):
271 """Convert user-specified master name to full master name.
272
273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
274 name, while the developers always use shortened master name
275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
276 function does the conversion for buildbucket migration.
277 """
278 prefix = 'master.'
279 if master.startswith(prefix):
280 return master
281 return '%s%s' % (prefix, master)
282
283
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000284def _buildbucket_retry(operation_name, http, *args, **kwargs):
285 """Retries requests to buildbucket service and returns parsed json content."""
286 try_count = 0
287 while True:
288 response, content = http.request(*args, **kwargs)
289 try:
290 content_json = json.loads(content)
291 except ValueError:
292 content_json = None
293
294 # Buildbucket could return an error even if status==200.
295 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000296 error = content_json.get('error')
297 if error.get('code') == 403:
298 raise BuildbucketResponseException(
299 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000300 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000301 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000302 raise BuildbucketResponseException(msg)
303
304 if response.status == 200:
305 if not content_json:
306 raise BuildbucketResponseException(
307 'Buildbucket returns invalid json content: %s.\n'
308 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
309 content)
310 return content_json
311 if response.status < 500 or try_count >= 2:
312 raise httplib2.HttpLib2Error(content)
313
314 # status >= 500 means transient failures.
315 logging.debug('Transient errors when %s. Will retry.', operation_name)
316 time.sleep(0.5 + 1.5*try_count)
317 try_count += 1
318 assert False, 'unreachable'
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322 rietveld_url = settings.GetDefaultServerUrl()
323 rietveld_host = urlparse.urlparse(rietveld_url).hostname
324 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
325 http = authenticator.authorize(httplib2.Http())
326 http.force_exception_to_status_code = True
327 issue_props = changelist.GetIssueProperties()
328 issue = changelist.GetIssue()
329 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000330 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332 buildbucket_put_url = (
333 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000334 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
336 hostname=rietveld_host,
337 issue=issue,
338 patch=patchset)
339
340 batch_req_body = {'builds': []}
341 print_text = []
342 print_text.append('Tried jobs on:')
343 for master, builders_and_tests in sorted(masters.iteritems()):
344 print_text.append('Master: %s' % master)
345 bucket = _prefix_master(master)
346 for builder, tests in sorted(builders_and_tests.iteritems()):
347 print_text.append(' %s: %s' % (builder, tests))
348 parameters = {
349 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000350 'changes': [{
351 'author': {'email': issue_props['owner_email']},
352 'revision': options.revision,
353 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'properties': {
355 'category': category,
356 'issue': issue,
357 'master': master,
358 'patch_project': issue_props['project'],
359 'patch_storage': 'rietveld',
360 'patchset': patchset,
361 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 },
364 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000365 if 'presubmit' in builder.lower():
366 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000367 if tests:
368 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000369 if properties:
370 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 if options.clobber:
372 parameters['properties']['clobber'] = True
373 batch_req_body['builds'].append(
374 {
375 'bucket': bucket,
376 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000378 'tags': ['builder:%s' % builder,
379 'buildset:%s' % buildset,
380 'master:%s' % master,
381 'user_agent:git_cl_try']
382 }
383 )
384
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700386 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 http,
388 buildbucket_put_url,
389 'PUT',
390 body=json.dumps(batch_req_body),
391 headers={'Content-Type': 'application/json'}
392 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000393 print_text.append('To see results here, run: git cl try-results')
394 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700395 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000398def fetch_try_jobs(auth_config, changelist, options):
qyearsleyeab3c042016-08-24 09:18:28 -0700399 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400
qyearsley53f48a12016-09-01 10:45:13 -0700401 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000402 """
403 rietveld_url = settings.GetDefaultServerUrl()
404 rietveld_host = urlparse.urlparse(rietveld_url).hostname
405 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
406 if authenticator.has_cached_credentials():
407 http = authenticator.authorize(httplib2.Http())
408 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700409 print('Warning: Some results might be missing because %s' %
410 # Get the message on how to login.
411 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000412 http = httplib2.Http()
413
414 http.force_exception_to_status_code = True
415
416 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
417 hostname=rietveld_host,
418 issue=changelist.GetIssue(),
419 patch=options.patchset)
420 params = {'tag': 'buildset:%s' % buildset}
421
422 builds = {}
423 while True:
424 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
425 hostname=options.buildbucket_host,
426 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700427 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000428 for build in content.get('builds', []):
429 builds[build['id']] = build
430 if 'next_cursor' in content:
431 params['start_cursor'] = content['next_cursor']
432 else:
433 break
434 return builds
435
436
qyearsleyeab3c042016-08-24 09:18:28 -0700437def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000438 """Prints nicely result of fetch_try_jobs."""
439 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700440 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000441 return
442
443 # Make a copy, because we'll be modifying builds dictionary.
444 builds = builds.copy()
445 builder_names_cache = {}
446
447 def get_builder(b):
448 try:
449 return builder_names_cache[b['id']]
450 except KeyError:
451 try:
452 parameters = json.loads(b['parameters_json'])
453 name = parameters['builder_name']
454 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700455 print('WARNING: failed to get builder name for build %s: %s' % (
456 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 name = None
458 builder_names_cache[b['id']] = name
459 return name
460
461 def get_bucket(b):
462 bucket = b['bucket']
463 if bucket.startswith('master.'):
464 return bucket[len('master.'):]
465 return bucket
466
467 if options.print_master:
468 name_fmt = '%%-%ds %%-%ds' % (
469 max(len(str(get_bucket(b))) for b in builds.itervalues()),
470 max(len(str(get_builder(b))) for b in builds.itervalues()))
471 def get_name(b):
472 return name_fmt % (get_bucket(b), get_builder(b))
473 else:
474 name_fmt = '%%-%ds' % (
475 max(len(str(get_builder(b))) for b in builds.itervalues()))
476 def get_name(b):
477 return name_fmt % get_builder(b)
478
479 def sort_key(b):
480 return b['status'], b.get('result'), get_name(b), b.get('url')
481
482 def pop(title, f, color=None, **kwargs):
483 """Pop matching builds from `builds` dict and print them."""
484
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000485 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486 colorize = str
487 else:
488 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
489
490 result = []
491 for b in builds.values():
492 if all(b.get(k) == v for k, v in kwargs.iteritems()):
493 builds.pop(b['id'])
494 result.append(b)
495 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700496 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000497 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700498 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000499
500 total = len(builds)
501 pop(status='COMPLETED', result='SUCCESS',
502 title='Successes:', color=Fore.GREEN,
503 f=lambda b: (get_name(b), b.get('url')))
504 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
505 title='Infra Failures:', color=Fore.MAGENTA,
506 f=lambda b: (get_name(b), b.get('url')))
507 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
508 title='Failures:', color=Fore.RED,
509 f=lambda b: (get_name(b), b.get('url')))
510 pop(status='COMPLETED', result='CANCELED',
511 title='Canceled:', color=Fore.MAGENTA,
512 f=lambda b: (get_name(b),))
513 pop(status='COMPLETED', result='FAILURE',
514 failure_reason='INVALID_BUILD_DEFINITION',
515 title='Wrong master/builder name:', color=Fore.MAGENTA,
516 f=lambda b: (get_name(b),))
517 pop(status='COMPLETED', result='FAILURE',
518 title='Other failures:',
519 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
520 pop(status='COMPLETED',
521 title='Other finished:',
522 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
523 pop(status='STARTED',
524 title='Started:', color=Fore.YELLOW,
525 f=lambda b: (get_name(b), b.get('url')))
526 pop(status='SCHEDULED',
527 title='Scheduled:',
528 f=lambda b: (get_name(b), 'id=%s' % b['id']))
529 # The last section is just in case buildbucket API changes OR there is a bug.
530 pop(title='Other:',
531 f=lambda b: (get_name(b), 'id=%s' % b['id']))
532 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700533 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
535
qyearsley53f48a12016-09-01 10:45:13 -0700536def write_try_results_json(output_file, builds):
537 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
538
539 The input |builds| dict is assumed to be generated by Buildbucket.
540 Buildbucket documentation: http://goo.gl/G0s101
541 """
542
543 def convert_build_dict(build):
544 return {
545 'buildbucket_id': build.get('id'),
546 'status': build.get('status'),
547 'result': build.get('result'),
548 'bucket': build.get('bucket'),
549 'builder_name': json.loads(
550 build.get('parameters_json', '{}')).get('builder_name'),
551 'failure_reason': build.get('failure_reason'),
552 'url': build.get('url'),
553 }
554
555 converted = []
556 for _, build in sorted(builds.items()):
557 converted.append(convert_build_dict(build))
558 write_json(output_file, converted)
559
560
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000561def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
562 """Return the corresponding git ref if |base_url| together with |glob_spec|
563 matches the full |url|.
564
565 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
566 """
567 fetch_suburl, as_ref = glob_spec.split(':')
568 if allow_wildcards:
569 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
570 if glob_match:
571 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
572 # "branches/{472,597,648}/src:refs/remotes/svn/*".
573 branch_re = re.escape(base_url)
574 if glob_match.group(1):
575 branch_re += '/' + re.escape(glob_match.group(1))
576 wildcard = glob_match.group(2)
577 if wildcard == '*':
578 branch_re += '([^/]*)'
579 else:
580 # Escape and replace surrounding braces with parentheses and commas
581 # with pipe symbols.
582 wildcard = re.escape(wildcard)
583 wildcard = re.sub('^\\\\{', '(', wildcard)
584 wildcard = re.sub('\\\\,', '|', wildcard)
585 wildcard = re.sub('\\\\}$', ')', wildcard)
586 branch_re += wildcard
587 if glob_match.group(3):
588 branch_re += re.escape(glob_match.group(3))
589 match = re.match(branch_re, url)
590 if match:
591 return re.sub('\*$', match.group(1), as_ref)
592
593 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
594 if fetch_suburl:
595 full_url = base_url + '/' + fetch_suburl
596 else:
597 full_url = base_url
598 if full_url == url:
599 return as_ref
600 return None
601
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000602
iannucci@chromium.org79540052012-10-19 23:15:26 +0000603def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000604 """Prints statistics about the change to the user."""
605 # --no-ext-diff is broken in some versions of Git, so try to work around
606 # this by overriding the environment (but there is still a problem if the
607 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000608 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000609 if 'GIT_EXTERNAL_DIFF' in env:
610 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000611
612 if find_copies:
613 similarity_options = ['--find-copies-harder', '-l100000',
614 '-C%s' % similarity]
615 else:
616 similarity_options = ['-M%s' % similarity]
617
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000618 try:
619 stdout = sys.stdout.fileno()
620 except AttributeError:
621 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000622 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000623 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000624 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000625 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000626
627
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000628class BuildbucketResponseException(Exception):
629 pass
630
631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632class Settings(object):
633 def __init__(self):
634 self.default_server = None
635 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000636 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 self.is_git_svn = None
638 self.svn_branch = None
639 self.tree_status_url = None
640 self.viewvc_url = None
641 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000642 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000643 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000644 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000645 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000646 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000647 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000648 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649
650 def LazyUpdateIfNeeded(self):
651 """Updates the settings from a codereview.settings file, if available."""
652 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000653 # The only value that actually changes the behavior is
654 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000655 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000656 error_ok=True
657 ).strip().lower()
658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000660 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 LoadCodereviewSettingsFromFile(cr_settings_file)
662 self.updated = True
663
664 def GetDefaultServerUrl(self, error_ok=False):
665 if not self.default_server:
666 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000667 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000668 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 if error_ok:
670 return self.default_server
671 if not self.default_server:
672 error_message = ('Could not find settings file. You must configure '
673 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000674 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 return self.default_server
677
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000678 @staticmethod
679 def GetRelativeRoot():
680 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000683 if self.root is None:
684 self.root = os.path.abspath(self.GetRelativeRoot())
685 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000687 def GetGitMirror(self, remote='origin'):
688 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000689 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000690 if not os.path.isdir(local_url):
691 return None
692 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
693 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
694 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
695 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
696 if mirror.exists():
697 return mirror
698 return None
699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700 def GetIsGitSvn(self):
701 """Return true if this repo looks like it's using git-svn."""
702 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000703 if self.GetPendingRefPrefix():
704 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
705 self.is_git_svn = False
706 else:
707 # If you have any "svn-remote.*" config keys, we think you're using svn.
708 self.is_git_svn = RunGitWithCode(
709 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 return self.is_git_svn
711
712 def GetSVNBranch(self):
713 if self.svn_branch is None:
714 if not self.GetIsGitSvn():
715 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
716
717 # Try to figure out which remote branch we're based on.
718 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000719 # 1) iterate through our branch history and find the svn URL.
720 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 # regexp matching the git-svn line that contains the URL.
723 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
724
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000725 # We don't want to go through all of history, so read a line from the
726 # pipe at a time.
727 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000728 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
730 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000731 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000732 for line in proc.stdout:
733 match = git_svn_re.match(line)
734 if match:
735 url = match.group(1)
736 proc.stdout.close() # Cut pipe.
737 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000739 if url:
740 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
741 remotes = RunGit(['config', '--get-regexp',
742 r'^svn-remote\..*\.url']).splitlines()
743 for remote in remotes:
744 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000746 remote = match.group(1)
747 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000748 rewrite_root = RunGit(
749 ['config', 'svn-remote.%s.rewriteRoot' % remote],
750 error_ok=True).strip()
751 if rewrite_root:
752 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000753 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000754 ['config', 'svn-remote.%s.fetch' % remote],
755 error_ok=True).strip()
756 if fetch_spec:
757 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
758 if self.svn_branch:
759 break
760 branch_spec = RunGit(
761 ['config', 'svn-remote.%s.branches' % remote],
762 error_ok=True).strip()
763 if branch_spec:
764 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
765 if self.svn_branch:
766 break
767 tag_spec = RunGit(
768 ['config', 'svn-remote.%s.tags' % remote],
769 error_ok=True).strip()
770 if tag_spec:
771 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
772 if self.svn_branch:
773 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 if not self.svn_branch:
776 DieWithError('Can\'t guess svn branch -- try specifying it on the '
777 'command line')
778
779 return self.svn_branch
780
781 def GetTreeStatusUrl(self, error_ok=False):
782 if not self.tree_status_url:
783 error_message = ('You must configure your tree status URL by running '
784 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000785 self.tree_status_url = self._GetRietveldConfig(
786 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 return self.tree_status_url
788
789 def GetViewVCUrl(self):
790 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 return self.viewvc_url
793
rmistry@google.com90752582014-01-14 21:04:50 +0000794 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000796
rmistry@google.com78948ed2015-07-08 23:09:57 +0000797 def GetIsSkipDependencyUpload(self, branch_name):
798 """Returns true if specified branch should skip dep uploads."""
799 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
800 error_ok=True)
801
rmistry@google.com5626a922015-02-26 14:03:30 +0000802 def GetRunPostUploadHook(self):
803 run_post_upload_hook = self._GetRietveldConfig(
804 'run-post-upload-hook', error_ok=True)
805 return run_post_upload_hook == "True"
806
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000807 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000809
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000810 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000812
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 def GetIsGerrit(self):
814 """Return true if this repo is assosiated with gerrit code review system."""
815 if self.is_gerrit is None:
816 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
817 return self.is_gerrit
818
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000819 def GetSquashGerritUploads(self):
820 """Return true if uploads to Gerrit should be squashed by default."""
821 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700822 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
823 if self.squash_gerrit_uploads is None:
824 # Default is squash now (http://crbug.com/611892#c23).
825 self.squash_gerrit_uploads = not (
826 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
827 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000828 return self.squash_gerrit_uploads
829
tandriia60502f2016-06-20 02:01:53 -0700830 def GetSquashGerritUploadsOverride(self):
831 """Return True or False if codereview.settings should be overridden.
832
833 Returns None if no override has been defined.
834 """
835 # See also http://crbug.com/611892#c23
836 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
837 error_ok=True).strip()
838 if result == 'true':
839 return True
840 if result == 'false':
841 return False
842 return None
843
tandrii@chromium.org28253532016-04-14 13:46:56 +0000844 def GetGerritSkipEnsureAuthenticated(self):
845 """Return True if EnsureAuthenticated should not be done for Gerrit
846 uploads."""
847 if self.gerrit_skip_ensure_authenticated is None:
848 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000849 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000850 error_ok=True).strip() == 'true')
851 return self.gerrit_skip_ensure_authenticated
852
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000853 def GetGitEditor(self):
854 """Return the editor specified in the git config, or None if none is."""
855 if self.git_editor is None:
856 self.git_editor = self._GetConfig('core.editor', error_ok=True)
857 return self.git_editor or None
858
thestig@chromium.org44202a22014-03-11 19:22:18 +0000859 def GetLintRegex(self):
860 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
861 DEFAULT_LINT_REGEX)
862
863 def GetLintIgnoreRegex(self):
864 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
865 DEFAULT_LINT_IGNORE_REGEX)
866
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000867 def GetProject(self):
868 if not self.project:
869 self.project = self._GetRietveldConfig('project', error_ok=True)
870 return self.project
871
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000872 def GetForceHttpsCommitUrl(self):
873 if not self.force_https_commit_url:
874 self.force_https_commit_url = self._GetRietveldConfig(
875 'force-https-commit-url', error_ok=True)
876 return self.force_https_commit_url
877
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000878 def GetPendingRefPrefix(self):
879 if not self.pending_ref_prefix:
880 self.pending_ref_prefix = self._GetRietveldConfig(
881 'pending-ref-prefix', error_ok=True)
882 return self.pending_ref_prefix
883
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000884 def _GetRietveldConfig(self, param, **kwargs):
885 return self._GetConfig('rietveld.' + param, **kwargs)
886
rmistry@google.com78948ed2015-07-08 23:09:57 +0000887 def _GetBranchConfig(self, branch_name, param, **kwargs):
888 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 def _GetConfig(self, param, **kwargs):
891 self.LazyUpdateIfNeeded()
892 return RunGit(['config', param], **kwargs).strip()
893
894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895def ShortBranchName(branch):
896 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000897 return branch.replace('refs/heads/', '', 1)
898
899
900def GetCurrentBranchRef():
901 """Returns branch ref (e.g., refs/heads/master) or None."""
902 return RunGit(['symbolic-ref', 'HEAD'],
903 stderr=subprocess2.VOID, error_ok=True).strip() or None
904
905
906def GetCurrentBranch():
907 """Returns current branch or None.
908
909 For refs/heads/* branches, returns just last part. For others, full ref.
910 """
911 branchref = GetCurrentBranchRef()
912 if branchref:
913 return ShortBranchName(branchref)
914 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915
916
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000917class _CQState(object):
918 """Enum for states of CL with respect to Commit Queue."""
919 NONE = 'none'
920 DRY_RUN = 'dry_run'
921 COMMIT = 'commit'
922
923 ALL_STATES = [NONE, DRY_RUN, COMMIT]
924
925
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000926class _ParsedIssueNumberArgument(object):
927 def __init__(self, issue=None, patchset=None, hostname=None):
928 self.issue = issue
929 self.patchset = patchset
930 self.hostname = hostname
931
932 @property
933 def valid(self):
934 return self.issue is not None
935
936
937class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
938 def __init__(self, *args, **kwargs):
939 self.patch_url = kwargs.pop('patch_url', None)
940 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
941
942
943def ParseIssueNumberArgument(arg):
944 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
945 fail_result = _ParsedIssueNumberArgument()
946
947 if arg.isdigit():
948 return _ParsedIssueNumberArgument(issue=int(arg))
949 if not arg.startswith('http'):
950 return fail_result
951 url = gclient_utils.UpgradeToHttps(arg)
952 try:
953 parsed_url = urlparse.urlparse(url)
954 except ValueError:
955 return fail_result
956 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
957 tmp = cls.ParseIssueURL(parsed_url)
958 if tmp is not None:
959 return tmp
960 return fail_result
961
962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 """Changelist works with one changelist in local branch.
965
966 Supports two codereview backends: Rietveld or Gerrit, selected at object
967 creation.
968
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000969 Notes:
970 * Not safe for concurrent multi-{thread,process} use.
971 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700972 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000973 """
974
975 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
976 """Create a new ChangeList instance.
977
978 If issue is given, the codereview must be given too.
979
980 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
981 Otherwise, it's decided based on current configuration of the local branch,
982 with default being 'rietveld' for backwards compatibility.
983 See _load_codereview_impl for more details.
984
985 **kwargs will be passed directly to codereview implementation.
986 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000988 global settings
989 if not settings:
990 # Happens when git_cl.py is used as a utility library.
991 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000992
993 if issue:
994 assert codereview, 'codereview must be known, if issue is known'
995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 self.branchref = branchref
997 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000998 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 self.branch = ShortBranchName(self.branchref)
1000 else:
1001 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001003 self.lookedup_issue = False
1004 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 self.has_description = False
1006 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001007 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001009 self.cc = None
1010 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001011 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001012
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001013 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001014 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001015 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001016 assert self._codereview_impl
1017 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001018
1019 def _load_codereview_impl(self, codereview=None, **kwargs):
1020 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001021 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1022 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1023 self._codereview = codereview
1024 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001025 return
1026
1027 # Automatic selection based on issue number set for a current branch.
1028 # Rietveld takes precedence over Gerrit.
1029 assert not self.issue
1030 # Whether we find issue or not, we are doing the lookup.
1031 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001032 if self.GetBranch():
1033 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1034 issue = _git_get_branch_config_value(
1035 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1036 if issue:
1037 self._codereview = codereview
1038 self._codereview_impl = cls(self, **kwargs)
1039 self.issue = int(issue)
1040 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001041
1042 # No issue is set for this branch, so decide based on repo-wide settings.
1043 return self._load_codereview_impl(
1044 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1045 **kwargs)
1046
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001047 def IsGerrit(self):
1048 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001049
1050 def GetCCList(self):
1051 """Return the users cc'd on this CL.
1052
agable92bec4f2016-08-24 09:27:27 -07001053 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001054 """
1055 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001056 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001057 more_cc = ','.join(self.watchers)
1058 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1059 return self.cc
1060
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001061 def GetCCListWithoutDefault(self):
1062 """Return the users cc'd on this CL excluding default ones."""
1063 if self.cc is None:
1064 self.cc = ','.join(self.watchers)
1065 return self.cc
1066
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001067 def SetWatchers(self, watchers):
1068 """Set the list of email addresses that should be cc'd based on the changed
1069 files in this CL.
1070 """
1071 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
1073 def GetBranch(self):
1074 """Returns the short branch name, e.g. 'master'."""
1075 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001077 if not branchref:
1078 return None
1079 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 self.branch = ShortBranchName(self.branchref)
1081 return self.branch
1082
1083 def GetBranchRef(self):
1084 """Returns the full branch name, e.g. 'refs/heads/master'."""
1085 self.GetBranch() # Poke the lazy loader.
1086 return self.branchref
1087
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001088 def ClearBranch(self):
1089 """Clears cached branch data of this object."""
1090 self.branch = self.branchref = None
1091
tandrii5d48c322016-08-18 16:19:37 -07001092 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1093 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1094 kwargs['branch'] = self.GetBranch()
1095 return _git_get_branch_config_value(key, default, **kwargs)
1096
1097 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1098 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1099 assert self.GetBranch(), (
1100 'this CL must have an associated branch to %sset %s%s' %
1101 ('un' if value is None else '',
1102 key,
1103 '' if value is None else ' to %r' % value))
1104 kwargs['branch'] = self.GetBranch()
1105 return _git_set_branch_config_value(key, value, **kwargs)
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 @staticmethod
1108 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001109 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 e.g. 'origin', 'refs/heads/master'
1111 """
1112 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001113 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001116 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001118 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1119 error_ok=True).strip()
1120 if upstream_branch:
1121 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001123 # Fall back on trying a git-svn upstream branch.
1124 if settings.GetIsGitSvn():
1125 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001127 # Else, try to guess the origin remote.
1128 remote_branches = RunGit(['branch', '-r']).split()
1129 if 'origin/master' in remote_branches:
1130 # Fall back on origin/master if it exits.
1131 remote = 'origin'
1132 upstream_branch = 'refs/heads/master'
1133 elif 'origin/trunk' in remote_branches:
1134 # Fall back on origin/trunk if it exists. Generally a shared
1135 # git-svn clone
1136 remote = 'origin'
1137 upstream_branch = 'refs/heads/trunk'
1138 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139 DieWithError(
1140 'Unable to determine default branch to diff against.\n'
1141 'Either pass complete "git diff"-style arguments, like\n'
1142 ' git cl upload origin/master\n'
1143 'or verify this branch is set up to track another \n'
1144 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 return remote, upstream_branch
1147
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001148 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001149 upstream_branch = self.GetUpstreamBranch()
1150 if not BranchExists(upstream_branch):
1151 DieWithError('The upstream for the current branch (%s) does not exist '
1152 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001153 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001154 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetUpstreamBranch(self):
1157 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001160 upstream_branch = upstream_branch.replace('refs/heads/',
1161 'refs/remotes/%s/' % remote)
1162 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1163 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 self.upstream_branch = upstream_branch
1165 return self.upstream_branch
1166
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001168 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 remote, branch = None, self.GetBranch()
1170 seen_branches = set()
1171 while branch not in seen_branches:
1172 seen_branches.add(branch)
1173 remote, branch = self.FetchUpstreamTuple(branch)
1174 branch = ShortBranchName(branch)
1175 if remote != '.' or branch.startswith('refs/remotes'):
1176 break
1177 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001178 remotes = RunGit(['remote'], error_ok=True).split()
1179 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001180 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001181 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001182 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001183 logging.warning('Could not determine which remote this change is '
1184 'associated with, so defaulting to "%s". This may '
1185 'not be what you want. You may prevent this message '
1186 'by running "git svn info" as documented here: %s',
1187 self._remote,
1188 GIT_INSTRUCTIONS_URL)
1189 else:
1190 logging.warn('Could not determine which remote this change is '
1191 'associated with. You may prevent this message by '
1192 'running "git svn info" as documented here: %s',
1193 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001194 branch = 'HEAD'
1195 if branch.startswith('refs/remotes'):
1196 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001197 elif branch.startswith('refs/branch-heads/'):
1198 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001199 else:
1200 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001201 return self._remote
1202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 def GitSanityChecks(self, upstream_git_obj):
1204 """Checks git repo status and ensures diff is from local commits."""
1205
sbc@chromium.org79706062015-01-14 21:18:12 +00001206 if upstream_git_obj is None:
1207 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001208 print('ERROR: unable to determine current branch (detached HEAD?)',
1209 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001210 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001211 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001212 return False
1213
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001214 # Verify the commit we're diffing against is in our current branch.
1215 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1216 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1217 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001218 print('ERROR: %s is not in the current branch. You may need to rebase '
1219 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 return False
1221
1222 # List the commits inside the diff, and verify they are all local.
1223 commits_in_diff = RunGit(
1224 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1225 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1226 remote_branch = remote_branch.strip()
1227 if code != 0:
1228 _, remote_branch = self.GetRemoteBranch()
1229
1230 commits_in_remote = RunGit(
1231 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1232
1233 common_commits = set(commits_in_diff) & set(commits_in_remote)
1234 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001235 print('ERROR: Your diff contains %d commits already in %s.\n'
1236 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1237 'the diff. If you are using a custom git flow, you can override'
1238 ' the reference used for this check with "git config '
1239 'gitcl.remotebranch <git-ref>".' % (
1240 len(common_commits), remote_branch, upstream_git_obj),
1241 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 return False
1243 return True
1244
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001245 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001246 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001247
1248 Returns None if it is not set.
1249 """
tandrii5d48c322016-08-18 16:19:37 -07001250 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001251
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001252 def GetGitSvnRemoteUrl(self):
1253 """Return the configured git-svn remote URL parsed from git svn info.
1254
1255 Returns None if it is not set.
1256 """
1257 # URL is dependent on the current directory.
1258 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1259 if data:
1260 keys = dict(line.split(': ', 1) for line in data.splitlines()
1261 if ': ' in line)
1262 return keys.get('URL', None)
1263 return None
1264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 def GetRemoteUrl(self):
1266 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1267
1268 Returns None if there is no remote.
1269 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001271 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1272
1273 # If URL is pointing to a local directory, it is probably a git cache.
1274 if os.path.isdir(url):
1275 url = RunGit(['config', 'remote.%s.url' % remote],
1276 error_ok=True,
1277 cwd=url).strip()
1278 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001280 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001281 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001282 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001283 self.issue = self._GitGetBranchConfigValue(
1284 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001285 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 return self.issue
1287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetIssueURL(self):
1289 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001290 issue = self.GetIssue()
1291 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001292 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001293 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
1295 def GetDescription(self, pretty=False):
1296 if not self.has_description:
1297 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 self.has_description = True
1300 if pretty:
1301 wrapper = textwrap.TextWrapper()
1302 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1303 return wrapper.fill(self.description)
1304 return self.description
1305
1306 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001307 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001308 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001309 self.patchset = self._GitGetBranchConfigValue(
1310 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001311 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 return self.patchset
1313
1314 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001315 """Set this branch's patchset. If patchset=0, clears the patchset."""
1316 assert self.GetBranch()
1317 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001318 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001319 else:
1320 self.patchset = int(patchset)
1321 self._GitSetBranchConfigValue(
1322 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001324 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001325 """Set this branch's issue. If issue isn't given, clears the issue."""
1326 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001328 issue = int(issue)
1329 self._GitSetBranchConfigValue(
1330 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001331 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001332 codereview_server = self._codereview_impl.GetCodereviewServer()
1333 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001334 self._GitSetBranchConfigValue(
1335 self._codereview_impl.CodereviewServerConfigKey(),
1336 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 else:
tandrii5d48c322016-08-18 16:19:37 -07001338 # Reset all of these just to be clean.
1339 reset_suffixes = [
1340 'last-upload-hash',
1341 self._codereview_impl.IssueConfigKey(),
1342 self._codereview_impl.PatchsetConfigKey(),
1343 self._codereview_impl.CodereviewServerConfigKey(),
1344 ] + self._PostUnsetIssueProperties()
1345 for prop in reset_suffixes:
1346 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001348 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001350 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 if not self.GitSanityChecks(upstream_branch):
1352 DieWithError('\nGit sanity check failure')
1353
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001354 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001355 if not root:
1356 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001357 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001358
1359 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001360 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001361 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001362 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001363 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001364 except subprocess2.CalledProcessError:
1365 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001366 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001367 'This branch probably doesn\'t exist anymore. To reset the\n'
1368 'tracking branch, please run\n'
1369 ' git branch --set-upstream %s trunk\n'
1370 'replacing trunk with origin/master or the relevant branch') %
1371 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001372
maruel@chromium.org52424302012-08-29 15:14:30 +00001373 issue = self.GetIssue()
1374 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001375 if issue:
1376 description = self.GetDescription()
1377 else:
1378 # If the change was never uploaded, use the log messages of all commits
1379 # up to the branch point, as git cl upload will prefill the description
1380 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001381 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1382 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001383
1384 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001385 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001386 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001387 name,
1388 description,
1389 absroot,
1390 files,
1391 issue,
1392 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001393 author,
1394 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001395
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001396 def UpdateDescription(self, description):
1397 self.description = description
1398 return self._codereview_impl.UpdateDescriptionRemote(description)
1399
1400 def RunHook(self, committing, may_prompt, verbose, change):
1401 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1402 try:
1403 return presubmit_support.DoPresubmitChecks(change, committing,
1404 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1405 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001406 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1407 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001408 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001409 DieWithError(
1410 ('%s\nMaybe your depot_tools is out of date?\n'
1411 'If all fails, contact maruel@') % e)
1412
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001413 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1414 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001415 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1416 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001417 else:
1418 # Assume url.
1419 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1420 urlparse.urlparse(issue_arg))
1421 if not parsed_issue_arg or not parsed_issue_arg.valid:
1422 DieWithError('Failed to parse issue argument "%s". '
1423 'Must be an issue number or a valid URL.' % issue_arg)
1424 return self._codereview_impl.CMDPatchWithParsedIssue(
1425 parsed_issue_arg, reject, nocommit, directory)
1426
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001427 def CMDUpload(self, options, git_diff_args, orig_args):
1428 """Uploads a change to codereview."""
1429 if git_diff_args:
1430 # TODO(ukai): is it ok for gerrit case?
1431 base_branch = git_diff_args[0]
1432 else:
1433 if self.GetBranch() is None:
1434 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1435
1436 # Default to diffing against common ancestor of upstream branch
1437 base_branch = self.GetCommonAncestorWithUpstream()
1438 git_diff_args = [base_branch, 'HEAD']
1439
1440 # Make sure authenticated to codereview before running potentially expensive
1441 # hooks. It is a fast, best efforts check. Codereview still can reject the
1442 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001443 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001444
1445 # Apply watchlists on upload.
1446 change = self.GetChange(base_branch, None)
1447 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1448 files = [f.LocalPath() for f in change.AffectedFiles()]
1449 if not options.bypass_watchlists:
1450 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1451
1452 if not options.bypass_hooks:
1453 if options.reviewers or options.tbr_owners:
1454 # Set the reviewer list now so that presubmit checks can access it.
1455 change_description = ChangeDescription(change.FullDescriptionText())
1456 change_description.update_reviewers(options.reviewers,
1457 options.tbr_owners,
1458 change)
1459 change.SetDescriptionText(change_description.description)
1460 hook_results = self.RunHook(committing=False,
1461 may_prompt=not options.force,
1462 verbose=options.verbose,
1463 change=change)
1464 if not hook_results.should_continue():
1465 return 1
1466 if not options.reviewers and hook_results.reviewers:
1467 options.reviewers = hook_results.reviewers.split(',')
1468
1469 if self.GetIssue():
1470 latest_patchset = self.GetMostRecentPatchset()
1471 local_patchset = self.GetPatchset()
1472 if (latest_patchset and local_patchset and
1473 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001474 print('The last upload made from this repository was patchset #%d but '
1475 'the most recent patchset on the server is #%d.'
1476 % (local_patchset, latest_patchset))
1477 print('Uploading will still work, but if you\'ve uploaded to this '
1478 'issue from another machine or branch the patch you\'re '
1479 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001480 ask_for_data('About to upload; enter to confirm.')
1481
1482 print_stats(options.similarity, options.find_copies, git_diff_args)
1483 ret = self.CMDUploadChange(options, git_diff_args, change)
1484 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001485 if options.use_commit_queue:
1486 self.SetCQState(_CQState.COMMIT)
1487 elif options.cq_dry_run:
1488 self.SetCQState(_CQState.DRY_RUN)
1489
tandrii5d48c322016-08-18 16:19:37 -07001490 _git_set_branch_config_value('last-upload-hash',
1491 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001492 # Run post upload hooks, if specified.
1493 if settings.GetRunPostUploadHook():
1494 presubmit_support.DoPostUploadExecuter(
1495 change,
1496 self,
1497 settings.GetRoot(),
1498 options.verbose,
1499 sys.stdout)
1500
1501 # Upload all dependencies if specified.
1502 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001503 print()
1504 print('--dependencies has been specified.')
1505 print('All dependent local branches will be re-uploaded.')
1506 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001507 # Remove the dependencies flag from args so that we do not end up in a
1508 # loop.
1509 orig_args.remove('--dependencies')
1510 ret = upload_branch_deps(self, orig_args)
1511 return ret
1512
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001513 def SetCQState(self, new_state):
1514 """Update the CQ state for latest patchset.
1515
1516 Issue must have been already uploaded and known.
1517 """
1518 assert new_state in _CQState.ALL_STATES
1519 assert self.GetIssue()
1520 return self._codereview_impl.SetCQState(new_state)
1521
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522 # Forward methods to codereview specific implementation.
1523
1524 def CloseIssue(self):
1525 return self._codereview_impl.CloseIssue()
1526
1527 def GetStatus(self):
1528 return self._codereview_impl.GetStatus()
1529
1530 def GetCodereviewServer(self):
1531 return self._codereview_impl.GetCodereviewServer()
1532
1533 def GetApprovingReviewers(self):
1534 return self._codereview_impl.GetApprovingReviewers()
1535
1536 def GetMostRecentPatchset(self):
1537 return self._codereview_impl.GetMostRecentPatchset()
1538
1539 def __getattr__(self, attr):
1540 # This is because lots of untested code accesses Rietveld-specific stuff
1541 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001542 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001543 # Note that child method defines __getattr__ as well, and forwards it here,
1544 # because _RietveldChangelistImpl is not cleaned up yet, and given
1545 # deprecation of Rietveld, it should probably be just removed.
1546 # Until that time, avoid infinite recursion by bypassing __getattr__
1547 # of implementation class.
1548 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001549
1550
1551class _ChangelistCodereviewBase(object):
1552 """Abstract base class encapsulating codereview specifics of a changelist."""
1553 def __init__(self, changelist):
1554 self._changelist = changelist # instance of Changelist
1555
1556 def __getattr__(self, attr):
1557 # Forward methods to changelist.
1558 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1559 # _RietveldChangelistImpl to avoid this hack?
1560 return getattr(self._changelist, attr)
1561
1562 def GetStatus(self):
1563 """Apply a rough heuristic to give a simple summary of an issue's review
1564 or CQ status, assuming adherence to a common workflow.
1565
1566 Returns None if no issue for this branch, or specific string keywords.
1567 """
1568 raise NotImplementedError()
1569
1570 def GetCodereviewServer(self):
1571 """Returns server URL without end slash, like "https://codereview.com"."""
1572 raise NotImplementedError()
1573
1574 def FetchDescription(self):
1575 """Fetches and returns description from the codereview server."""
1576 raise NotImplementedError()
1577
tandrii5d48c322016-08-18 16:19:37 -07001578 @classmethod
1579 def IssueConfigKey(cls):
1580 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001581 raise NotImplementedError()
1582
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001583 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001584 def PatchsetConfigKey(cls):
1585 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001586 raise NotImplementedError()
1587
tandrii5d48c322016-08-18 16:19:37 -07001588 @classmethod
1589 def CodereviewServerConfigKey(cls):
1590 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001591 raise NotImplementedError()
1592
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001593 def _PostUnsetIssueProperties(self):
1594 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001595 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001596
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001597 def GetRieveldObjForPresubmit(self):
1598 # This is an unfortunate Rietveld-embeddedness in presubmit.
1599 # For non-Rietveld codereviews, this probably should return a dummy object.
1600 raise NotImplementedError()
1601
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001602 def GetGerritObjForPresubmit(self):
1603 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1604 return None
1605
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001606 def UpdateDescriptionRemote(self, description):
1607 """Update the description on codereview site."""
1608 raise NotImplementedError()
1609
1610 def CloseIssue(self):
1611 """Closes the issue."""
1612 raise NotImplementedError()
1613
1614 def GetApprovingReviewers(self):
1615 """Returns a list of reviewers approving the change.
1616
1617 Note: not necessarily committers.
1618 """
1619 raise NotImplementedError()
1620
1621 def GetMostRecentPatchset(self):
1622 """Returns the most recent patchset number from the codereview site."""
1623 raise NotImplementedError()
1624
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001625 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1626 directory):
1627 """Fetches and applies the issue.
1628
1629 Arguments:
1630 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1631 reject: if True, reject the failed patch instead of switching to 3-way
1632 merge. Rietveld only.
1633 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1634 only.
1635 directory: switch to directory before applying the patch. Rietveld only.
1636 """
1637 raise NotImplementedError()
1638
1639 @staticmethod
1640 def ParseIssueURL(parsed_url):
1641 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1642 failed."""
1643 raise NotImplementedError()
1644
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001645 def EnsureAuthenticated(self, force):
1646 """Best effort check that user is authenticated with codereview server.
1647
1648 Arguments:
1649 force: whether to skip confirmation questions.
1650 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001651 raise NotImplementedError()
1652
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001653 def CMDUploadChange(self, options, args, change):
1654 """Uploads a change to codereview."""
1655 raise NotImplementedError()
1656
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001657 def SetCQState(self, new_state):
1658 """Update the CQ state for latest patchset.
1659
1660 Issue must have been already uploaded and known.
1661 """
1662 raise NotImplementedError()
1663
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001664
1665class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1666 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1667 super(_RietveldChangelistImpl, self).__init__(changelist)
1668 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001669 if not rietveld_server:
1670 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001671
1672 self._rietveld_server = rietveld_server
1673 self._auth_config = auth_config
1674 self._props = None
1675 self._rpc_server = None
1676
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677 def GetCodereviewServer(self):
1678 if not self._rietveld_server:
1679 # If we're on a branch then get the server potentially associated
1680 # with that branch.
1681 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001682 self._rietveld_server = gclient_utils.UpgradeToHttps(
1683 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001684 if not self._rietveld_server:
1685 self._rietveld_server = settings.GetDefaultServerUrl()
1686 return self._rietveld_server
1687
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001688 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001689 """Best effort check that user is authenticated with Rietveld server."""
1690 if self._auth_config.use_oauth2:
1691 authenticator = auth.get_authenticator_for_host(
1692 self.GetCodereviewServer(), self._auth_config)
1693 if not authenticator.has_cached_credentials():
1694 raise auth.LoginRequiredError(self.GetCodereviewServer())
1695
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 def FetchDescription(self):
1697 issue = self.GetIssue()
1698 assert issue
1699 try:
1700 return self.RpcServer().get_description(issue).strip()
1701 except urllib2.HTTPError as e:
1702 if e.code == 404:
1703 DieWithError(
1704 ('\nWhile fetching the description for issue %d, received a '
1705 '404 (not found)\n'
1706 'error. It is likely that you deleted this '
1707 'issue on the server. If this is the\n'
1708 'case, please run\n\n'
1709 ' git cl issue 0\n\n'
1710 'to clear the association with the deleted issue. Then run '
1711 'this command again.') % issue)
1712 else:
1713 DieWithError(
1714 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1715 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001716 print('Warning: Failed to retrieve CL description due to network '
1717 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001718 return ''
1719
1720 def GetMostRecentPatchset(self):
1721 return self.GetIssueProperties()['patchsets'][-1]
1722
1723 def GetPatchSetDiff(self, issue, patchset):
1724 return self.RpcServer().get(
1725 '/download/issue%s_%s.diff' % (issue, patchset))
1726
1727 def GetIssueProperties(self):
1728 if self._props is None:
1729 issue = self.GetIssue()
1730 if not issue:
1731 self._props = {}
1732 else:
1733 self._props = self.RpcServer().get_issue_properties(issue, True)
1734 return self._props
1735
1736 def GetApprovingReviewers(self):
1737 return get_approving_reviewers(self.GetIssueProperties())
1738
1739 def AddComment(self, message):
1740 return self.RpcServer().add_comment(self.GetIssue(), message)
1741
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001742 def GetStatus(self):
1743 """Apply a rough heuristic to give a simple summary of an issue's review
1744 or CQ status, assuming adherence to a common workflow.
1745
1746 Returns None if no issue for this branch, or one of the following keywords:
1747 * 'error' - error from review tool (including deleted issues)
1748 * 'unsent' - not sent for review
1749 * 'waiting' - waiting for review
1750 * 'reply' - waiting for owner to reply to review
1751 * 'lgtm' - LGTM from at least one approved reviewer
1752 * 'commit' - in the commit queue
1753 * 'closed' - closed
1754 """
1755 if not self.GetIssue():
1756 return None
1757
1758 try:
1759 props = self.GetIssueProperties()
1760 except urllib2.HTTPError:
1761 return 'error'
1762
1763 if props.get('closed'):
1764 # Issue is closed.
1765 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001766 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001767 # Issue is in the commit queue.
1768 return 'commit'
1769
1770 try:
1771 reviewers = self.GetApprovingReviewers()
1772 except urllib2.HTTPError:
1773 return 'error'
1774
1775 if reviewers:
1776 # Was LGTM'ed.
1777 return 'lgtm'
1778
1779 messages = props.get('messages') or []
1780
tandrii9d2c7a32016-06-22 03:42:45 -07001781 # Skip CQ messages that don't require owner's action.
1782 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1783 if 'Dry run:' in messages[-1]['text']:
1784 messages.pop()
1785 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1786 # This message always follows prior messages from CQ,
1787 # so skip this too.
1788 messages.pop()
1789 else:
1790 # This is probably a CQ messages warranting user attention.
1791 break
1792
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001793 if not messages:
1794 # No message was sent.
1795 return 'unsent'
1796 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001797 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001798 return 'reply'
1799 return 'waiting'
1800
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001802 return self.RpcServer().update_description(
1803 self.GetIssue(), self.description)
1804
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001806 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001807
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001808 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001809 return self.SetFlags({flag: value})
1810
1811 def SetFlags(self, flags):
1812 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001813 """
phajdan.jr68598232016-08-10 03:28:28 -07001814 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001815 try:
tandrii4b233bd2016-07-06 03:50:29 -07001816 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001817 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001818 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001819 if e.code == 404:
1820 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1821 if e.code == 403:
1822 DieWithError(
1823 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001824 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001825 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001827 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828 """Returns an upload.RpcServer() to access this review's rietveld instance.
1829 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001830 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001831 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001833 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001834 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001836 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001837 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001838 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839
tandrii5d48c322016-08-18 16:19:37 -07001840 @classmethod
1841 def PatchsetConfigKey(cls):
1842 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843
tandrii5d48c322016-08-18 16:19:37 -07001844 @classmethod
1845 def CodereviewServerConfigKey(cls):
1846 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001847
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 def GetRieveldObjForPresubmit(self):
1849 return self.RpcServer()
1850
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001851 def SetCQState(self, new_state):
1852 props = self.GetIssueProperties()
1853 if props.get('private'):
1854 DieWithError('Cannot set-commit on private issue')
1855
1856 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001857 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001858 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001859 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001860 else:
tandrii4b233bd2016-07-06 03:50:29 -07001861 assert new_state == _CQState.DRY_RUN
1862 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001863
1864
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001865 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1866 directory):
1867 # TODO(maruel): Use apply_issue.py
1868
1869 # PatchIssue should never be called with a dirty tree. It is up to the
1870 # caller to check this, but just in case we assert here since the
1871 # consequences of the caller not checking this could be dire.
1872 assert(not git_common.is_dirty_git_tree('apply'))
1873 assert(parsed_issue_arg.valid)
1874 self._changelist.issue = parsed_issue_arg.issue
1875 if parsed_issue_arg.hostname:
1876 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1877
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001878 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1879 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001880 assert parsed_issue_arg.patchset
1881 patchset = parsed_issue_arg.patchset
1882 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1883 else:
1884 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1885 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1886
1887 # Switch up to the top-level directory, if necessary, in preparation for
1888 # applying the patch.
1889 top = settings.GetRelativeRoot()
1890 if top:
1891 os.chdir(top)
1892
1893 # Git patches have a/ at the beginning of source paths. We strip that out
1894 # with a sed script rather than the -p flag to patch so we can feed either
1895 # Git or svn-style patches into the same apply command.
1896 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1897 try:
1898 patch_data = subprocess2.check_output(
1899 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1900 except subprocess2.CalledProcessError:
1901 DieWithError('Git patch mungling failed.')
1902 logging.info(patch_data)
1903
1904 # We use "git apply" to apply the patch instead of "patch" so that we can
1905 # pick up file adds.
1906 # The --index flag means: also insert into the index (so we catch adds).
1907 cmd = ['git', 'apply', '--index', '-p0']
1908 if directory:
1909 cmd.extend(('--directory', directory))
1910 if reject:
1911 cmd.append('--reject')
1912 elif IsGitVersionAtLeast('1.7.12'):
1913 cmd.append('--3way')
1914 try:
1915 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1916 stdin=patch_data, stdout=subprocess2.VOID)
1917 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001918 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001919 return 1
1920
1921 # If we had an issue, commit the current state and register the issue.
1922 if not nocommit:
1923 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1924 'patch from issue %(i)s at patchset '
1925 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1926 % {'i': self.GetIssue(), 'p': patchset})])
1927 self.SetIssue(self.GetIssue())
1928 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001929 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001930 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001931 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001932 return 0
1933
1934 @staticmethod
1935 def ParseIssueURL(parsed_url):
1936 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1937 return None
wychen3c1c1722016-08-04 11:46:36 -07001938 # Rietveld patch: https://domain/<number>/#ps<patchset>
1939 match = re.match(r'/(\d+)/$', parsed_url.path)
1940 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1941 if match and match2:
1942 return _RietveldParsedIssueNumberArgument(
1943 issue=int(match.group(1)),
1944 patchset=int(match2.group(1)),
1945 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001946 # Typical url: https://domain/<issue_number>[/[other]]
1947 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1948 if match:
1949 return _RietveldParsedIssueNumberArgument(
1950 issue=int(match.group(1)),
1951 hostname=parsed_url.netloc)
1952 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1953 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1954 if match:
1955 return _RietveldParsedIssueNumberArgument(
1956 issue=int(match.group(1)),
1957 patchset=int(match.group(2)),
1958 hostname=parsed_url.netloc,
1959 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1960 return None
1961
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001962 def CMDUploadChange(self, options, args, change):
1963 """Upload the patch to Rietveld."""
1964 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1965 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001966 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1967 if options.emulate_svn_auto_props:
1968 upload_args.append('--emulate_svn_auto_props')
1969
1970 change_desc = None
1971
1972 if options.email is not None:
1973 upload_args.extend(['--email', options.email])
1974
1975 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001976 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001977 upload_args.extend(['--title', options.title])
1978 if options.message:
1979 upload_args.extend(['--message', options.message])
1980 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001981 print('This branch is associated with issue %s. '
1982 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001983 else:
nodirca166002016-06-27 10:59:51 -07001984 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001985 upload_args.extend(['--title', options.title])
1986 message = (options.title or options.message or
1987 CreateDescriptionFromLog(args))
1988 change_desc = ChangeDescription(message)
1989 if options.reviewers or options.tbr_owners:
1990 change_desc.update_reviewers(options.reviewers,
1991 options.tbr_owners,
1992 change)
1993 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001994 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995
1996 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001997 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001998 return 1
1999
2000 upload_args.extend(['--message', change_desc.description])
2001 if change_desc.get_reviewers():
2002 upload_args.append('--reviewers=%s' % ','.join(
2003 change_desc.get_reviewers()))
2004 if options.send_mail:
2005 if not change_desc.get_reviewers():
2006 DieWithError("Must specify reviewers to send email.")
2007 upload_args.append('--send_mail')
2008
2009 # We check this before applying rietveld.private assuming that in
2010 # rietveld.cc only addresses which we can send private CLs to are listed
2011 # if rietveld.private is set, and so we should ignore rietveld.cc only
2012 # when --private is specified explicitly on the command line.
2013 if options.private:
2014 logging.warn('rietveld.cc is ignored since private flag is specified. '
2015 'You need to review and add them manually if necessary.')
2016 cc = self.GetCCListWithoutDefault()
2017 else:
2018 cc = self.GetCCList()
2019 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2020 if cc:
2021 upload_args.extend(['--cc', cc])
2022
2023 if options.private or settings.GetDefaultPrivateFlag() == "True":
2024 upload_args.append('--private')
2025
2026 upload_args.extend(['--git_similarity', str(options.similarity)])
2027 if not options.find_copies:
2028 upload_args.extend(['--git_no_find_copies'])
2029
2030 # Include the upstream repo's URL in the change -- this is useful for
2031 # projects that have their source spread across multiple repos.
2032 remote_url = self.GetGitBaseUrlFromConfig()
2033 if not remote_url:
2034 if settings.GetIsGitSvn():
2035 remote_url = self.GetGitSvnRemoteUrl()
2036 else:
2037 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2038 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2039 self.GetUpstreamBranch().split('/')[-1])
2040 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002041 remote, remote_branch = self.GetRemoteBranch()
2042 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2043 settings.GetPendingRefPrefix())
2044 if target_ref:
2045 upload_args.extend(['--target_ref', target_ref])
2046
2047 # Look for dependent patchsets. See crbug.com/480453 for more details.
2048 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2049 upstream_branch = ShortBranchName(upstream_branch)
2050 if remote is '.':
2051 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002052 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002053 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002054 print()
2055 print('Skipping dependency patchset upload because git config '
2056 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2057 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002058 else:
2059 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002060 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002061 auth_config=auth_config)
2062 branch_cl_issue_url = branch_cl.GetIssueURL()
2063 branch_cl_issue = branch_cl.GetIssue()
2064 branch_cl_patchset = branch_cl.GetPatchset()
2065 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2066 upload_args.extend(
2067 ['--depends_on_patchset', '%s:%s' % (
2068 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002069 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002070 '\n'
2071 'The current branch (%s) is tracking a local branch (%s) with '
2072 'an associated CL.\n'
2073 'Adding %s/#ps%s as a dependency patchset.\n'
2074 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2075 branch_cl_patchset))
2076
2077 project = settings.GetProject()
2078 if project:
2079 upload_args.extend(['--project', project])
2080
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002081 try:
2082 upload_args = ['upload'] + upload_args + args
2083 logging.info('upload.RealMain(%s)', upload_args)
2084 issue, patchset = upload.RealMain(upload_args)
2085 issue = int(issue)
2086 patchset = int(patchset)
2087 except KeyboardInterrupt:
2088 sys.exit(1)
2089 except:
2090 # If we got an exception after the user typed a description for their
2091 # change, back up the description before re-raising.
2092 if change_desc:
2093 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2094 print('\nGot exception while uploading -- saving description to %s\n' %
2095 backup_path)
2096 backup_file = open(backup_path, 'w')
2097 backup_file.write(change_desc.description)
2098 backup_file.close()
2099 raise
2100
2101 if not self.GetIssue():
2102 self.SetIssue(issue)
2103 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002104 return 0
2105
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002106
2107class _GerritChangelistImpl(_ChangelistCodereviewBase):
2108 def __init__(self, changelist, auth_config=None):
2109 # auth_config is Rietveld thing, kept here to preserve interface only.
2110 super(_GerritChangelistImpl, self).__init__(changelist)
2111 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002112 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002113 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002114 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115
2116 def _GetGerritHost(self):
2117 # Lazy load of configs.
2118 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002119 if self._gerrit_host and '.' not in self._gerrit_host:
2120 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2121 # This happens for internal stuff http://crbug.com/614312.
2122 parsed = urlparse.urlparse(self.GetRemoteUrl())
2123 if parsed.scheme == 'sso':
2124 print('WARNING: using non https URLs for remote is likely broken\n'
2125 ' Your current remote is: %s' % self.GetRemoteUrl())
2126 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2127 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002128 return self._gerrit_host
2129
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002130 def _GetGitHost(self):
2131 """Returns git host to be used when uploading change to Gerrit."""
2132 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2133
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002134 def GetCodereviewServer(self):
2135 if not self._gerrit_server:
2136 # If we're on a branch then get the server potentially associated
2137 # with that branch.
2138 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002139 self._gerrit_server = self._GitGetBranchConfigValue(
2140 self.CodereviewServerConfigKey())
2141 if self._gerrit_server:
2142 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002143 if not self._gerrit_server:
2144 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2145 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002146 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002147 parts[0] = parts[0] + '-review'
2148 self._gerrit_host = '.'.join(parts)
2149 self._gerrit_server = 'https://%s' % self._gerrit_host
2150 return self._gerrit_server
2151
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002152 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002153 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002154 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002155
tandrii5d48c322016-08-18 16:19:37 -07002156 @classmethod
2157 def PatchsetConfigKey(cls):
2158 return 'gerritpatchset'
2159
2160 @classmethod
2161 def CodereviewServerConfigKey(cls):
2162 return 'gerritserver'
2163
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002164 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002165 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002166 if settings.GetGerritSkipEnsureAuthenticated():
2167 # For projects with unusual authentication schemes.
2168 # See http://crbug.com/603378.
2169 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002170 # Lazy-loader to identify Gerrit and Git hosts.
2171 if gerrit_util.GceAuthenticator.is_gce():
2172 return
2173 self.GetCodereviewServer()
2174 git_host = self._GetGitHost()
2175 assert self._gerrit_server and self._gerrit_host
2176 cookie_auth = gerrit_util.CookiesAuthenticator()
2177
2178 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2179 git_auth = cookie_auth.get_auth_header(git_host)
2180 if gerrit_auth and git_auth:
2181 if gerrit_auth == git_auth:
2182 return
2183 print((
2184 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2185 ' Check your %s or %s file for credentials of hosts:\n'
2186 ' %s\n'
2187 ' %s\n'
2188 ' %s') %
2189 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2190 git_host, self._gerrit_host,
2191 cookie_auth.get_new_password_message(git_host)))
2192 if not force:
2193 ask_for_data('If you know what you are doing, press Enter to continue, '
2194 'Ctrl+C to abort.')
2195 return
2196 else:
2197 missing = (
2198 [] if gerrit_auth else [self._gerrit_host] +
2199 [] if git_auth else [git_host])
2200 DieWithError('Credentials for the following hosts are required:\n'
2201 ' %s\n'
2202 'These are read from %s (or legacy %s)\n'
2203 '%s' % (
2204 '\n '.join(missing),
2205 cookie_auth.get_gitcookies_path(),
2206 cookie_auth.get_netrc_path(),
2207 cookie_auth.get_new_password_message(git_host)))
2208
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002209 def _PostUnsetIssueProperties(self):
2210 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002211 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002212
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002213 def GetRieveldObjForPresubmit(self):
2214 class ThisIsNotRietveldIssue(object):
2215 def __nonzero__(self):
2216 # This is a hack to make presubmit_support think that rietveld is not
2217 # defined, yet still ensure that calls directly result in a decent
2218 # exception message below.
2219 return False
2220
2221 def __getattr__(self, attr):
2222 print(
2223 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2224 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2225 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2226 'or use Rietveld for codereview.\n'
2227 'See also http://crbug.com/579160.' % attr)
2228 raise NotImplementedError()
2229 return ThisIsNotRietveldIssue()
2230
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002231 def GetGerritObjForPresubmit(self):
2232 return presubmit_support.GerritAccessor(self._GetGerritHost())
2233
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002234 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002235 """Apply a rough heuristic to give a simple summary of an issue's review
2236 or CQ status, assuming adherence to a common workflow.
2237
2238 Returns None if no issue for this branch, or one of the following keywords:
2239 * 'error' - error from review tool (including deleted issues)
2240 * 'unsent' - no reviewers added
2241 * 'waiting' - waiting for review
2242 * 'reply' - waiting for owner to reply to review
2243 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2244 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2245 * 'commit' - in the commit queue
2246 * 'closed' - abandoned
2247 """
2248 if not self.GetIssue():
2249 return None
2250
2251 try:
2252 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2253 except httplib.HTTPException:
2254 return 'error'
2255
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002256 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002257 return 'closed'
2258
2259 cq_label = data['labels'].get('Commit-Queue', {})
2260 if cq_label:
2261 # Vote value is a stringified integer, which we expect from 0 to 2.
2262 vote_value = cq_label.get('value', '0')
2263 vote_text = cq_label.get('values', {}).get(vote_value, '')
2264 if vote_text.lower() == 'commit':
2265 return 'commit'
2266
2267 lgtm_label = data['labels'].get('Code-Review', {})
2268 if lgtm_label:
2269 if 'rejected' in lgtm_label:
2270 return 'not lgtm'
2271 if 'approved' in lgtm_label:
2272 return 'lgtm'
2273
2274 if not data.get('reviewers', {}).get('REVIEWER', []):
2275 return 'unsent'
2276
2277 messages = data.get('messages', [])
2278 if messages:
2279 owner = data['owner'].get('_account_id')
2280 last_message_author = messages[-1].get('author', {}).get('_account_id')
2281 if owner != last_message_author:
2282 # Some reply from non-owner.
2283 return 'reply'
2284
2285 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002286
2287 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002288 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002289 return data['revisions'][data['current_revision']]['_number']
2290
2291 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002292 data = self._GetChangeDetail(['CURRENT_REVISION'])
2293 current_rev = data['current_revision']
2294 url = data['revisions'][current_rev]['fetch']['http']['url']
2295 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002296
2297 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002298 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2299 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002300
2301 def CloseIssue(self):
2302 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2303
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002304 def GetApprovingReviewers(self):
2305 """Returns a list of reviewers approving the change.
2306
2307 Note: not necessarily committers.
2308 """
2309 raise NotImplementedError()
2310
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002311 def SubmitIssue(self, wait_for_merge=True):
2312 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2313 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002315 def _GetChangeDetail(self, options=None, issue=None):
2316 options = options or []
2317 issue = issue or self.GetIssue()
2318 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002319 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2320 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002321
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002322 def CMDLand(self, force, bypass_hooks, verbose):
2323 if git_common.is_dirty_git_tree('land'):
2324 return 1
tandriid60367b2016-06-22 05:25:12 -07002325 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2326 if u'Commit-Queue' in detail.get('labels', {}):
2327 if not force:
2328 ask_for_data('\nIt seems this repository has a Commit Queue, '
2329 'which can test and land changes for you. '
2330 'Are you sure you wish to bypass it?\n'
2331 'Press Enter to continue, Ctrl+C to abort.')
2332
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002333 differs = True
tandriic4344b52016-08-29 06:04:54 -07002334 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002335 # Note: git diff outputs nothing if there is no diff.
2336 if not last_upload or RunGit(['diff', last_upload]).strip():
2337 print('WARNING: some changes from local branch haven\'t been uploaded')
2338 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002339 if detail['current_revision'] == last_upload:
2340 differs = False
2341 else:
2342 print('WARNING: local branch contents differ from latest uploaded '
2343 'patchset')
2344 if differs:
2345 if not force:
2346 ask_for_data(
2347 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2348 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2349 elif not bypass_hooks:
2350 hook_results = self.RunHook(
2351 committing=True,
2352 may_prompt=not force,
2353 verbose=verbose,
2354 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2355 if not hook_results.should_continue():
2356 return 1
2357
2358 self.SubmitIssue(wait_for_merge=True)
2359 print('Issue %s has been submitted.' % self.GetIssueURL())
2360 return 0
2361
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002362 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2363 directory):
2364 assert not reject
2365 assert not nocommit
2366 assert not directory
2367 assert parsed_issue_arg.valid
2368
2369 self._changelist.issue = parsed_issue_arg.issue
2370
2371 if parsed_issue_arg.hostname:
2372 self._gerrit_host = parsed_issue_arg.hostname
2373 self._gerrit_server = 'https://%s' % self._gerrit_host
2374
2375 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2376
2377 if not parsed_issue_arg.patchset:
2378 # Use current revision by default.
2379 revision_info = detail['revisions'][detail['current_revision']]
2380 patchset = int(revision_info['_number'])
2381 else:
2382 patchset = parsed_issue_arg.patchset
2383 for revision_info in detail['revisions'].itervalues():
2384 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2385 break
2386 else:
2387 DieWithError('Couldn\'t find patchset %i in issue %i' %
2388 (parsed_issue_arg.patchset, self.GetIssue()))
2389
2390 fetch_info = revision_info['fetch']['http']
2391 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2392 RunGit(['cherry-pick', 'FETCH_HEAD'])
2393 self.SetIssue(self.GetIssue())
2394 self.SetPatchset(patchset)
2395 print('Committed patch for issue %i pathset %i locally' %
2396 (self.GetIssue(), self.GetPatchset()))
2397 return 0
2398
2399 @staticmethod
2400 def ParseIssueURL(parsed_url):
2401 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2402 return None
2403 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2404 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2405 # Short urls like https://domain/<issue_number> can be used, but don't allow
2406 # specifying the patchset (you'd 404), but we allow that here.
2407 if parsed_url.path == '/':
2408 part = parsed_url.fragment
2409 else:
2410 part = parsed_url.path
2411 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2412 if match:
2413 return _ParsedIssueNumberArgument(
2414 issue=int(match.group(2)),
2415 patchset=int(match.group(4)) if match.group(4) else None,
2416 hostname=parsed_url.netloc)
2417 return None
2418
tandrii16e0b4e2016-06-07 10:34:28 -07002419 def _GerritCommitMsgHookCheck(self, offer_removal):
2420 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2421 if not os.path.exists(hook):
2422 return
2423 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2424 # custom developer made one.
2425 data = gclient_utils.FileRead(hook)
2426 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2427 return
2428 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002429 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002430 'and may interfere with it in subtle ways.\n'
2431 'We recommend you remove the commit-msg hook.')
2432 if offer_removal:
2433 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2434 if reply.lower().startswith('y'):
2435 gclient_utils.rm_file_or_tree(hook)
2436 print('Gerrit commit-msg hook removed.')
2437 else:
2438 print('OK, will keep Gerrit commit-msg hook in place.')
2439
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002440 def CMDUploadChange(self, options, args, change):
2441 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002442 if options.squash and options.no_squash:
2443 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002444
2445 if not options.squash and not options.no_squash:
2446 # Load default for user, repo, squash=true, in this order.
2447 options.squash = settings.GetSquashGerritUploads()
2448 elif options.no_squash:
2449 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002450
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002451 # We assume the remote called "origin" is the one we want.
2452 # It is probably not worthwhile to support different workflows.
2453 gerrit_remote = 'origin'
2454
2455 remote, remote_branch = self.GetRemoteBranch()
2456 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2457 pending_prefix='')
2458
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002459 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002460 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002461 if self.GetIssue():
2462 # Try to get the message from a previous upload.
2463 message = self.GetDescription()
2464 if not message:
2465 DieWithError(
2466 'failed to fetch description from current Gerrit issue %d\n'
2467 '%s' % (self.GetIssue(), self.GetIssueURL()))
2468 change_id = self._GetChangeDetail()['change_id']
2469 while True:
2470 footer_change_ids = git_footers.get_footer_change_id(message)
2471 if footer_change_ids == [change_id]:
2472 break
2473 if not footer_change_ids:
2474 message = git_footers.add_footer_change_id(message, change_id)
2475 print('WARNING: appended missing Change-Id to issue description')
2476 continue
2477 # There is already a valid footer but with different or several ids.
2478 # Doing this automatically is non-trivial as we don't want to lose
2479 # existing other footers, yet we want to append just 1 desired
2480 # Change-Id. Thus, just create a new footer, but let user verify the
2481 # new description.
2482 message = '%s\n\nChange-Id: %s' % (message, change_id)
2483 print(
2484 'WARNING: issue %s has Change-Id footer(s):\n'
2485 ' %s\n'
2486 'but issue has Change-Id %s, according to Gerrit.\n'
2487 'Please, check the proposed correction to the description, '
2488 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2489 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2490 change_id))
2491 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2492 if not options.force:
2493 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002494 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002495 message = change_desc.description
2496 if not message:
2497 DieWithError("Description is empty. Aborting...")
2498 # Continue the while loop.
2499 # Sanity check of this code - we should end up with proper message
2500 # footer.
2501 assert [change_id] == git_footers.get_footer_change_id(message)
2502 change_desc = ChangeDescription(message)
2503 else:
2504 change_desc = ChangeDescription(
2505 options.message or CreateDescriptionFromLog(args))
2506 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002507 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002508 if not change_desc.description:
2509 DieWithError("Description is empty. Aborting...")
2510 message = change_desc.description
2511 change_ids = git_footers.get_footer_change_id(message)
2512 if len(change_ids) > 1:
2513 DieWithError('too many Change-Id footers, at most 1 allowed.')
2514 if not change_ids:
2515 # Generate the Change-Id automatically.
2516 message = git_footers.add_footer_change_id(
2517 message, GenerateGerritChangeId(message))
2518 change_desc.set_description(message)
2519 change_ids = git_footers.get_footer_change_id(message)
2520 assert len(change_ids) == 1
2521 change_id = change_ids[0]
2522
2523 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2524 if remote is '.':
2525 # If our upstream branch is local, we base our squashed commit on its
2526 # squashed version.
2527 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2528 # Check the squashed hash of the parent.
2529 parent = RunGit(['config',
2530 'branch.%s.gerritsquashhash' % upstream_branch_name],
2531 error_ok=True).strip()
2532 # Verify that the upstream branch has been uploaded too, otherwise
2533 # Gerrit will create additional CLs when uploading.
2534 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2535 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002536 DieWithError(
2537 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002538 'Note: maybe you\'ve uploaded it with --no-squash. '
2539 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002540 ' git cl upload --squash\n' % upstream_branch_name)
2541 else:
2542 parent = self.GetCommonAncestorWithUpstream()
2543
2544 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2545 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2546 '-m', message]).strip()
2547 else:
2548 change_desc = ChangeDescription(
2549 options.message or CreateDescriptionFromLog(args))
2550 if not change_desc.description:
2551 DieWithError("Description is empty. Aborting...")
2552
2553 if not git_footers.get_footer_change_id(change_desc.description):
2554 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002555 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2556 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002557 ref_to_push = 'HEAD'
2558 parent = '%s/%s' % (gerrit_remote, branch)
2559 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2560
2561 assert change_desc
2562 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2563 ref_to_push)]).splitlines()
2564 if len(commits) > 1:
2565 print('WARNING: This will upload %d commits. Run the following command '
2566 'to see which commits will be uploaded: ' % len(commits))
2567 print('git log %s..%s' % (parent, ref_to_push))
2568 print('You can also use `git squash-branch` to squash these into a '
2569 'single commit.')
2570 ask_for_data('About to upload; enter to confirm.')
2571
2572 if options.reviewers or options.tbr_owners:
2573 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2574 change)
2575
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002576 # Extra options that can be specified at push time. Doc:
2577 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2578 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002579 if change_desc.get_reviewers(tbr_only=True):
2580 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2581 refspec_opts.append('l=Code-Review+1')
2582
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002583 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002584 if not re.match(r'^[\w ]+$', options.title):
2585 options.title = re.sub(r'[^\w ]', '', options.title)
2586 print('WARNING: Patchset title may only contain alphanumeric chars '
2587 'and spaces. Cleaned up title:\n%s' % options.title)
2588 if not options.force:
2589 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002590 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2591 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002592 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2593
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002594 if options.send_mail:
2595 if not change_desc.get_reviewers():
2596 DieWithError('Must specify reviewers to send email.')
2597 refspec_opts.append('notify=ALL')
2598 else:
2599 refspec_opts.append('notify=NONE')
2600
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002601 cc = self.GetCCList().split(',')
2602 if options.cc:
2603 cc.extend(options.cc)
2604 cc = filter(None, cc)
2605 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002606 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002607
tandrii99a72f22016-08-17 14:33:24 -07002608 reviewers = change_desc.get_reviewers()
2609 if reviewers:
2610 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002611
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002612 refspec_suffix = ''
2613 if refspec_opts:
2614 refspec_suffix = '%' + ','.join(refspec_opts)
2615 assert ' ' not in refspec_suffix, (
2616 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002617 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002618
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002619 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002620 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002621 print_stdout=True,
2622 # Flush after every line: useful for seeing progress when running as
2623 # recipe.
2624 filter_fn=lambda _: sys.stdout.flush())
2625
2626 if options.squash:
2627 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2628 change_numbers = [m.group(1)
2629 for m in map(regex.match, push_stdout.splitlines())
2630 if m]
2631 if len(change_numbers) != 1:
2632 DieWithError(
2633 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2634 'Change-Id: %s') % (len(change_numbers), change_id))
2635 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002636 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002637 return 0
2638
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002639 def _AddChangeIdToCommitMessage(self, options, args):
2640 """Re-commits using the current message, assumes the commit hook is in
2641 place.
2642 """
2643 log_desc = options.message or CreateDescriptionFromLog(args)
2644 git_command = ['commit', '--amend', '-m', log_desc]
2645 RunGit(git_command)
2646 new_log_desc = CreateDescriptionFromLog(args)
2647 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002648 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002649 return new_log_desc
2650 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002651 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002652
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002653 def SetCQState(self, new_state):
2654 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002655 vote_map = {
2656 _CQState.NONE: 0,
2657 _CQState.DRY_RUN: 1,
2658 _CQState.COMMIT : 2,
2659 }
2660 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2661 labels={'Commit-Queue': vote_map[new_state]})
2662
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002663
2664_CODEREVIEW_IMPLEMENTATIONS = {
2665 'rietveld': _RietveldChangelistImpl,
2666 'gerrit': _GerritChangelistImpl,
2667}
2668
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002669
iannuccie53c9352016-08-17 14:40:40 -07002670def _add_codereview_issue_select_options(parser, extra=""):
2671 _add_codereview_select_options(parser)
2672
2673 text = ('Operate on this issue number instead of the current branch\'s '
2674 'implicit issue.')
2675 if extra:
2676 text += ' '+extra
2677 parser.add_option('-i', '--issue', type=int, help=text)
2678
2679
2680def _process_codereview_issue_select_options(parser, options):
2681 _process_codereview_select_options(parser, options)
2682 if options.issue is not None and not options.forced_codereview:
2683 parser.error('--issue must be specified with either --rietveld or --gerrit')
2684
2685
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002686def _add_codereview_select_options(parser):
2687 """Appends --gerrit and --rietveld options to force specific codereview."""
2688 parser.codereview_group = optparse.OptionGroup(
2689 parser, 'EXPERIMENTAL! Codereview override options')
2690 parser.add_option_group(parser.codereview_group)
2691 parser.codereview_group.add_option(
2692 '--gerrit', action='store_true',
2693 help='Force the use of Gerrit for codereview')
2694 parser.codereview_group.add_option(
2695 '--rietveld', action='store_true',
2696 help='Force the use of Rietveld for codereview')
2697
2698
2699def _process_codereview_select_options(parser, options):
2700 if options.gerrit and options.rietveld:
2701 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2702 options.forced_codereview = None
2703 if options.gerrit:
2704 options.forced_codereview = 'gerrit'
2705 elif options.rietveld:
2706 options.forced_codereview = 'rietveld'
2707
2708
tandriif9aefb72016-07-01 09:06:51 -07002709def _get_bug_line_values(default_project, bugs):
2710 """Given default_project and comma separated list of bugs, yields bug line
2711 values.
2712
2713 Each bug can be either:
2714 * a number, which is combined with default_project
2715 * string, which is left as is.
2716
2717 This function may produce more than one line, because bugdroid expects one
2718 project per line.
2719
2720 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2721 ['v8:123', 'chromium:789']
2722 """
2723 default_bugs = []
2724 others = []
2725 for bug in bugs.split(','):
2726 bug = bug.strip()
2727 if bug:
2728 try:
2729 default_bugs.append(int(bug))
2730 except ValueError:
2731 others.append(bug)
2732
2733 if default_bugs:
2734 default_bugs = ','.join(map(str, default_bugs))
2735 if default_project:
2736 yield '%s:%s' % (default_project, default_bugs)
2737 else:
2738 yield default_bugs
2739 for other in sorted(others):
2740 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2741 yield other
2742
2743
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002744class ChangeDescription(object):
2745 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002746 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002747 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002748
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002749 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002750 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002751
agable@chromium.org42c20792013-09-12 17:34:49 +00002752 @property # www.logilab.org/ticket/89786
2753 def description(self): # pylint: disable=E0202
2754 return '\n'.join(self._description_lines)
2755
2756 def set_description(self, desc):
2757 if isinstance(desc, basestring):
2758 lines = desc.splitlines()
2759 else:
2760 lines = [line.rstrip() for line in desc]
2761 while lines and not lines[0]:
2762 lines.pop(0)
2763 while lines and not lines[-1]:
2764 lines.pop(-1)
2765 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002766
piman@chromium.org336f9122014-09-04 02:16:55 +00002767 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002768 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002769 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002770 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002771 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002772 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002773
agable@chromium.org42c20792013-09-12 17:34:49 +00002774 # Get the set of R= and TBR= lines and remove them from the desciption.
2775 regexp = re.compile(self.R_LINE)
2776 matches = [regexp.match(line) for line in self._description_lines]
2777 new_desc = [l for i, l in enumerate(self._description_lines)
2778 if not matches[i]]
2779 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002780
agable@chromium.org42c20792013-09-12 17:34:49 +00002781 # Construct new unified R= and TBR= lines.
2782 r_names = []
2783 tbr_names = []
2784 for match in matches:
2785 if not match:
2786 continue
2787 people = cleanup_list([match.group(2).strip()])
2788 if match.group(1) == 'TBR':
2789 tbr_names.extend(people)
2790 else:
2791 r_names.extend(people)
2792 for name in r_names:
2793 if name not in reviewers:
2794 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002795 if add_owners_tbr:
2796 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002797 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002798 all_reviewers = set(tbr_names + reviewers)
2799 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2800 all_reviewers)
2801 tbr_names.extend(owners_db.reviewers_for(missing_files,
2802 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002803 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2804 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2805
2806 # Put the new lines in the description where the old first R= line was.
2807 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2808 if 0 <= line_loc < len(self._description_lines):
2809 if new_tbr_line:
2810 self._description_lines.insert(line_loc, new_tbr_line)
2811 if new_r_line:
2812 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002813 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002814 if new_r_line:
2815 self.append_footer(new_r_line)
2816 if new_tbr_line:
2817 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002818
tandriif9aefb72016-07-01 09:06:51 -07002819 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002820 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002821 self.set_description([
2822 '# Enter a description of the change.',
2823 '# This will be displayed on the codereview site.',
2824 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002825 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002826 '--------------------',
2827 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002828
agable@chromium.org42c20792013-09-12 17:34:49 +00002829 regexp = re.compile(self.BUG_LINE)
2830 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002831 prefix = settings.GetBugPrefix()
2832 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2833 for value in values:
2834 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2835 self.append_footer('BUG=%s' % value)
2836
agable@chromium.org42c20792013-09-12 17:34:49 +00002837 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002838 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002839 if not content:
2840 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002841 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002842
2843 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002844 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2845 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002846 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002847 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002848
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002849 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002850 """Adds a footer line to the description.
2851
2852 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2853 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2854 that Gerrit footers are always at the end.
2855 """
2856 parsed_footer_line = git_footers.parse_footer(line)
2857 if parsed_footer_line:
2858 # Line is a gerrit footer in the form: Footer-Key: any value.
2859 # Thus, must be appended observing Gerrit footer rules.
2860 self.set_description(
2861 git_footers.add_footer(self.description,
2862 key=parsed_footer_line[0],
2863 value=parsed_footer_line[1]))
2864 return
2865
2866 if not self._description_lines:
2867 self._description_lines.append(line)
2868 return
2869
2870 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2871 if gerrit_footers:
2872 # git_footers.split_footers ensures that there is an empty line before
2873 # actual (gerrit) footers, if any. We have to keep it that way.
2874 assert top_lines and top_lines[-1] == ''
2875 top_lines, separator = top_lines[:-1], top_lines[-1:]
2876 else:
2877 separator = [] # No need for separator if there are no gerrit_footers.
2878
2879 prev_line = top_lines[-1] if top_lines else ''
2880 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2881 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2882 top_lines.append('')
2883 top_lines.append(line)
2884 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002885
tandrii99a72f22016-08-17 14:33:24 -07002886 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002887 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002888 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002889 reviewers = [match.group(2).strip()
2890 for match in matches
2891 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002892 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002893
2894
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002895def get_approving_reviewers(props):
2896 """Retrieves the reviewers that approved a CL from the issue properties with
2897 messages.
2898
2899 Note that the list may contain reviewers that are not committer, thus are not
2900 considered by the CQ.
2901 """
2902 return sorted(
2903 set(
2904 message['sender']
2905 for message in props['messages']
2906 if message['approval'] and message['sender'] in props['reviewers']
2907 )
2908 )
2909
2910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911def FindCodereviewSettingsFile(filename='codereview.settings'):
2912 """Finds the given file starting in the cwd and going up.
2913
2914 Only looks up to the top of the repository unless an
2915 'inherit-review-settings-ok' file exists in the root of the repository.
2916 """
2917 inherit_ok_file = 'inherit-review-settings-ok'
2918 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002919 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002920 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2921 root = '/'
2922 while True:
2923 if filename in os.listdir(cwd):
2924 if os.path.isfile(os.path.join(cwd, filename)):
2925 return open(os.path.join(cwd, filename))
2926 if cwd == root:
2927 break
2928 cwd = os.path.dirname(cwd)
2929
2930
2931def LoadCodereviewSettingsFromFile(fileobj):
2932 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002933 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935 def SetProperty(name, setting, unset_error_ok=False):
2936 fullname = 'rietveld.' + name
2937 if setting in keyvals:
2938 RunGit(['config', fullname, keyvals[setting]])
2939 else:
2940 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2941
2942 SetProperty('server', 'CODE_REVIEW_SERVER')
2943 # Only server setting is required. Other settings can be absent.
2944 # In that case, we ignore errors raised during option deletion attempt.
2945 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002946 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2948 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002949 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002950 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002951 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2952 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002953 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002954 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002955 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002956 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2957 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002958
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002959 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002960 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002961
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002962 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002963 RunGit(['config', 'gerrit.squash-uploads',
2964 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002965
tandrii@chromium.org28253532016-04-14 13:46:56 +00002966 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002967 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002968 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2971 #should be of the form
2972 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2973 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2974 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2975 keyvals['ORIGIN_URL_CONFIG']])
2976
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002977
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002978def urlretrieve(source, destination):
2979 """urllib is broken for SSL connections via a proxy therefore we
2980 can't use urllib.urlretrieve()."""
2981 with open(destination, 'w') as f:
2982 f.write(urllib2.urlopen(source).read())
2983
2984
ukai@chromium.org712d6102013-11-27 00:52:58 +00002985def hasSheBang(fname):
2986 """Checks fname is a #! script."""
2987 with open(fname) as f:
2988 return f.read(2).startswith('#!')
2989
2990
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002991# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2992def DownloadHooks(*args, **kwargs):
2993 pass
2994
2995
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002996def DownloadGerritHook(force):
2997 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002998
2999 Args:
3000 force: True to update hooks. False to install hooks if not present.
3001 """
3002 if not settings.GetIsGerrit():
3003 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003004 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003005 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3006 if not os.access(dst, os.X_OK):
3007 if os.path.exists(dst):
3008 if not force:
3009 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003010 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003011 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003012 if not hasSheBang(dst):
3013 DieWithError('Not a script: %s\n'
3014 'You need to download from\n%s\n'
3015 'into .git/hooks/commit-msg and '
3016 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003017 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3018 except Exception:
3019 if os.path.exists(dst):
3020 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003021 DieWithError('\nFailed to download hooks.\n'
3022 'You need to download from\n%s\n'
3023 'into .git/hooks/commit-msg and '
3024 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003025
3026
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003027
3028def GetRietveldCodereviewSettingsInteractively():
3029 """Prompt the user for settings."""
3030 server = settings.GetDefaultServerUrl(error_ok=True)
3031 prompt = 'Rietveld server (host[:port])'
3032 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3033 newserver = ask_for_data(prompt + ':')
3034 if not server and not newserver:
3035 newserver = DEFAULT_SERVER
3036 if newserver:
3037 newserver = gclient_utils.UpgradeToHttps(newserver)
3038 if newserver != server:
3039 RunGit(['config', 'rietveld.server', newserver])
3040
3041 def SetProperty(initial, caption, name, is_url):
3042 prompt = caption
3043 if initial:
3044 prompt += ' ("x" to clear) [%s]' % initial
3045 new_val = ask_for_data(prompt + ':')
3046 if new_val == 'x':
3047 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3048 elif new_val:
3049 if is_url:
3050 new_val = gclient_utils.UpgradeToHttps(new_val)
3051 if new_val != initial:
3052 RunGit(['config', 'rietveld.' + name, new_val])
3053
3054 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3055 SetProperty(settings.GetDefaultPrivateFlag(),
3056 'Private flag (rietveld only)', 'private', False)
3057 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3058 'tree-status-url', False)
3059 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3060 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3061 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3062 'run-post-upload-hook', False)
3063
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003064@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003066 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003068 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003069 'For Gerrit, see http://crbug.com/603116.')
3070 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003071 parser.add_option('--activate-update', action='store_true',
3072 help='activate auto-updating [rietveld] section in '
3073 '.git/config')
3074 parser.add_option('--deactivate-update', action='store_true',
3075 help='deactivate auto-updating [rietveld] section in '
3076 '.git/config')
3077 options, args = parser.parse_args(args)
3078
3079 if options.deactivate_update:
3080 RunGit(['config', 'rietveld.autoupdate', 'false'])
3081 return
3082
3083 if options.activate_update:
3084 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3085 return
3086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003087 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003088 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089 return 0
3090
3091 url = args[0]
3092 if not url.endswith('codereview.settings'):
3093 url = os.path.join(url, 'codereview.settings')
3094
3095 # Load code review settings and download hooks (if available).
3096 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3097 return 0
3098
3099
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003100def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003101 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003102 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3103 branch = ShortBranchName(branchref)
3104 _, args = parser.parse_args(args)
3105 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003106 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003107 return RunGit(['config', 'branch.%s.base-url' % branch],
3108 error_ok=False).strip()
3109 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003110 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003111 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3112 error_ok=False).strip()
3113
3114
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003115def color_for_status(status):
3116 """Maps a Changelist status to color, for CMDstatus and other tools."""
3117 return {
3118 'unsent': Fore.RED,
3119 'waiting': Fore.BLUE,
3120 'reply': Fore.YELLOW,
3121 'lgtm': Fore.GREEN,
3122 'commit': Fore.MAGENTA,
3123 'closed': Fore.CYAN,
3124 'error': Fore.WHITE,
3125 }.get(status, Fore.WHITE)
3126
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003127
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003128def get_cl_statuses(changes, fine_grained, max_processes=None):
3129 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003130
3131 If fine_grained is true, this will fetch CL statuses from the server.
3132 Otherwise, simply indicate if there's a matching url for the given branches.
3133
3134 If max_processes is specified, it is used as the maximum number of processes
3135 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3136 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003137
3138 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003139 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003140 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003141 upload.verbosity = 0
3142
3143 if fine_grained:
3144 # Process one branch synchronously to work through authentication, then
3145 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003146 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003147 def fetch(cl):
3148 try:
3149 return (cl, cl.GetStatus())
3150 except:
3151 # See http://crbug.com/629863.
3152 logging.exception('failed to fetch status for %s:', cl)
3153 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003154 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003155
tandriiea9514a2016-08-17 12:32:37 -07003156 changes_to_fetch = changes[1:]
3157 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003158 # Exit early if there was only one branch to fetch.
3159 return
3160
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003161 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003162 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003163 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003164 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003165
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003166 fetched_cls = set()
3167 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003168 while True:
3169 try:
3170 row = it.next(timeout=5)
3171 except multiprocessing.TimeoutError:
3172 break
3173
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003174 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003175 yield row
3176
3177 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003178 for cl in set(changes_to_fetch) - fetched_cls:
3179 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003180
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003181 else:
3182 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003183 for cl in changes:
3184 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003185
rmistry@google.com2dd99862015-06-22 12:22:18 +00003186
3187def upload_branch_deps(cl, args):
3188 """Uploads CLs of local branches that are dependents of the current branch.
3189
3190 If the local branch dependency tree looks like:
3191 test1 -> test2.1 -> test3.1
3192 -> test3.2
3193 -> test2.2 -> test3.3
3194
3195 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3196 run on the dependent branches in this order:
3197 test2.1, test3.1, test3.2, test2.2, test3.3
3198
3199 Note: This function does not rebase your local dependent branches. Use it when
3200 you make a change to the parent branch that will not conflict with its
3201 dependent branches, and you would like their dependencies updated in
3202 Rietveld.
3203 """
3204 if git_common.is_dirty_git_tree('upload-branch-deps'):
3205 return 1
3206
3207 root_branch = cl.GetBranch()
3208 if root_branch is None:
3209 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3210 'Get on a branch!')
3211 if not cl.GetIssue() or not cl.GetPatchset():
3212 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3213 'patchset dependencies without an uploaded CL.')
3214
3215 branches = RunGit(['for-each-ref',
3216 '--format=%(refname:short) %(upstream:short)',
3217 'refs/heads'])
3218 if not branches:
3219 print('No local branches found.')
3220 return 0
3221
3222 # Create a dictionary of all local branches to the branches that are dependent
3223 # on it.
3224 tracked_to_dependents = collections.defaultdict(list)
3225 for b in branches.splitlines():
3226 tokens = b.split()
3227 if len(tokens) == 2:
3228 branch_name, tracked = tokens
3229 tracked_to_dependents[tracked].append(branch_name)
3230
vapiera7fbd5a2016-06-16 09:17:49 -07003231 print()
3232 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003233 dependents = []
3234 def traverse_dependents_preorder(branch, padding=''):
3235 dependents_to_process = tracked_to_dependents.get(branch, [])
3236 padding += ' '
3237 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003238 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003239 dependents.append(dependent)
3240 traverse_dependents_preorder(dependent, padding)
3241 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003242 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003243
3244 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003245 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003246 return 0
3247
vapiera7fbd5a2016-06-16 09:17:49 -07003248 print('This command will checkout all dependent branches and run '
3249 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003250 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3251
andybons@chromium.org962f9462016-02-03 20:00:42 +00003252 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003253 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003254 args.extend(['-t', 'Updated patchset dependency'])
3255
rmistry@google.com2dd99862015-06-22 12:22:18 +00003256 # Record all dependents that failed to upload.
3257 failures = {}
3258 # Go through all dependents, checkout the branch and upload.
3259 try:
3260 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003261 print()
3262 print('--------------------------------------')
3263 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003264 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003265 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003266 try:
3267 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003268 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003269 failures[dependent_branch] = 1
3270 except: # pylint: disable=W0702
3271 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003272 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003273 finally:
3274 # Swap back to the original root branch.
3275 RunGit(['checkout', '-q', root_branch])
3276
vapiera7fbd5a2016-06-16 09:17:49 -07003277 print()
3278 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003279 for dependent_branch in dependents:
3280 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003281 print(' %s : %s' % (dependent_branch, upload_status))
3282 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003283
3284 return 0
3285
3286
kmarshall3bff56b2016-06-06 18:31:47 -07003287def CMDarchive(parser, args):
3288 """Archives and deletes branches associated with closed changelists."""
3289 parser.add_option(
3290 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003291 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003292 parser.add_option(
3293 '-f', '--force', action='store_true',
3294 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003295 parser.add_option(
3296 '-d', '--dry-run', action='store_true',
3297 help='Skip the branch tagging and removal steps.')
3298 parser.add_option(
3299 '-t', '--notags', action='store_true',
3300 help='Do not tag archived branches. '
3301 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003302
3303 auth.add_auth_options(parser)
3304 options, args = parser.parse_args(args)
3305 if args:
3306 parser.error('Unsupported args: %s' % ' '.join(args))
3307 auth_config = auth.extract_auth_config_from_options(options)
3308
3309 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3310 if not branches:
3311 return 0
3312
vapiera7fbd5a2016-06-16 09:17:49 -07003313 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003314 changes = [Changelist(branchref=b, auth_config=auth_config)
3315 for b in branches.splitlines()]
3316 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3317 statuses = get_cl_statuses(changes,
3318 fine_grained=True,
3319 max_processes=options.maxjobs)
3320 proposal = [(cl.GetBranch(),
3321 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3322 for cl, status in statuses
3323 if status == 'closed']
3324 proposal.sort()
3325
3326 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003327 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003328 return 0
3329
3330 current_branch = GetCurrentBranch()
3331
vapiera7fbd5a2016-06-16 09:17:49 -07003332 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003333 if options.notags:
3334 for next_item in proposal:
3335 print(' ' + next_item[0])
3336 else:
3337 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3338 for next_item in proposal:
3339 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003340
kmarshall9249e012016-08-23 12:02:16 -07003341 # Quit now on precondition failure or if instructed by the user, either
3342 # via an interactive prompt or by command line flags.
3343 if options.dry_run:
3344 print('\nNo changes were made (dry run).\n')
3345 return 0
3346 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003347 print('You are currently on a branch \'%s\' which is associated with a '
3348 'closed codereview issue, so archive cannot proceed. Please '
3349 'checkout another branch and run this command again.' %
3350 current_branch)
3351 return 1
kmarshall9249e012016-08-23 12:02:16 -07003352 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003353 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3354 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003355 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003356 return 1
3357
3358 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003359 if not options.notags:
3360 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003361 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003362
vapiera7fbd5a2016-06-16 09:17:49 -07003363 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003364
3365 return 0
3366
3367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003368def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003369 """Show status of changelists.
3370
3371 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003372 - Red not sent for review or broken
3373 - Blue waiting for review
3374 - Yellow waiting for you to reply to review
3375 - Green LGTM'ed
3376 - Magenta in the commit queue
3377 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003378
3379 Also see 'git cl comments'.
3380 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003382 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003383 parser.add_option('-f', '--fast', action='store_true',
3384 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003385 parser.add_option(
3386 '-j', '--maxjobs', action='store', type=int,
3387 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003388
3389 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003390 _add_codereview_issue_select_options(
3391 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003392 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003393 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003394 if args:
3395 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003396 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003397
iannuccie53c9352016-08-17 14:40:40 -07003398 if options.issue is not None and not options.field:
3399 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003402 cl = Changelist(auth_config=auth_config, issue=options.issue,
3403 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003404 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003405 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003406 elif options.field == 'id':
3407 issueid = cl.GetIssue()
3408 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003409 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003410 elif options.field == 'patch':
3411 patchset = cl.GetPatchset()
3412 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003413 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003414 elif options.field == 'status':
3415 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003416 elif options.field == 'url':
3417 url = cl.GetIssueURL()
3418 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003420 return 0
3421
3422 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3423 if not branches:
3424 print('No local branch found.')
3425 return 0
3426
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003427 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003428 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003429 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003430 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003431 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003432 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003433 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003434
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003435 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003436 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3437 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3438 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003439 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003440 c, status = output.next()
3441 branch_statuses[c.GetBranch()] = status
3442 status = branch_statuses.pop(branch)
3443 url = cl.GetIssueURL()
3444 if url and (not status or status == 'error'):
3445 # The issue probably doesn't exist anymore.
3446 url += ' (broken)'
3447
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003448 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003449 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003450 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003451 color = ''
3452 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003453 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003454 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003455 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003456 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003457
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003458 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003459 print()
3460 print('Current branch:',)
3461 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003462 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003463 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003464 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003465 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003466 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003467 print('Issue description:')
3468 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003469 return 0
3470
3471
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003472def colorize_CMDstatus_doc():
3473 """To be called once in main() to add colors to git cl status help."""
3474 colors = [i for i in dir(Fore) if i[0].isupper()]
3475
3476 def colorize_line(line):
3477 for color in colors:
3478 if color in line.upper():
3479 # Extract whitespaces first and the leading '-'.
3480 indent = len(line) - len(line.lstrip(' ')) + 1
3481 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3482 return line
3483
3484 lines = CMDstatus.__doc__.splitlines()
3485 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3486
3487
phajdan.jre328cf92016-08-22 04:12:17 -07003488def write_json(path, contents):
3489 with open(path, 'w') as f:
3490 json.dump(contents, f)
3491
3492
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003493@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003494def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003495 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496
3497 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003498 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003499 parser.add_option('-r', '--reverse', action='store_true',
3500 help='Lookup the branch(es) for the specified issues. If '
3501 'no issues are specified, all branches with mapped '
3502 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003503 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003504 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003505 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003506 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003507
dnj@chromium.org406c4402015-03-03 17:22:28 +00003508 if options.reverse:
3509 branches = RunGit(['for-each-ref', 'refs/heads',
3510 '--format=%(refname:short)']).splitlines()
3511
3512 # Reverse issue lookup.
3513 issue_branch_map = {}
3514 for branch in branches:
3515 cl = Changelist(branchref=branch)
3516 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3517 if not args:
3518 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003519 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003520 for issue in args:
3521 if not issue:
3522 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003523 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003524 print('Branch for issue number %s: %s' % (
3525 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003526 if options.json:
3527 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003528 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003529 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003530 if len(args) > 0:
3531 try:
3532 issue = int(args[0])
3533 except ValueError:
3534 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003535 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003536 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003537 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003538 if options.json:
3539 write_json(options.json, {
3540 'issue': cl.GetIssue(),
3541 'issue_url': cl.GetIssueURL(),
3542 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543 return 0
3544
3545
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003546def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003547 """Shows or posts review comments for any changelist."""
3548 parser.add_option('-a', '--add-comment', dest='comment',
3549 help='comment to add to an issue')
3550 parser.add_option('-i', dest='issue',
3551 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003552 parser.add_option('-j', '--json-file',
3553 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003554 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003555 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003556 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003557
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003558 issue = None
3559 if options.issue:
3560 try:
3561 issue = int(options.issue)
3562 except ValueError:
3563 DieWithError('A review issue id is expected to be a number')
3564
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003565 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003566
3567 if options.comment:
3568 cl.AddComment(options.comment)
3569 return 0
3570
3571 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003572 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003573 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003574 summary.append({
3575 'date': message['date'],
3576 'lgtm': False,
3577 'message': message['text'],
3578 'not_lgtm': False,
3579 'sender': message['sender'],
3580 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003581 if message['disapproval']:
3582 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003583 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003584 elif message['approval']:
3585 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003586 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003587 elif message['sender'] == data['owner_email']:
3588 color = Fore.MAGENTA
3589 else:
3590 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003592 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003593 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003594 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003596 if options.json_file:
3597 with open(options.json_file, 'wb') as f:
3598 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003599 return 0
3600
3601
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003602@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003603def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003604 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003605 parser.add_option('-d', '--display', action='store_true',
3606 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003607 parser.add_option('-n', '--new-description',
3608 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003609
3610 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003612 options, args = parser.parse_args(args)
3613 _process_codereview_select_options(parser, options)
3614
3615 target_issue = None
3616 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003617 target_issue = ParseIssueNumberArgument(args[0])
3618 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003619 parser.print_help()
3620 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003621
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003622 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003623
martiniss6eda05f2016-06-30 10:18:35 -07003624 kwargs = {
3625 'auth_config': auth_config,
3626 'codereview': options.forced_codereview,
3627 }
3628 if target_issue:
3629 kwargs['issue'] = target_issue.issue
3630 if options.forced_codereview == 'rietveld':
3631 kwargs['rietveld_server'] = target_issue.hostname
3632
3633 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003634
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003635 if not cl.GetIssue():
3636 DieWithError('This branch has no associated changelist.')
3637 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003638
smut@google.com34fb6b12015-07-13 20:03:26 +00003639 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003640 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003641 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003642
3643 if options.new_description:
3644 text = options.new_description
3645 if text == '-':
3646 text = '\n'.join(l.rstrip() for l in sys.stdin)
3647
3648 description.set_description(text)
3649 else:
3650 description.prompt()
3651
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003652 if cl.GetDescription() != description.description:
3653 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003654 return 0
3655
3656
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657def CreateDescriptionFromLog(args):
3658 """Pulls out the commit log to use as a base for the CL description."""
3659 log_args = []
3660 if len(args) == 1 and not args[0].endswith('.'):
3661 log_args = [args[0] + '..']
3662 elif len(args) == 1 and args[0].endswith('...'):
3663 log_args = [args[0][:-1]]
3664 elif len(args) == 2:
3665 log_args = [args[0] + '..' + args[1]]
3666 else:
3667 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003668 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669
3670
thestig@chromium.org44202a22014-03-11 19:22:18 +00003671def CMDlint(parser, args):
3672 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003673 parser.add_option('--filter', action='append', metavar='-x,+y',
3674 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003675 auth.add_auth_options(parser)
3676 options, args = parser.parse_args(args)
3677 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003678
3679 # Access to a protected member _XX of a client class
3680 # pylint: disable=W0212
3681 try:
3682 import cpplint
3683 import cpplint_chromium
3684 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003685 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003686 return 1
3687
3688 # Change the current working directory before calling lint so that it
3689 # shows the correct base.
3690 previous_cwd = os.getcwd()
3691 os.chdir(settings.GetRoot())
3692 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003693 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003694 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3695 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003696 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003697 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003698 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003699
3700 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003701 command = args + files
3702 if options.filter:
3703 command = ['--filter=' + ','.join(options.filter)] + command
3704 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003705
3706 white_regex = re.compile(settings.GetLintRegex())
3707 black_regex = re.compile(settings.GetLintIgnoreRegex())
3708 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3709 for filename in filenames:
3710 if white_regex.match(filename):
3711 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003713 else:
3714 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3715 extra_check_functions)
3716 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003718 finally:
3719 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003720 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003721 if cpplint._cpplint_state.error_count != 0:
3722 return 1
3723 return 0
3724
3725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003726def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003727 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003728 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003730 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003731 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003732 auth.add_auth_options(parser)
3733 options, args = parser.parse_args(args)
3734 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735
sbc@chromium.org71437c02015-04-09 19:29:40 +00003736 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003737 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003738 return 1
3739
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003740 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003741 if args:
3742 base_branch = args[0]
3743 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003744 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003745 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003746
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003747 cl.RunHook(
3748 committing=not options.upload,
3749 may_prompt=False,
3750 verbose=options.verbose,
3751 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003752 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753
3754
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003755def GenerateGerritChangeId(message):
3756 """Returns Ixxxxxx...xxx change id.
3757
3758 Works the same way as
3759 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3760 but can be called on demand on all platforms.
3761
3762 The basic idea is to generate git hash of a state of the tree, original commit
3763 message, author/committer info and timestamps.
3764 """
3765 lines = []
3766 tree_hash = RunGitSilent(['write-tree'])
3767 lines.append('tree %s' % tree_hash.strip())
3768 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3769 if code == 0:
3770 lines.append('parent %s' % parent.strip())
3771 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3772 lines.append('author %s' % author.strip())
3773 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3774 lines.append('committer %s' % committer.strip())
3775 lines.append('')
3776 # Note: Gerrit's commit-hook actually cleans message of some lines and
3777 # whitespace. This code is not doing this, but it clearly won't decrease
3778 # entropy.
3779 lines.append(message)
3780 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3781 stdin='\n'.join(lines))
3782 return 'I%s' % change_hash.strip()
3783
3784
wittman@chromium.org455dc922015-01-26 20:15:50 +00003785def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3786 """Computes the remote branch ref to use for the CL.
3787
3788 Args:
3789 remote (str): The git remote for the CL.
3790 remote_branch (str): The git remote branch for the CL.
3791 target_branch (str): The target branch specified by the user.
3792 pending_prefix (str): The pending prefix from the settings.
3793 """
3794 if not (remote and remote_branch):
3795 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003796
wittman@chromium.org455dc922015-01-26 20:15:50 +00003797 if target_branch:
3798 # Cannonicalize branch references to the equivalent local full symbolic
3799 # refs, which are then translated into the remote full symbolic refs
3800 # below.
3801 if '/' not in target_branch:
3802 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3803 else:
3804 prefix_replacements = (
3805 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3806 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3807 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3808 )
3809 match = None
3810 for regex, replacement in prefix_replacements:
3811 match = re.search(regex, target_branch)
3812 if match:
3813 remote_branch = target_branch.replace(match.group(0), replacement)
3814 break
3815 if not match:
3816 # This is a branch path but not one we recognize; use as-is.
3817 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003818 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3819 # Handle the refs that need to land in different refs.
3820 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003821
wittman@chromium.org455dc922015-01-26 20:15:50 +00003822 # Create the true path to the remote branch.
3823 # Does the following translation:
3824 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3825 # * refs/remotes/origin/master -> refs/heads/master
3826 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3827 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3828 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3829 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3830 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3831 'refs/heads/')
3832 elif remote_branch.startswith('refs/remotes/branch-heads'):
3833 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3834 # If a pending prefix exists then replace refs/ with it.
3835 if pending_prefix:
3836 remote_branch = remote_branch.replace('refs/', pending_prefix)
3837 return remote_branch
3838
3839
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003840def cleanup_list(l):
3841 """Fixes a list so that comma separated items are put as individual items.
3842
3843 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3844 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3845 """
3846 items = sum((i.split(',') for i in l), [])
3847 stripped_items = (i.strip() for i in items)
3848 return sorted(filter(None, stripped_items))
3849
3850
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003851@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003852def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003853 """Uploads the current changelist to codereview.
3854
3855 Can skip dependency patchset uploads for a branch by running:
3856 git config branch.branch_name.skip-deps-uploads True
3857 To unset run:
3858 git config --unset branch.branch_name.skip-deps-uploads
3859 Can also set the above globally by using the --global flag.
3860 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003861 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3862 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003863 parser.add_option('--bypass-watchlists', action='store_true',
3864 dest='bypass_watchlists',
3865 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003866 parser.add_option('-f', action='store_true', dest='force',
3867 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003868 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003869 parser.add_option('-b', '--bug',
3870 help='pre-populate the bug number(s) for this issue. '
3871 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003872 parser.add_option('--message-file', dest='message_file',
3873 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003874 parser.add_option('-t', dest='title',
3875 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003876 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003877 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003878 help='reviewer email addresses')
3879 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003880 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003881 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003882 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003883 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003884 parser.add_option('--emulate_svn_auto_props',
3885 '--emulate-svn-auto-props',
3886 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003887 dest="emulate_svn_auto_props",
3888 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003889 parser.add_option('-c', '--use-commit-queue', action='store_true',
3890 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003891 parser.add_option('--private', action='store_true',
3892 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003893 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003894 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003895 metavar='TARGET',
3896 help='Apply CL to remote ref TARGET. ' +
3897 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003898 parser.add_option('--squash', action='store_true',
3899 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003900 parser.add_option('--no-squash', action='store_true',
3901 help='Don\'t squash multiple commits into one ' +
3902 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003903 parser.add_option('--email', default=None,
3904 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003905 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3906 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003907 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3908 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003909 help='Send the patchset to do a CQ dry run right after '
3910 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003911 parser.add_option('--dependencies', action='store_true',
3912 help='Uploads CLs of all the local branches that depend on '
3913 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003914
rmistry@google.com2dd99862015-06-22 12:22:18 +00003915 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003916 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003917 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003918 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003919 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003920 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003921 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003922
sbc@chromium.org71437c02015-04-09 19:29:40 +00003923 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003924 return 1
3925
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003926 options.reviewers = cleanup_list(options.reviewers)
3927 options.cc = cleanup_list(options.cc)
3928
tandriib80458a2016-06-23 12:20:07 -07003929 if options.message_file:
3930 if options.message:
3931 parser.error('only one of --message and --message-file allowed.')
3932 options.message = gclient_utils.FileRead(options.message_file)
3933 options.message_file = None
3934
tandrii4d0545a2016-07-06 03:56:49 -07003935 if options.cq_dry_run and options.use_commit_queue:
3936 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3937
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003938 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3939 settings.GetIsGerrit()
3940
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003941 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003942 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003943
3944
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003945def IsSubmoduleMergeCommit(ref):
3946 # When submodules are added to the repo, we expect there to be a single
3947 # non-git-svn merge commit at remote HEAD with a signature comment.
3948 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003949 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003950 return RunGit(cmd) != ''
3951
3952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003953def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003954 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003956 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3957 upstream and closes the issue automatically and atomically.
3958
3959 Otherwise (in case of Rietveld):
3960 Squashes branch into a single commit.
3961 Updates changelog with metadata (e.g. pointer to review).
3962 Pushes/dcommits the code upstream.
3963 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 """
3965 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3966 help='bypass upload presubmit hook')
3967 parser.add_option('-m', dest='message',
3968 help="override review description")
3969 parser.add_option('-f', action='store_true', dest='force',
3970 help="force yes to questions (don't prompt)")
3971 parser.add_option('-c', dest='contributor',
3972 help="external contributor for patch (appended to " +
3973 "description and used as author for git). Should be " +
3974 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003975 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003976 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003978 auth_config = auth.extract_auth_config_from_options(options)
3979
3980 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003982 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3983 if cl.IsGerrit():
3984 if options.message:
3985 # This could be implemented, but it requires sending a new patch to
3986 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3987 # Besides, Gerrit has the ability to change the commit message on submit
3988 # automatically, thus there is no need to support this option (so far?).
3989 parser.error('-m MESSAGE option is not supported for Gerrit.')
3990 if options.contributor:
3991 parser.error(
3992 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3993 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3994 'the contributor\'s "name <email>". If you can\'t upload such a '
3995 'commit for review, contact your repository admin and request'
3996 '"Forge-Author" permission.')
3997 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3998 options.verbose)
3999
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004000 current = cl.GetBranch()
4001 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4002 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004003 print()
4004 print('Attempting to push branch %r into another local branch!' % current)
4005 print()
4006 print('Either reparent this branch on top of origin/master:')
4007 print(' git reparent-branch --root')
4008 print()
4009 print('OR run `git rebase-update` if you think the parent branch is ')
4010 print('already committed.')
4011 print()
4012 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004013 return 1
4014
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004015 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016 # Default to merging against our best guess of the upstream branch.
4017 args = [cl.GetUpstreamBranch()]
4018
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004019 if options.contributor:
4020 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004021 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004022 return 1
4023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004024 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004025 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026
sbc@chromium.org71437c02015-04-09 19:29:40 +00004027 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004028 return 1
4029
4030 # This rev-list syntax means "show all commits not in my branch that
4031 # are in base_branch".
4032 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4033 base_branch]).splitlines()
4034 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004035 print('Base branch "%s" has %d commits '
4036 'not in this branch.' % (base_branch, len(upstream_commits)))
4037 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038 return 1
4039
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004040 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004041 svn_head = None
4042 if cmd == 'dcommit' or base_has_submodules:
4043 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4044 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004046 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004047 # If the base_head is a submodule merge commit, the first parent of the
4048 # base_head should be a git-svn commit, which is what we're interested in.
4049 base_svn_head = base_branch
4050 if base_has_submodules:
4051 base_svn_head += '^1'
4052
4053 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004054 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004055 print('This branch has %d additional commits not upstreamed yet.'
4056 % len(extra_commits.splitlines()))
4057 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4058 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 return 1
4060
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004061 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004062 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004063 author = None
4064 if options.contributor:
4065 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004066 hook_results = cl.RunHook(
4067 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004068 may_prompt=not options.force,
4069 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004070 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004071 if not hook_results.should_continue():
4072 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004074 # Check the tree status if the tree status URL is set.
4075 status = GetTreeStatus()
4076 if 'closed' == status:
4077 print('The tree is closed. Please wait for it to reopen. Use '
4078 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4079 return 1
4080 elif 'unknown' == status:
4081 print('Unable to determine tree status. Please verify manually and '
4082 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4083 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004085 change_desc = ChangeDescription(options.message)
4086 if not change_desc.description and cl.GetIssue():
4087 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004089 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004090 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004091 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004092 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004093 print('No description set.')
4094 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004095 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004096
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004097 # Keep a separate copy for the commit message, because the commit message
4098 # contains the link to the Rietveld issue, while the Rietveld message contains
4099 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004100 # Keep a separate copy for the commit message.
4101 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004102 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004103
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004104 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004105 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004106 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004107 # after it. Add a period on a new line to circumvent this. Also add a space
4108 # before the period to make sure that Gitiles continues to correctly resolve
4109 # the URL.
4110 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004111 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004112 commit_desc.append_footer('Patch from %s.' % options.contributor)
4113
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004114 print('Description:')
4115 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004117 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004119 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004120
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004121 # We want to squash all this branch's commits into one commit with the proper
4122 # description. We do this by doing a "reset --soft" to the base branch (which
4123 # keeps the working copy the same), then dcommitting that. If origin/master
4124 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4125 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004126 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004127 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4128 # Delete the branches if they exist.
4129 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4130 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4131 result = RunGitWithCode(showref_cmd)
4132 if result[0] == 0:
4133 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134
4135 # We might be in a directory that's present in this branch but not in the
4136 # trunk. Move up to the top of the tree so that git commands that expect a
4137 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004138 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139 if rel_base_path:
4140 os.chdir(rel_base_path)
4141
4142 # Stuff our change into the merge branch.
4143 # We wrap in a try...finally block so if anything goes wrong,
4144 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004145 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004146 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004147 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004148 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004149 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004150 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004151 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004152 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004153 RunGit(
4154 [
4155 'commit', '--author', options.contributor,
4156 '-m', commit_desc.description,
4157 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004158 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004159 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004160 if base_has_submodules:
4161 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4162 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4163 RunGit(['checkout', CHERRY_PICK_BRANCH])
4164 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004165 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004166 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004167 mirror = settings.GetGitMirror(remote)
4168 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004169 pending_prefix = settings.GetPendingRefPrefix()
4170 if not pending_prefix or branch.startswith(pending_prefix):
4171 # If not using refs/pending/heads/* at all, or target ref is already set
4172 # to pending, then push to the target ref directly.
4173 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004174 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004175 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004176 else:
4177 # Cherry-pick the change on top of pending ref and then push it.
4178 assert branch.startswith('refs/'), branch
4179 assert pending_prefix[-1] == '/', pending_prefix
4180 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004181 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004182 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004183 if retcode == 0:
4184 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185 else:
4186 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004187 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004188 'svn', 'dcommit',
4189 '-C%s' % options.similarity,
4190 '--no-rebase', '--rmdir',
4191 ]
4192 if settings.GetForceHttpsCommitUrl():
4193 # Allow forcing https commit URLs for some projects that don't allow
4194 # committing to http URLs (like Google Code).
4195 remote_url = cl.GetGitSvnRemoteUrl()
4196 if urlparse.urlparse(remote_url).scheme == 'http':
4197 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004198 cmd_args.append('--commit-url=%s' % remote_url)
4199 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004200 if 'Committed r' in output:
4201 revision = re.match(
4202 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4203 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 finally:
4205 # And then swap back to the original branch and clean up.
4206 RunGit(['checkout', '-q', cl.GetBranch()])
4207 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004208 if base_has_submodules:
4209 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004210
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004211 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004212 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004213 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004214
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004215 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004216 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004217 try:
4218 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4219 # We set pushed_to_pending to False, since it made it all the way to the
4220 # real ref.
4221 pushed_to_pending = False
4222 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004223 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004225 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004226 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004227 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004228 if not to_pending:
4229 if viewvc_url and revision:
4230 change_desc.append_footer(
4231 'Committed: %s%s' % (viewvc_url, revision))
4232 elif revision:
4233 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004234 print('Closing issue '
4235 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004236 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004238 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004239 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004240 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004241 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004242 if options.bypass_hooks:
4243 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4244 else:
4245 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004246 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004247
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004248 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004249 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print('The commit is in the pending queue (%s).' % pending_ref)
4251 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4252 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004253
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004254 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4255 if os.path.isfile(hook):
4256 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004257
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004258 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004259
4260
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004261def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print()
4263 print('Waiting for commit to be landed on %s...' % real_ref)
4264 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004265 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4266 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004267 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004268
4269 loop = 0
4270 while True:
4271 sys.stdout.write('fetching (%d)... \r' % loop)
4272 sys.stdout.flush()
4273 loop += 1
4274
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004275 if mirror:
4276 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004277 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4278 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4279 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4280 for commit in commits.splitlines():
4281 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004282 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004283 return commit
4284
4285 current_rev = to_rev
4286
4287
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004288def PushToGitPending(remote, pending_ref, upstream_ref):
4289 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4290
4291 Returns:
4292 (retcode of last operation, output log of last operation).
4293 """
4294 assert pending_ref.startswith('refs/'), pending_ref
4295 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4296 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4297 code = 0
4298 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004299 max_attempts = 3
4300 attempts_left = max_attempts
4301 while attempts_left:
4302 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004303 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004304 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004305
4306 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004307 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004308 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004309 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004310 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004311 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004312 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004313 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004314 continue
4315
4316 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004318 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004319 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004320 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004321 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4322 'the following files have merge conflicts:' % pending_ref)
4323 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4324 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004325 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004326 return code, out
4327
4328 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004329 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004330 code, out = RunGitWithCode(
4331 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4332 if code == 0:
4333 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004334 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004335 return code, out
4336
vapiera7fbd5a2016-06-16 09:17:49 -07004337 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004338 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004339 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004340 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004341 print('Fatal push error. Make sure your .netrc credentials and git '
4342 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004343 return code, out
4344
vapiera7fbd5a2016-06-16 09:17:49 -07004345 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004346 return code, out
4347
4348
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004349def IsFatalPushFailure(push_stdout):
4350 """True if retrying push won't help."""
4351 return '(prohibited by Gerrit)' in push_stdout
4352
4353
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004354@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004356 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004358 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004359 # If it looks like previous commits were mirrored with git-svn.
4360 message = """This repository appears to be a git-svn mirror, but no
4361upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4362 else:
4363 message = """This doesn't appear to be an SVN repository.
4364If your project has a true, writeable git repository, you probably want to run
4365'git cl land' instead.
4366If your project has a git mirror of an upstream SVN master, you probably need
4367to run 'git svn init'.
4368
4369Using the wrong command might cause your commit to appear to succeed, and the
4370review to be closed, without actually landing upstream. If you choose to
4371proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004372 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004373 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004374 # TODO(tandrii): kill this post SVN migration with
4375 # https://codereview.chromium.org/2076683002
4376 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4377 'Please let us know of this project you are committing to:'
4378 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004379 return SendUpstream(parser, args, 'dcommit')
4380
4381
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004382@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004383def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004384 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004385 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004386 print('This appears to be an SVN repository.')
4387 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004388 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004389 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004390 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391
4392
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004393@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004395 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004396 parser.add_option('-b', dest='newbranch',
4397 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004398 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004400 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4401 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004402 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004403 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004404 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004405 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004407 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004408
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004409
4410 group = optparse.OptionGroup(
4411 parser,
4412 'Options for continuing work on the current issue uploaded from a '
4413 'different clone (e.g. different machine). Must be used independently '
4414 'from the other options. No issue number should be specified, and the '
4415 'branch must have an issue number associated with it')
4416 group.add_option('--reapply', action='store_true', dest='reapply',
4417 help='Reset the branch and reapply the issue.\n'
4418 'CAUTION: This will undo any local changes in this '
4419 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004420
4421 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004422 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004423 parser.add_option_group(group)
4424
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004425 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004426 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004428 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004429 auth_config = auth.extract_auth_config_from_options(options)
4430
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004431
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004432 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004433 if options.newbranch:
4434 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004435 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004436 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004437
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004438 cl = Changelist(auth_config=auth_config,
4439 codereview=options.forced_codereview)
4440 if not cl.GetIssue():
4441 parser.error('current branch must have an associated issue')
4442
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004443 upstream = cl.GetUpstreamBranch()
4444 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004445 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004446
4447 RunGit(['reset', '--hard', upstream])
4448 if options.pull:
4449 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004450
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004451 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4452 options.directory)
4453
4454 if len(args) != 1 or not args[0]:
4455 parser.error('Must specify issue number or url')
4456
4457 # We don't want uncommitted changes mixed up with the patch.
4458 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004459 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004460
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004461 if options.newbranch:
4462 if options.force:
4463 RunGit(['branch', '-D', options.newbranch],
4464 stderr=subprocess2.PIPE, error_ok=True)
4465 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004466 elif not GetCurrentBranch():
4467 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004468
4469 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4470
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004471 if cl.IsGerrit():
4472 if options.reject:
4473 parser.error('--reject is not supported with Gerrit codereview.')
4474 if options.nocommit:
4475 parser.error('--nocommit is not supported with Gerrit codereview.')
4476 if options.directory:
4477 parser.error('--directory is not supported with Gerrit codereview.')
4478
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004479 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004480 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004481
4482
4483def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004484 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004485 # Provide a wrapper for git svn rebase to help avoid accidental
4486 # git svn dcommit.
4487 # It's the only command that doesn't use parser at all since we just defer
4488 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004489
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004490 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004491
4492
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004493def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004494 """Fetches the tree status and returns either 'open', 'closed',
4495 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004496 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004497 if url:
4498 status = urllib2.urlopen(url).read().lower()
4499 if status.find('closed') != -1 or status == '0':
4500 return 'closed'
4501 elif status.find('open') != -1 or status == '1':
4502 return 'open'
4503 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504 return 'unset'
4505
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004507def GetTreeStatusReason():
4508 """Fetches the tree status from a json url and returns the message
4509 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004510 url = settings.GetTreeStatusUrl()
4511 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004512 connection = urllib2.urlopen(json_url)
4513 status = json.loads(connection.read())
4514 connection.close()
4515 return status['message']
4516
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004517
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004518def GetBuilderMaster(bot_list):
4519 """For a given builder, fetch the master from AE if available."""
4520 map_url = 'https://builders-map.appspot.com/'
4521 try:
4522 master_map = json.load(urllib2.urlopen(map_url))
4523 except urllib2.URLError as e:
4524 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4525 (map_url, e))
4526 except ValueError as e:
4527 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4528 if not master_map:
4529 return None, 'Failed to build master map.'
4530
4531 result_master = ''
4532 for bot in bot_list:
4533 builder = bot.split(':', 1)[0]
4534 master_list = master_map.get(builder, [])
4535 if not master_list:
4536 return None, ('No matching master for builder %s.' % builder)
4537 elif len(master_list) > 1:
4538 return None, ('The builder name %s exists in multiple masters %s.' %
4539 (builder, master_list))
4540 else:
4541 cur_master = master_list[0]
4542 if not result_master:
4543 result_master = cur_master
4544 elif result_master != cur_master:
4545 return None, 'The builders do not belong to the same master.'
4546 return result_master, None
4547
4548
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004549def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004550 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004551 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004552 status = GetTreeStatus()
4553 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004554 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004555 return 2
4556
vapiera7fbd5a2016-06-16 09:17:49 -07004557 print('The tree is %s' % status)
4558 print()
4559 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004560 if status != 'open':
4561 return 1
4562 return 0
4563
4564
maruel@chromium.org15192402012-09-06 12:38:29 +00004565def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004566 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004567 group = optparse.OptionGroup(parser, "Try job options")
4568 group.add_option(
4569 "-b", "--bot", action="append",
4570 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4571 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004572 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004573 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004574 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004575 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004576 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004577 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004578 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004579 "-r", "--revision",
4580 help="Revision to use for the try job; default: the "
4581 "revision will be determined by the try server; see "
4582 "its waterfall for more info")
4583 group.add_option(
4584 "-c", "--clobber", action="store_true", default=False,
4585 help="Force a clobber before building; e.g. don't do an "
4586 "incremental build")
4587 group.add_option(
4588 "--project",
4589 help="Override which project to use. Projects are defined "
4590 "server-side to define what default bot set to use")
4591 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004592 "-p", "--property", dest="properties", action="append", default=[],
4593 help="Specify generic properties in the form -p key1=value1 -p "
4594 "key2=value2 etc (buildbucket only). The value will be treated as "
4595 "json if decodable, or as string otherwise.")
4596 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004597 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004598 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004599 "--use-rietveld", action="store_true", default=False,
4600 help="Use Rietveld to trigger try jobs.")
4601 group.add_option(
4602 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4603 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004604 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004605 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004606 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004607 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004608
machenbach@chromium.org45453142015-09-15 08:45:22 +00004609 if options.use_rietveld and options.properties:
4610 parser.error('Properties can only be specified with buildbucket')
4611
4612 # Make sure that all properties are prop=value pairs.
4613 bad_params = [x for x in options.properties if '=' not in x]
4614 if bad_params:
4615 parser.error('Got properties with missing "=": %s' % bad_params)
4616
maruel@chromium.org15192402012-09-06 12:38:29 +00004617 if args:
4618 parser.error('Unknown arguments: %s' % args)
4619
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004620 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004621 if not cl.GetIssue():
4622 parser.error('Need to upload first')
4623
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004624 if cl.IsGerrit():
4625 parser.error(
4626 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4627 'If your project has Commit Queue, dry run is a workaround:\n'
4628 ' git cl set-commit --dry-run')
4629 # Code below assumes Rietveld issue.
4630 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4631
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004632 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004633 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004634 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004635
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004636 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004637 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004638
maruel@chromium.org15192402012-09-06 12:38:29 +00004639 if not options.name:
4640 options.name = cl.GetBranch()
4641
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004642 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004643 options.master, err_msg = GetBuilderMaster(options.bot)
4644 if err_msg:
4645 parser.error('Tryserver master cannot be found because: %s\n'
4646 'Please manually specify the tryserver master'
4647 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004648
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004649 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004650 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004651 if not options.bot:
4652 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004653
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004654 # Get try masters from PRESUBMIT.py files.
4655 masters = presubmit_support.DoGetTryMasters(
4656 change,
4657 change.LocalPaths(),
4658 settings.GetRoot(),
4659 None,
4660 None,
4661 options.verbose,
4662 sys.stdout)
4663 if masters:
4664 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004665
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004666 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4667 options.bot = presubmit_support.DoGetTrySlaves(
4668 change,
4669 change.LocalPaths(),
4670 settings.GetRoot(),
4671 None,
4672 None,
4673 options.verbose,
4674 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004675
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004676 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004677 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004678
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004679 builders_and_tests = {}
4680 # TODO(machenbach): The old style command-line options don't support
4681 # multiple try masters yet.
4682 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4683 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4684
4685 for bot in old_style:
4686 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004687 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004688 elif ',' in bot:
4689 parser.error('Specify one bot per --bot flag')
4690 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004691 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004692
4693 for bot, tests in new_style:
4694 builders_and_tests.setdefault(bot, []).extend(tests)
4695
4696 # Return a master map with one master to be backwards compatible. The
4697 # master name defaults to an empty string, which will cause the master
4698 # not to be set on rietveld (deprecated).
4699 return {options.master: builders_and_tests}
4700
4701 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004702 if not masters:
4703 # Default to triggering Dry Run (see http://crbug.com/625697).
4704 if options.verbose:
4705 print('git cl try with no bots now defaults to CQ Dry Run.')
4706 try:
4707 cl.SetCQState(_CQState.DRY_RUN)
4708 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4709 return 0
4710 except KeyboardInterrupt:
4711 raise
4712 except:
4713 print('WARNING: failed to trigger CQ Dry Run.\n'
4714 'Either:\n'
4715 ' * your project has no CQ\n'
4716 ' * you don\'t have permission to trigger Dry Run\n'
4717 ' * bug in this code (see stack trace below).\n'
4718 'Consider specifying which bots to trigger manually '
4719 'or asking your project owners for permissions '
4720 'or contacting Chrome Infrastructure team at '
4721 'https://www.chromium.org/infra\n\n')
4722 # Still raise exception so that stack trace is printed.
4723 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004724
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004725 for builders in masters.itervalues():
4726 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004727 print('ERROR You are trying to send a job to a triggered bot. This type '
4728 'of bot requires an\ninitial job from a parent (usually a builder).'
4729 ' Instead send your job to the parent.\n'
4730 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004731 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004732
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004733 patchset = cl.GetMostRecentPatchset()
4734 if patchset and patchset != cl.GetPatchset():
4735 print(
4736 '\nWARNING Mismatch between local config and server. Did a previous '
4737 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4738 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004739 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004740 try:
4741 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4742 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004743 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004744 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004745 except Exception as e:
4746 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004747 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004748 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004749 return 1
4750 else:
4751 try:
4752 cl.RpcServer().trigger_distributed_try_jobs(
4753 cl.GetIssue(), patchset, options.name, options.clobber,
4754 options.revision, masters)
4755 except urllib2.HTTPError as e:
4756 if e.code == 404:
4757 print('404 from rietveld; '
4758 'did you mean to use "git try" instead of "git cl try"?')
4759 return 1
4760 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004761
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004762 for (master, builders) in sorted(masters.iteritems()):
4763 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004764 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004765 length = max(len(builder) for builder in builders)
4766 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004767 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004768 return 0
4769
4770
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004771def CMDtry_results(parser, args):
4772 group = optparse.OptionGroup(parser, "Try job results options")
4773 group.add_option(
4774 "-p", "--patchset", type=int, help="patchset number if not current.")
4775 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004776 "--print-master", action='store_true', help="print master name as well.")
4777 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004778 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004779 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004780 group.add_option(
4781 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4782 help="Host of buildbucket. The default host is %default.")
qyearsley53f48a12016-09-01 10:45:13 -07004783 group.add_option(
4784 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004785 parser.add_option_group(group)
4786 auth.add_auth_options(parser)
4787 options, args = parser.parse_args(args)
4788 if args:
4789 parser.error('Unrecognized args: %s' % ' '.join(args))
4790
4791 auth_config = auth.extract_auth_config_from_options(options)
4792 cl = Changelist(auth_config=auth_config)
4793 if not cl.GetIssue():
4794 parser.error('Need to upload first')
4795
4796 if not options.patchset:
4797 options.patchset = cl.GetMostRecentPatchset()
4798 if options.patchset and options.patchset != cl.GetPatchset():
4799 print(
4800 '\nWARNING Mismatch between local config and server. Did a previous '
4801 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4802 'Continuing using\npatchset %s.\n' % options.patchset)
4803 try:
4804 jobs = fetch_try_jobs(auth_config, cl, options)
4805 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004806 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004807 return 1
4808 except Exception as e:
4809 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004810 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004811 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004812 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004813 if options.json:
4814 write_try_results_json(options.json, jobs)
4815 else:
4816 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004817 return 0
4818
4819
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004820@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004821def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004822 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004823 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004824 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004825 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004827 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004828 if args:
4829 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004830 branch = cl.GetBranch()
4831 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004832 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004833 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004834
4835 # Clear configured merge-base, if there is one.
4836 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004837 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004838 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004839 return 0
4840
4841
thestig@chromium.org00858c82013-12-02 23:08:03 +00004842def CMDweb(parser, args):
4843 """Opens the current CL in the web browser."""
4844 _, args = parser.parse_args(args)
4845 if args:
4846 parser.error('Unrecognized args: %s' % ' '.join(args))
4847
4848 issue_url = Changelist().GetIssueURL()
4849 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004850 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004851 return 1
4852
4853 webbrowser.open(issue_url)
4854 return 0
4855
4856
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004857def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004858 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004859 parser.add_option('-d', '--dry-run', action='store_true',
4860 help='trigger in dry run mode')
4861 parser.add_option('-c', '--clear', action='store_true',
4862 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004863 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004864 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004865 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004866 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004867 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004868 if args:
4869 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004870 if options.dry_run and options.clear:
4871 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4872
iannuccie53c9352016-08-17 14:40:40 -07004873 cl = Changelist(auth_config=auth_config, issue=options.issue,
4874 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004875 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004876 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004877 elif options.dry_run:
4878 state = _CQState.DRY_RUN
4879 else:
4880 state = _CQState.COMMIT
4881 if not cl.GetIssue():
4882 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004883 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004884 return 0
4885
4886
groby@chromium.org411034a2013-02-26 15:12:01 +00004887def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004888 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004889 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004890 auth.add_auth_options(parser)
4891 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004892 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004893 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004894 if args:
4895 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004896 cl = Changelist(auth_config=auth_config, issue=options.issue,
4897 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004898 # Ensure there actually is an issue to close.
4899 cl.GetDescription()
4900 cl.CloseIssue()
4901 return 0
4902
4903
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004904def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004905 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004906 parser.add_option(
4907 '--stat',
4908 action='store_true',
4909 dest='stat',
4910 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004911 auth.add_auth_options(parser)
4912 options, args = parser.parse_args(args)
4913 auth_config = auth.extract_auth_config_from_options(options)
4914 if args:
4915 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004916
4917 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004918 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004919 # Staged changes would be committed along with the patch from last
4920 # upload, hence counted toward the "last upload" side in the final
4921 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004922 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004923 return 1
4924
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004925 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004926 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004927 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004928 if not issue:
4929 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004930 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004931 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004932
4933 # Create a new branch based on the merge-base
4934 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004935 # Clear cached branch in cl object, to avoid overwriting original CL branch
4936 # properties.
4937 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004938 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004939 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004940 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004941 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004942 return rtn
4943
wychen@chromium.org06928532015-02-03 02:11:29 +00004944 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004945 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004946 cmd = ['git', 'diff']
4947 if options.stat:
4948 cmd.append('--stat')
4949 cmd.extend([TMP_BRANCH, branch, '--'])
4950 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004951 finally:
4952 RunGit(['checkout', '-q', branch])
4953 RunGit(['branch', '-D', TMP_BRANCH])
4954
4955 return 0
4956
4957
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004958def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004959 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004960 parser.add_option(
4961 '--no-color',
4962 action='store_true',
4963 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004964 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004965 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004966 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004967
4968 author = RunGit(['config', 'user.email']).strip() or None
4969
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004970 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004971
4972 if args:
4973 if len(args) > 1:
4974 parser.error('Unknown args')
4975 base_branch = args[0]
4976 else:
4977 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004978 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004979
4980 change = cl.GetChange(base_branch, None)
4981 return owners_finder.OwnersFinder(
4982 [f.LocalPath() for f in
4983 cl.GetChange(base_branch, None).AffectedFiles()],
4984 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004985 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004986 disable_color=options.no_color).run()
4987
4988
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004989def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004990 """Generates a diff command."""
4991 # Generate diff for the current branch's changes.
4992 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4993 upstream_commit, '--' ]
4994
4995 if args:
4996 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004997 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004998 diff_cmd.append(arg)
4999 else:
5000 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005001
5002 return diff_cmd
5003
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005004def MatchingFileType(file_name, extensions):
5005 """Returns true if the file name ends with one of the given extensions."""
5006 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005007
enne@chromium.org555cfe42014-01-29 18:21:39 +00005008@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005009def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005010 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005011 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005012 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005013 parser.add_option('--full', action='store_true',
5014 help='Reformat the full content of all touched files')
5015 parser.add_option('--dry-run', action='store_true',
5016 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005017 parser.add_option('--python', action='store_true',
5018 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005019 parser.add_option('--diff', action='store_true',
5020 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005021 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005022
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005023 # git diff generates paths against the root of the repository. Change
5024 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005025 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005026 if rel_base_path:
5027 os.chdir(rel_base_path)
5028
digit@chromium.org29e47272013-05-17 17:01:46 +00005029 # Grab the merge-base commit, i.e. the upstream commit of the current
5030 # branch when it was created or the last time it was rebased. This is
5031 # to cover the case where the user may have called "git fetch origin",
5032 # moving the origin branch to a newer commit, but hasn't rebased yet.
5033 upstream_commit = None
5034 cl = Changelist()
5035 upstream_branch = cl.GetUpstreamBranch()
5036 if upstream_branch:
5037 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5038 upstream_commit = upstream_commit.strip()
5039
5040 if not upstream_commit:
5041 DieWithError('Could not find base commit for this branch. '
5042 'Are you in detached state?')
5043
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005044 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5045 diff_output = RunGit(changed_files_cmd)
5046 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005047 # Filter out files deleted by this CL
5048 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005049
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005050 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5051 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5052 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005053 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005054
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005055 top_dir = os.path.normpath(
5056 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5057
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005058 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5059 # formatted. This is used to block during the presubmit.
5060 return_value = 0
5061
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005062 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005063 # Locate the clang-format binary in the checkout
5064 try:
5065 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005066 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005067 DieWithError(e)
5068
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005069 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005070 cmd = [clang_format_tool]
5071 if not opts.dry_run and not opts.diff:
5072 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005073 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005074 if opts.diff:
5075 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005076 else:
5077 env = os.environ.copy()
5078 env['PATH'] = str(os.path.dirname(clang_format_tool))
5079 try:
5080 script = clang_format.FindClangFormatScriptInChromiumTree(
5081 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005082 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005083 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005084
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005085 cmd = [sys.executable, script, '-p0']
5086 if not opts.dry_run and not opts.diff:
5087 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005088
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005089 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5090 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005091
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005092 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5093 if opts.diff:
5094 sys.stdout.write(stdout)
5095 if opts.dry_run and len(stdout) > 0:
5096 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005097
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005098 # Similar code to above, but using yapf on .py files rather than clang-format
5099 # on C/C++ files
5100 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005101 yapf_tool = gclient_utils.FindExecutable('yapf')
5102 if yapf_tool is None:
5103 DieWithError('yapf not found in PATH')
5104
5105 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005106 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005107 cmd = [yapf_tool]
5108 if not opts.dry_run and not opts.diff:
5109 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005110 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005111 if opts.diff:
5112 sys.stdout.write(stdout)
5113 else:
5114 # TODO(sbc): yapf --lines mode still has some issues.
5115 # https://github.com/google/yapf/issues/154
5116 DieWithError('--python currently only works with --full')
5117
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005118 # Dart's formatter does not have the nice property of only operating on
5119 # modified chunks, so hard code full.
5120 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005121 try:
5122 command = [dart_format.FindDartFmtToolInChromiumTree()]
5123 if not opts.dry_run and not opts.diff:
5124 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005125 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005126
ppi@chromium.org6593d932016-03-03 15:41:15 +00005127 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005128 if opts.dry_run and stdout:
5129 return_value = 2
5130 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005131 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5132 'found in this checkout. Files in other languages are still '
5133 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005134
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005135 # Format GN build files. Always run on full build files for canonical form.
5136 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005137 cmd = ['gn', 'format' ]
5138 if opts.dry_run or opts.diff:
5139 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005140 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005141 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5142 shell=sys.platform == 'win32',
5143 cwd=top_dir)
5144 if opts.dry_run and gn_ret == 2:
5145 return_value = 2 # Not formatted.
5146 elif opts.diff and gn_ret == 2:
5147 # TODO this should compute and print the actual diff.
5148 print("This change has GN build file diff for " + gn_diff_file)
5149 elif gn_ret != 0:
5150 # For non-dry run cases (and non-2 return values for dry-run), a
5151 # nonzero error code indicates a failure, probably because the file
5152 # doesn't parse.
5153 DieWithError("gn format failed on " + gn_diff_file +
5154 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005155
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005156 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005157
5158
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005159@subcommand.usage('<codereview url or issue id>')
5160def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005161 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005162 _, args = parser.parse_args(args)
5163
5164 if len(args) != 1:
5165 parser.print_help()
5166 return 1
5167
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005168 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005169 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005170 parser.print_help()
5171 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005172 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005173
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005174 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005175 output = RunGit(['config', '--local', '--get-regexp',
5176 r'branch\..*\.%s' % issueprefix],
5177 error_ok=True)
5178 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005179 if issue == target_issue:
5180 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005181
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005182 branches = []
5183 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005184 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005185 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005186 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005187 return 1
5188 if len(branches) == 1:
5189 RunGit(['checkout', branches[0]])
5190 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005191 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005192 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005193 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005194 which = raw_input('Choose by index: ')
5195 try:
5196 RunGit(['checkout', branches[int(which)]])
5197 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005198 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005199 return 1
5200
5201 return 0
5202
5203
maruel@chromium.org29404b52014-09-08 22:58:00 +00005204def CMDlol(parser, args):
5205 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005206 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005207 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5208 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5209 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005210 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005211 return 0
5212
5213
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005214class OptionParser(optparse.OptionParser):
5215 """Creates the option parse and add --verbose support."""
5216 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005217 optparse.OptionParser.__init__(
5218 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005219 self.add_option(
5220 '-v', '--verbose', action='count', default=0,
5221 help='Use 2 times for more debugging info')
5222
5223 def parse_args(self, args=None, values=None):
5224 options, args = optparse.OptionParser.parse_args(self, args, values)
5225 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5226 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5227 return options, args
5228
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005229
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005230def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005231 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005232 print('\nYour python version %s is unsupported, please upgrade.\n' %
5233 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005234 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005235
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005236 # Reload settings.
5237 global settings
5238 settings = Settings()
5239
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005240 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005241 dispatcher = subcommand.CommandDispatcher(__name__)
5242 try:
5243 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005244 except auth.AuthenticationError as e:
5245 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005246 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005247 if e.code != 500:
5248 raise
5249 DieWithError(
5250 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5251 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005252 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005253
5254
5255if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005256 # These affect sys.stdout so do it outside of main() to simplify mocks in
5257 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005258 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005259 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005260 try:
5261 sys.exit(main(sys.argv[1:]))
5262 except KeyboardInterrupt:
5263 sys.stderr.write('interrupted\n')
5264 sys.exit(1)