blob: 24a8c996913eed0f9b6a33ef6a5ba204e60be649 [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
401 Returns a map from build id to build info as json dictionary.
402 """
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
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000536def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
537 """Return the corresponding git ref if |base_url| together with |glob_spec|
538 matches the full |url|.
539
540 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
541 """
542 fetch_suburl, as_ref = glob_spec.split(':')
543 if allow_wildcards:
544 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
545 if glob_match:
546 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
547 # "branches/{472,597,648}/src:refs/remotes/svn/*".
548 branch_re = re.escape(base_url)
549 if glob_match.group(1):
550 branch_re += '/' + re.escape(glob_match.group(1))
551 wildcard = glob_match.group(2)
552 if wildcard == '*':
553 branch_re += '([^/]*)'
554 else:
555 # Escape and replace surrounding braces with parentheses and commas
556 # with pipe symbols.
557 wildcard = re.escape(wildcard)
558 wildcard = re.sub('^\\\\{', '(', wildcard)
559 wildcard = re.sub('\\\\,', '|', wildcard)
560 wildcard = re.sub('\\\\}$', ')', wildcard)
561 branch_re += wildcard
562 if glob_match.group(3):
563 branch_re += re.escape(glob_match.group(3))
564 match = re.match(branch_re, url)
565 if match:
566 return re.sub('\*$', match.group(1), as_ref)
567
568 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
569 if fetch_suburl:
570 full_url = base_url + '/' + fetch_suburl
571 else:
572 full_url = base_url
573 if full_url == url:
574 return as_ref
575 return None
576
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000577
iannucci@chromium.org79540052012-10-19 23:15:26 +0000578def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000579 """Prints statistics about the change to the user."""
580 # --no-ext-diff is broken in some versions of Git, so try to work around
581 # this by overriding the environment (but there is still a problem if the
582 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000583 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000584 if 'GIT_EXTERNAL_DIFF' in env:
585 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000586
587 if find_copies:
588 similarity_options = ['--find-copies-harder', '-l100000',
589 '-C%s' % similarity]
590 else:
591 similarity_options = ['-M%s' % similarity]
592
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000593 try:
594 stdout = sys.stdout.fileno()
595 except AttributeError:
596 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000597 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000598 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000599 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000600 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000601
602
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000603class BuildbucketResponseException(Exception):
604 pass
605
606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607class Settings(object):
608 def __init__(self):
609 self.default_server = None
610 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000611 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 self.is_git_svn = None
613 self.svn_branch = None
614 self.tree_status_url = None
615 self.viewvc_url = None
616 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000617 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000618 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000619 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000620 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000621 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000622 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000623 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624
625 def LazyUpdateIfNeeded(self):
626 """Updates the settings from a codereview.settings file, if available."""
627 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000628 # The only value that actually changes the behavior is
629 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000630 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000631 error_ok=True
632 ).strip().lower()
633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000635 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636 LoadCodereviewSettingsFromFile(cr_settings_file)
637 self.updated = True
638
639 def GetDefaultServerUrl(self, error_ok=False):
640 if not self.default_server:
641 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000642 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000643 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 if error_ok:
645 return self.default_server
646 if not self.default_server:
647 error_message = ('Could not find settings file. You must configure '
648 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000649 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000650 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 return self.default_server
652
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000653 @staticmethod
654 def GetRelativeRoot():
655 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000656
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000658 if self.root is None:
659 self.root = os.path.abspath(self.GetRelativeRoot())
660 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000662 def GetGitMirror(self, remote='origin'):
663 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000664 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000665 if not os.path.isdir(local_url):
666 return None
667 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
668 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
669 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
670 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
671 if mirror.exists():
672 return mirror
673 return None
674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675 def GetIsGitSvn(self):
676 """Return true if this repo looks like it's using git-svn."""
677 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000678 if self.GetPendingRefPrefix():
679 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
680 self.is_git_svn = False
681 else:
682 # If you have any "svn-remote.*" config keys, we think you're using svn.
683 self.is_git_svn = RunGitWithCode(
684 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 return self.is_git_svn
686
687 def GetSVNBranch(self):
688 if self.svn_branch is None:
689 if not self.GetIsGitSvn():
690 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
691
692 # Try to figure out which remote branch we're based on.
693 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000694 # 1) iterate through our branch history and find the svn URL.
695 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696
697 # regexp matching the git-svn line that contains the URL.
698 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
699
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 # We don't want to go through all of history, so read a line from the
701 # pipe at a time.
702 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000703 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000704 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
705 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000706 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000707 for line in proc.stdout:
708 match = git_svn_re.match(line)
709 if match:
710 url = match.group(1)
711 proc.stdout.close() # Cut pipe.
712 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000714 if url:
715 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
716 remotes = RunGit(['config', '--get-regexp',
717 r'^svn-remote\..*\.url']).splitlines()
718 for remote in remotes:
719 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000721 remote = match.group(1)
722 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000723 rewrite_root = RunGit(
724 ['config', 'svn-remote.%s.rewriteRoot' % remote],
725 error_ok=True).strip()
726 if rewrite_root:
727 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000728 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000729 ['config', 'svn-remote.%s.fetch' % remote],
730 error_ok=True).strip()
731 if fetch_spec:
732 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
733 if self.svn_branch:
734 break
735 branch_spec = RunGit(
736 ['config', 'svn-remote.%s.branches' % remote],
737 error_ok=True).strip()
738 if branch_spec:
739 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
740 if self.svn_branch:
741 break
742 tag_spec = RunGit(
743 ['config', 'svn-remote.%s.tags' % remote],
744 error_ok=True).strip()
745 if tag_spec:
746 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
747 if self.svn_branch:
748 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749
750 if not self.svn_branch:
751 DieWithError('Can\'t guess svn branch -- try specifying it on the '
752 'command line')
753
754 return self.svn_branch
755
756 def GetTreeStatusUrl(self, error_ok=False):
757 if not self.tree_status_url:
758 error_message = ('You must configure your tree status URL by running '
759 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 self.tree_status_url = self._GetRietveldConfig(
761 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 return self.tree_status_url
763
764 def GetViewVCUrl(self):
765 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 return self.viewvc_url
768
rmistry@google.com90752582014-01-14 21:04:50 +0000769 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000770 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000771
rmistry@google.com78948ed2015-07-08 23:09:57 +0000772 def GetIsSkipDependencyUpload(self, branch_name):
773 """Returns true if specified branch should skip dep uploads."""
774 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
775 error_ok=True)
776
rmistry@google.com5626a922015-02-26 14:03:30 +0000777 def GetRunPostUploadHook(self):
778 run_post_upload_hook = self._GetRietveldConfig(
779 'run-post-upload-hook', error_ok=True)
780 return run_post_upload_hook == "True"
781
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000782 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000783 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000784
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000785 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000787
ukai@chromium.orge8077812012-02-03 03:41:46 +0000788 def GetIsGerrit(self):
789 """Return true if this repo is assosiated with gerrit code review system."""
790 if self.is_gerrit is None:
791 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
792 return self.is_gerrit
793
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000794 def GetSquashGerritUploads(self):
795 """Return true if uploads to Gerrit should be squashed by default."""
796 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700797 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
798 if self.squash_gerrit_uploads is None:
799 # Default is squash now (http://crbug.com/611892#c23).
800 self.squash_gerrit_uploads = not (
801 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
802 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000803 return self.squash_gerrit_uploads
804
tandriia60502f2016-06-20 02:01:53 -0700805 def GetSquashGerritUploadsOverride(self):
806 """Return True or False if codereview.settings should be overridden.
807
808 Returns None if no override has been defined.
809 """
810 # See also http://crbug.com/611892#c23
811 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
812 error_ok=True).strip()
813 if result == 'true':
814 return True
815 if result == 'false':
816 return False
817 return None
818
tandrii@chromium.org28253532016-04-14 13:46:56 +0000819 def GetGerritSkipEnsureAuthenticated(self):
820 """Return True if EnsureAuthenticated should not be done for Gerrit
821 uploads."""
822 if self.gerrit_skip_ensure_authenticated is None:
823 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000824 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000825 error_ok=True).strip() == 'true')
826 return self.gerrit_skip_ensure_authenticated
827
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000828 def GetGitEditor(self):
829 """Return the editor specified in the git config, or None if none is."""
830 if self.git_editor is None:
831 self.git_editor = self._GetConfig('core.editor', error_ok=True)
832 return self.git_editor or None
833
thestig@chromium.org44202a22014-03-11 19:22:18 +0000834 def GetLintRegex(self):
835 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
836 DEFAULT_LINT_REGEX)
837
838 def GetLintIgnoreRegex(self):
839 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
840 DEFAULT_LINT_IGNORE_REGEX)
841
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000842 def GetProject(self):
843 if not self.project:
844 self.project = self._GetRietveldConfig('project', error_ok=True)
845 return self.project
846
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000847 def GetForceHttpsCommitUrl(self):
848 if not self.force_https_commit_url:
849 self.force_https_commit_url = self._GetRietveldConfig(
850 'force-https-commit-url', error_ok=True)
851 return self.force_https_commit_url
852
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000853 def GetPendingRefPrefix(self):
854 if not self.pending_ref_prefix:
855 self.pending_ref_prefix = self._GetRietveldConfig(
856 'pending-ref-prefix', error_ok=True)
857 return self.pending_ref_prefix
858
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000859 def _GetRietveldConfig(self, param, **kwargs):
860 return self._GetConfig('rietveld.' + param, **kwargs)
861
rmistry@google.com78948ed2015-07-08 23:09:57 +0000862 def _GetBranchConfig(self, branch_name, param, **kwargs):
863 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
864
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 def _GetConfig(self, param, **kwargs):
866 self.LazyUpdateIfNeeded()
867 return RunGit(['config', param], **kwargs).strip()
868
869
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870def ShortBranchName(branch):
871 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000872 return branch.replace('refs/heads/', '', 1)
873
874
875def GetCurrentBranchRef():
876 """Returns branch ref (e.g., refs/heads/master) or None."""
877 return RunGit(['symbolic-ref', 'HEAD'],
878 stderr=subprocess2.VOID, error_ok=True).strip() or None
879
880
881def GetCurrentBranch():
882 """Returns current branch or None.
883
884 For refs/heads/* branches, returns just last part. For others, full ref.
885 """
886 branchref = GetCurrentBranchRef()
887 if branchref:
888 return ShortBranchName(branchref)
889 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890
891
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000892class _CQState(object):
893 """Enum for states of CL with respect to Commit Queue."""
894 NONE = 'none'
895 DRY_RUN = 'dry_run'
896 COMMIT = 'commit'
897
898 ALL_STATES = [NONE, DRY_RUN, COMMIT]
899
900
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000901class _ParsedIssueNumberArgument(object):
902 def __init__(self, issue=None, patchset=None, hostname=None):
903 self.issue = issue
904 self.patchset = patchset
905 self.hostname = hostname
906
907 @property
908 def valid(self):
909 return self.issue is not None
910
911
912class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
913 def __init__(self, *args, **kwargs):
914 self.patch_url = kwargs.pop('patch_url', None)
915 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
916
917
918def ParseIssueNumberArgument(arg):
919 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
920 fail_result = _ParsedIssueNumberArgument()
921
922 if arg.isdigit():
923 return _ParsedIssueNumberArgument(issue=int(arg))
924 if not arg.startswith('http'):
925 return fail_result
926 url = gclient_utils.UpgradeToHttps(arg)
927 try:
928 parsed_url = urlparse.urlparse(url)
929 except ValueError:
930 return fail_result
931 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
932 tmp = cls.ParseIssueURL(parsed_url)
933 if tmp is not None:
934 return tmp
935 return fail_result
936
937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000939 """Changelist works with one changelist in local branch.
940
941 Supports two codereview backends: Rietveld or Gerrit, selected at object
942 creation.
943
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000944 Notes:
945 * Not safe for concurrent multi-{thread,process} use.
946 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700947 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000948 """
949
950 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
951 """Create a new ChangeList instance.
952
953 If issue is given, the codereview must be given too.
954
955 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
956 Otherwise, it's decided based on current configuration of the local branch,
957 with default being 'rietveld' for backwards compatibility.
958 See _load_codereview_impl for more details.
959
960 **kwargs will be passed directly to codereview implementation.
961 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000963 global settings
964 if not settings:
965 # Happens when git_cl.py is used as a utility library.
966 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967
968 if issue:
969 assert codereview, 'codereview must be known, if issue is known'
970
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971 self.branchref = branchref
972 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000973 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 self.branch = ShortBranchName(self.branchref)
975 else:
976 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000978 self.lookedup_issue = False
979 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980 self.has_description = False
981 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000982 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000984 self.cc = None
985 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000986 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000987
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000989 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000990 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000991 assert self._codereview_impl
992 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000993
994 def _load_codereview_impl(self, codereview=None, **kwargs):
995 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000996 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
997 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
998 self._codereview = codereview
999 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001000 return
1001
1002 # Automatic selection based on issue number set for a current branch.
1003 # Rietveld takes precedence over Gerrit.
1004 assert not self.issue
1005 # Whether we find issue or not, we are doing the lookup.
1006 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001007 if self.GetBranch():
1008 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1009 issue = _git_get_branch_config_value(
1010 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1011 if issue:
1012 self._codereview = codereview
1013 self._codereview_impl = cls(self, **kwargs)
1014 self.issue = int(issue)
1015 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001016
1017 # No issue is set for this branch, so decide based on repo-wide settings.
1018 return self._load_codereview_impl(
1019 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1020 **kwargs)
1021
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001022 def IsGerrit(self):
1023 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001024
1025 def GetCCList(self):
1026 """Return the users cc'd on this CL.
1027
agable92bec4f2016-08-24 09:27:27 -07001028 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001029 """
1030 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001031 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001032 more_cc = ','.join(self.watchers)
1033 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1034 return self.cc
1035
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001036 def GetCCListWithoutDefault(self):
1037 """Return the users cc'd on this CL excluding default ones."""
1038 if self.cc is None:
1039 self.cc = ','.join(self.watchers)
1040 return self.cc
1041
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001042 def SetWatchers(self, watchers):
1043 """Set the list of email addresses that should be cc'd based on the changed
1044 files in this CL.
1045 """
1046 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047
1048 def GetBranch(self):
1049 """Returns the short branch name, e.g. 'master'."""
1050 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001051 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001052 if not branchref:
1053 return None
1054 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 self.branch = ShortBranchName(self.branchref)
1056 return self.branch
1057
1058 def GetBranchRef(self):
1059 """Returns the full branch name, e.g. 'refs/heads/master'."""
1060 self.GetBranch() # Poke the lazy loader.
1061 return self.branchref
1062
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001063 def ClearBranch(self):
1064 """Clears cached branch data of this object."""
1065 self.branch = self.branchref = None
1066
tandrii5d48c322016-08-18 16:19:37 -07001067 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1068 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1069 kwargs['branch'] = self.GetBranch()
1070 return _git_get_branch_config_value(key, default, **kwargs)
1071
1072 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1073 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1074 assert self.GetBranch(), (
1075 'this CL must have an associated branch to %sset %s%s' %
1076 ('un' if value is None else '',
1077 key,
1078 '' if value is None else ' to %r' % value))
1079 kwargs['branch'] = self.GetBranch()
1080 return _git_set_branch_config_value(key, value, **kwargs)
1081
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001082 @staticmethod
1083 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001084 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 e.g. 'origin', 'refs/heads/master'
1086 """
1087 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001088 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001091 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001093 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1094 error_ok=True).strip()
1095 if upstream_branch:
1096 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001098 # Fall back on trying a git-svn upstream branch.
1099 if settings.GetIsGitSvn():
1100 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001102 # Else, try to guess the origin remote.
1103 remote_branches = RunGit(['branch', '-r']).split()
1104 if 'origin/master' in remote_branches:
1105 # Fall back on origin/master if it exits.
1106 remote = 'origin'
1107 upstream_branch = 'refs/heads/master'
1108 elif 'origin/trunk' in remote_branches:
1109 # Fall back on origin/trunk if it exists. Generally a shared
1110 # git-svn clone
1111 remote = 'origin'
1112 upstream_branch = 'refs/heads/trunk'
1113 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 DieWithError(
1115 'Unable to determine default branch to diff against.\n'
1116 'Either pass complete "git diff"-style arguments, like\n'
1117 ' git cl upload origin/master\n'
1118 'or verify this branch is set up to track another \n'
1119 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
1121 return remote, upstream_branch
1122
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001123 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001124 upstream_branch = self.GetUpstreamBranch()
1125 if not BranchExists(upstream_branch):
1126 DieWithError('The upstream for the current branch (%s) does not exist '
1127 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001128 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001129 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001130
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 def GetUpstreamBranch(self):
1132 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001133 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001135 upstream_branch = upstream_branch.replace('refs/heads/',
1136 'refs/remotes/%s/' % remote)
1137 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1138 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.upstream_branch = upstream_branch
1140 return self.upstream_branch
1141
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001142 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001143 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001144 remote, branch = None, self.GetBranch()
1145 seen_branches = set()
1146 while branch not in seen_branches:
1147 seen_branches.add(branch)
1148 remote, branch = self.FetchUpstreamTuple(branch)
1149 branch = ShortBranchName(branch)
1150 if remote != '.' or branch.startswith('refs/remotes'):
1151 break
1152 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001153 remotes = RunGit(['remote'], error_ok=True).split()
1154 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001155 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001156 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001157 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001158 logging.warning('Could not determine which remote this change is '
1159 'associated with, so defaulting to "%s". This may '
1160 'not be what you want. You may prevent this message '
1161 'by running "git svn info" as documented here: %s',
1162 self._remote,
1163 GIT_INSTRUCTIONS_URL)
1164 else:
1165 logging.warn('Could not determine which remote this change is '
1166 'associated with. You may prevent this message by '
1167 'running "git svn info" as documented here: %s',
1168 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 branch = 'HEAD'
1170 if branch.startswith('refs/remotes'):
1171 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001172 elif branch.startswith('refs/branch-heads/'):
1173 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001174 else:
1175 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001176 return self._remote
1177
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 def GitSanityChecks(self, upstream_git_obj):
1179 """Checks git repo status and ensures diff is from local commits."""
1180
sbc@chromium.org79706062015-01-14 21:18:12 +00001181 if upstream_git_obj is None:
1182 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001183 print('ERROR: unable to determine current branch (detached HEAD?)',
1184 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001185 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001186 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001187 return False
1188
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 # Verify the commit we're diffing against is in our current branch.
1190 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1191 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1192 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001193 print('ERROR: %s is not in the current branch. You may need to rebase '
1194 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001195 return False
1196
1197 # List the commits inside the diff, and verify they are all local.
1198 commits_in_diff = RunGit(
1199 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1200 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1201 remote_branch = remote_branch.strip()
1202 if code != 0:
1203 _, remote_branch = self.GetRemoteBranch()
1204
1205 commits_in_remote = RunGit(
1206 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1207
1208 common_commits = set(commits_in_diff) & set(commits_in_remote)
1209 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001210 print('ERROR: Your diff contains %d commits already in %s.\n'
1211 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1212 'the diff. If you are using a custom git flow, you can override'
1213 ' the reference used for this check with "git config '
1214 'gitcl.remotebranch <git-ref>".' % (
1215 len(common_commits), remote_branch, upstream_git_obj),
1216 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001217 return False
1218 return True
1219
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001220 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001221 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001222
1223 Returns None if it is not set.
1224 """
tandrii5d48c322016-08-18 16:19:37 -07001225 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001226
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001227 def GetGitSvnRemoteUrl(self):
1228 """Return the configured git-svn remote URL parsed from git svn info.
1229
1230 Returns None if it is not set.
1231 """
1232 # URL is dependent on the current directory.
1233 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1234 if data:
1235 keys = dict(line.split(': ', 1) for line in data.splitlines()
1236 if ': ' in line)
1237 return keys.get('URL', None)
1238 return None
1239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 def GetRemoteUrl(self):
1241 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1242
1243 Returns None if there is no remote.
1244 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001246 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1247
1248 # If URL is pointing to a local directory, it is probably a git cache.
1249 if os.path.isdir(url):
1250 url = RunGit(['config', 'remote.%s.url' % remote],
1251 error_ok=True,
1252 cwd=url).strip()
1253 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001255 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001256 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001257 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001258 self.issue = self._GitGetBranchConfigValue(
1259 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001260 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 return self.issue
1262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 def GetIssueURL(self):
1264 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001265 issue = self.GetIssue()
1266 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001267 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001268 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269
1270 def GetDescription(self, pretty=False):
1271 if not self.has_description:
1272 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001273 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 self.has_description = True
1275 if pretty:
1276 wrapper = textwrap.TextWrapper()
1277 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1278 return wrapper.fill(self.description)
1279 return self.description
1280
1281 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001282 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001283 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001284 self.patchset = self._GitGetBranchConfigValue(
1285 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001286 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 return self.patchset
1288
1289 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001290 """Set this branch's patchset. If patchset=0, clears the patchset."""
1291 assert self.GetBranch()
1292 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001293 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001294 else:
1295 self.patchset = int(patchset)
1296 self._GitSetBranchConfigValue(
1297 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001299 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001300 """Set this branch's issue. If issue isn't given, clears the issue."""
1301 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001303 issue = int(issue)
1304 self._GitSetBranchConfigValue(
1305 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001306 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001307 codereview_server = self._codereview_impl.GetCodereviewServer()
1308 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001309 self._GitSetBranchConfigValue(
1310 self._codereview_impl.CodereviewServerConfigKey(),
1311 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 else:
tandrii5d48c322016-08-18 16:19:37 -07001313 # Reset all of these just to be clean.
1314 reset_suffixes = [
1315 'last-upload-hash',
1316 self._codereview_impl.IssueConfigKey(),
1317 self._codereview_impl.PatchsetConfigKey(),
1318 self._codereview_impl.CodereviewServerConfigKey(),
1319 ] + self._PostUnsetIssueProperties()
1320 for prop in reset_suffixes:
1321 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001322 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001323 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001325 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 if not self.GitSanityChecks(upstream_branch):
1327 DieWithError('\nGit sanity check failure')
1328
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001329 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001330 if not root:
1331 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001332 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001333
1334 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001335 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001336 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001337 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001338 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001339 except subprocess2.CalledProcessError:
1340 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001341 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001342 'This branch probably doesn\'t exist anymore. To reset the\n'
1343 'tracking branch, please run\n'
1344 ' git branch --set-upstream %s trunk\n'
1345 'replacing trunk with origin/master or the relevant branch') %
1346 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001347
maruel@chromium.org52424302012-08-29 15:14:30 +00001348 issue = self.GetIssue()
1349 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001350 if issue:
1351 description = self.GetDescription()
1352 else:
1353 # If the change was never uploaded, use the log messages of all commits
1354 # up to the branch point, as git cl upload will prefill the description
1355 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001356 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1357 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001358
1359 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001360 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001361 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001362 name,
1363 description,
1364 absroot,
1365 files,
1366 issue,
1367 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001368 author,
1369 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001370
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001371 def UpdateDescription(self, description):
1372 self.description = description
1373 return self._codereview_impl.UpdateDescriptionRemote(description)
1374
1375 def RunHook(self, committing, may_prompt, verbose, change):
1376 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1377 try:
1378 return presubmit_support.DoPresubmitChecks(change, committing,
1379 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1380 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001381 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1382 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001383 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001384 DieWithError(
1385 ('%s\nMaybe your depot_tools is out of date?\n'
1386 'If all fails, contact maruel@') % e)
1387
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001388 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1389 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001390 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1391 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001392 else:
1393 # Assume url.
1394 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1395 urlparse.urlparse(issue_arg))
1396 if not parsed_issue_arg or not parsed_issue_arg.valid:
1397 DieWithError('Failed to parse issue argument "%s". '
1398 'Must be an issue number or a valid URL.' % issue_arg)
1399 return self._codereview_impl.CMDPatchWithParsedIssue(
1400 parsed_issue_arg, reject, nocommit, directory)
1401
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001402 def CMDUpload(self, options, git_diff_args, orig_args):
1403 """Uploads a change to codereview."""
1404 if git_diff_args:
1405 # TODO(ukai): is it ok for gerrit case?
1406 base_branch = git_diff_args[0]
1407 else:
1408 if self.GetBranch() is None:
1409 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1410
1411 # Default to diffing against common ancestor of upstream branch
1412 base_branch = self.GetCommonAncestorWithUpstream()
1413 git_diff_args = [base_branch, 'HEAD']
1414
1415 # Make sure authenticated to codereview before running potentially expensive
1416 # hooks. It is a fast, best efforts check. Codereview still can reject the
1417 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001418 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001419
1420 # Apply watchlists on upload.
1421 change = self.GetChange(base_branch, None)
1422 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1423 files = [f.LocalPath() for f in change.AffectedFiles()]
1424 if not options.bypass_watchlists:
1425 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1426
1427 if not options.bypass_hooks:
1428 if options.reviewers or options.tbr_owners:
1429 # Set the reviewer list now so that presubmit checks can access it.
1430 change_description = ChangeDescription(change.FullDescriptionText())
1431 change_description.update_reviewers(options.reviewers,
1432 options.tbr_owners,
1433 change)
1434 change.SetDescriptionText(change_description.description)
1435 hook_results = self.RunHook(committing=False,
1436 may_prompt=not options.force,
1437 verbose=options.verbose,
1438 change=change)
1439 if not hook_results.should_continue():
1440 return 1
1441 if not options.reviewers and hook_results.reviewers:
1442 options.reviewers = hook_results.reviewers.split(',')
1443
1444 if self.GetIssue():
1445 latest_patchset = self.GetMostRecentPatchset()
1446 local_patchset = self.GetPatchset()
1447 if (latest_patchset and local_patchset and
1448 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001449 print('The last upload made from this repository was patchset #%d but '
1450 'the most recent patchset on the server is #%d.'
1451 % (local_patchset, latest_patchset))
1452 print('Uploading will still work, but if you\'ve uploaded to this '
1453 'issue from another machine or branch the patch you\'re '
1454 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001455 ask_for_data('About to upload; enter to confirm.')
1456
1457 print_stats(options.similarity, options.find_copies, git_diff_args)
1458 ret = self.CMDUploadChange(options, git_diff_args, change)
1459 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001460 if options.use_commit_queue:
1461 self.SetCQState(_CQState.COMMIT)
1462 elif options.cq_dry_run:
1463 self.SetCQState(_CQState.DRY_RUN)
1464
tandrii5d48c322016-08-18 16:19:37 -07001465 _git_set_branch_config_value('last-upload-hash',
1466 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001467 # Run post upload hooks, if specified.
1468 if settings.GetRunPostUploadHook():
1469 presubmit_support.DoPostUploadExecuter(
1470 change,
1471 self,
1472 settings.GetRoot(),
1473 options.verbose,
1474 sys.stdout)
1475
1476 # Upload all dependencies if specified.
1477 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001478 print()
1479 print('--dependencies has been specified.')
1480 print('All dependent local branches will be re-uploaded.')
1481 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001482 # Remove the dependencies flag from args so that we do not end up in a
1483 # loop.
1484 orig_args.remove('--dependencies')
1485 ret = upload_branch_deps(self, orig_args)
1486 return ret
1487
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001488 def SetCQState(self, new_state):
1489 """Update the CQ state for latest patchset.
1490
1491 Issue must have been already uploaded and known.
1492 """
1493 assert new_state in _CQState.ALL_STATES
1494 assert self.GetIssue()
1495 return self._codereview_impl.SetCQState(new_state)
1496
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001497 # Forward methods to codereview specific implementation.
1498
1499 def CloseIssue(self):
1500 return self._codereview_impl.CloseIssue()
1501
1502 def GetStatus(self):
1503 return self._codereview_impl.GetStatus()
1504
1505 def GetCodereviewServer(self):
1506 return self._codereview_impl.GetCodereviewServer()
1507
1508 def GetApprovingReviewers(self):
1509 return self._codereview_impl.GetApprovingReviewers()
1510
1511 def GetMostRecentPatchset(self):
1512 return self._codereview_impl.GetMostRecentPatchset()
1513
1514 def __getattr__(self, attr):
1515 # This is because lots of untested code accesses Rietveld-specific stuff
1516 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001517 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001518 # Note that child method defines __getattr__ as well, and forwards it here,
1519 # because _RietveldChangelistImpl is not cleaned up yet, and given
1520 # deprecation of Rietveld, it should probably be just removed.
1521 # Until that time, avoid infinite recursion by bypassing __getattr__
1522 # of implementation class.
1523 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001524
1525
1526class _ChangelistCodereviewBase(object):
1527 """Abstract base class encapsulating codereview specifics of a changelist."""
1528 def __init__(self, changelist):
1529 self._changelist = changelist # instance of Changelist
1530
1531 def __getattr__(self, attr):
1532 # Forward methods to changelist.
1533 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1534 # _RietveldChangelistImpl to avoid this hack?
1535 return getattr(self._changelist, attr)
1536
1537 def GetStatus(self):
1538 """Apply a rough heuristic to give a simple summary of an issue's review
1539 or CQ status, assuming adherence to a common workflow.
1540
1541 Returns None if no issue for this branch, or specific string keywords.
1542 """
1543 raise NotImplementedError()
1544
1545 def GetCodereviewServer(self):
1546 """Returns server URL without end slash, like "https://codereview.com"."""
1547 raise NotImplementedError()
1548
1549 def FetchDescription(self):
1550 """Fetches and returns description from the codereview server."""
1551 raise NotImplementedError()
1552
tandrii5d48c322016-08-18 16:19:37 -07001553 @classmethod
1554 def IssueConfigKey(cls):
1555 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001556 raise NotImplementedError()
1557
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001558 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001559 def PatchsetConfigKey(cls):
1560 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561 raise NotImplementedError()
1562
tandrii5d48c322016-08-18 16:19:37 -07001563 @classmethod
1564 def CodereviewServerConfigKey(cls):
1565 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001566 raise NotImplementedError()
1567
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001568 def _PostUnsetIssueProperties(self):
1569 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001570 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001571
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001572 def GetRieveldObjForPresubmit(self):
1573 # This is an unfortunate Rietveld-embeddedness in presubmit.
1574 # For non-Rietveld codereviews, this probably should return a dummy object.
1575 raise NotImplementedError()
1576
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001577 def GetGerritObjForPresubmit(self):
1578 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1579 return None
1580
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001581 def UpdateDescriptionRemote(self, description):
1582 """Update the description on codereview site."""
1583 raise NotImplementedError()
1584
1585 def CloseIssue(self):
1586 """Closes the issue."""
1587 raise NotImplementedError()
1588
1589 def GetApprovingReviewers(self):
1590 """Returns a list of reviewers approving the change.
1591
1592 Note: not necessarily committers.
1593 """
1594 raise NotImplementedError()
1595
1596 def GetMostRecentPatchset(self):
1597 """Returns the most recent patchset number from the codereview site."""
1598 raise NotImplementedError()
1599
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001600 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1601 directory):
1602 """Fetches and applies the issue.
1603
1604 Arguments:
1605 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1606 reject: if True, reject the failed patch instead of switching to 3-way
1607 merge. Rietveld only.
1608 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1609 only.
1610 directory: switch to directory before applying the patch. Rietveld only.
1611 """
1612 raise NotImplementedError()
1613
1614 @staticmethod
1615 def ParseIssueURL(parsed_url):
1616 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1617 failed."""
1618 raise NotImplementedError()
1619
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001620 def EnsureAuthenticated(self, force):
1621 """Best effort check that user is authenticated with codereview server.
1622
1623 Arguments:
1624 force: whether to skip confirmation questions.
1625 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001626 raise NotImplementedError()
1627
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001628 def CMDUploadChange(self, options, args, change):
1629 """Uploads a change to codereview."""
1630 raise NotImplementedError()
1631
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001632 def SetCQState(self, new_state):
1633 """Update the CQ state for latest patchset.
1634
1635 Issue must have been already uploaded and known.
1636 """
1637 raise NotImplementedError()
1638
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001639
1640class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1641 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1642 super(_RietveldChangelistImpl, self).__init__(changelist)
1643 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001644 if not rietveld_server:
1645 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001646
1647 self._rietveld_server = rietveld_server
1648 self._auth_config = auth_config
1649 self._props = None
1650 self._rpc_server = None
1651
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001652 def GetCodereviewServer(self):
1653 if not self._rietveld_server:
1654 # If we're on a branch then get the server potentially associated
1655 # with that branch.
1656 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001657 self._rietveld_server = gclient_utils.UpgradeToHttps(
1658 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001659 if not self._rietveld_server:
1660 self._rietveld_server = settings.GetDefaultServerUrl()
1661 return self._rietveld_server
1662
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001663 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001664 """Best effort check that user is authenticated with Rietveld server."""
1665 if self._auth_config.use_oauth2:
1666 authenticator = auth.get_authenticator_for_host(
1667 self.GetCodereviewServer(), self._auth_config)
1668 if not authenticator.has_cached_credentials():
1669 raise auth.LoginRequiredError(self.GetCodereviewServer())
1670
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001671 def FetchDescription(self):
1672 issue = self.GetIssue()
1673 assert issue
1674 try:
1675 return self.RpcServer().get_description(issue).strip()
1676 except urllib2.HTTPError as e:
1677 if e.code == 404:
1678 DieWithError(
1679 ('\nWhile fetching the description for issue %d, received a '
1680 '404 (not found)\n'
1681 'error. It is likely that you deleted this '
1682 'issue on the server. If this is the\n'
1683 'case, please run\n\n'
1684 ' git cl issue 0\n\n'
1685 'to clear the association with the deleted issue. Then run '
1686 'this command again.') % issue)
1687 else:
1688 DieWithError(
1689 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1690 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001691 print('Warning: Failed to retrieve CL description due to network '
1692 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693 return ''
1694
1695 def GetMostRecentPatchset(self):
1696 return self.GetIssueProperties()['patchsets'][-1]
1697
1698 def GetPatchSetDiff(self, issue, patchset):
1699 return self.RpcServer().get(
1700 '/download/issue%s_%s.diff' % (issue, patchset))
1701
1702 def GetIssueProperties(self):
1703 if self._props is None:
1704 issue = self.GetIssue()
1705 if not issue:
1706 self._props = {}
1707 else:
1708 self._props = self.RpcServer().get_issue_properties(issue, True)
1709 return self._props
1710
1711 def GetApprovingReviewers(self):
1712 return get_approving_reviewers(self.GetIssueProperties())
1713
1714 def AddComment(self, message):
1715 return self.RpcServer().add_comment(self.GetIssue(), message)
1716
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001717 def GetStatus(self):
1718 """Apply a rough heuristic to give a simple summary of an issue's review
1719 or CQ status, assuming adherence to a common workflow.
1720
1721 Returns None if no issue for this branch, or one of the following keywords:
1722 * 'error' - error from review tool (including deleted issues)
1723 * 'unsent' - not sent for review
1724 * 'waiting' - waiting for review
1725 * 'reply' - waiting for owner to reply to review
1726 * 'lgtm' - LGTM from at least one approved reviewer
1727 * 'commit' - in the commit queue
1728 * 'closed' - closed
1729 """
1730 if not self.GetIssue():
1731 return None
1732
1733 try:
1734 props = self.GetIssueProperties()
1735 except urllib2.HTTPError:
1736 return 'error'
1737
1738 if props.get('closed'):
1739 # Issue is closed.
1740 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001741 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001742 # Issue is in the commit queue.
1743 return 'commit'
1744
1745 try:
1746 reviewers = self.GetApprovingReviewers()
1747 except urllib2.HTTPError:
1748 return 'error'
1749
1750 if reviewers:
1751 # Was LGTM'ed.
1752 return 'lgtm'
1753
1754 messages = props.get('messages') or []
1755
tandrii9d2c7a32016-06-22 03:42:45 -07001756 # Skip CQ messages that don't require owner's action.
1757 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1758 if 'Dry run:' in messages[-1]['text']:
1759 messages.pop()
1760 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1761 # This message always follows prior messages from CQ,
1762 # so skip this too.
1763 messages.pop()
1764 else:
1765 # This is probably a CQ messages warranting user attention.
1766 break
1767
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001768 if not messages:
1769 # No message was sent.
1770 return 'unsent'
1771 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001772 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001773 return 'reply'
1774 return 'waiting'
1775
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001777 return self.RpcServer().update_description(
1778 self.GetIssue(), self.description)
1779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001781 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001783 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001784 return self.SetFlags({flag: value})
1785
1786 def SetFlags(self, flags):
1787 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001788 """
phajdan.jr68598232016-08-10 03:28:28 -07001789 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001790 try:
tandrii4b233bd2016-07-06 03:50:29 -07001791 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001792 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001793 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001794 if e.code == 404:
1795 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1796 if e.code == 403:
1797 DieWithError(
1798 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001799 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001800 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001802 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 """Returns an upload.RpcServer() to access this review's rietveld instance.
1804 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001805 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001806 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001808 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001809 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001811 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001812 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001813 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
tandrii5d48c322016-08-18 16:19:37 -07001815 @classmethod
1816 def PatchsetConfigKey(cls):
1817 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818
tandrii5d48c322016-08-18 16:19:37 -07001819 @classmethod
1820 def CodereviewServerConfigKey(cls):
1821 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001822
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001823 def GetRieveldObjForPresubmit(self):
1824 return self.RpcServer()
1825
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001826 def SetCQState(self, new_state):
1827 props = self.GetIssueProperties()
1828 if props.get('private'):
1829 DieWithError('Cannot set-commit on private issue')
1830
1831 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001832 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001833 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001834 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001835 else:
tandrii4b233bd2016-07-06 03:50:29 -07001836 assert new_state == _CQState.DRY_RUN
1837 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001838
1839
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001840 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1841 directory):
1842 # TODO(maruel): Use apply_issue.py
1843
1844 # PatchIssue should never be called with a dirty tree. It is up to the
1845 # caller to check this, but just in case we assert here since the
1846 # consequences of the caller not checking this could be dire.
1847 assert(not git_common.is_dirty_git_tree('apply'))
1848 assert(parsed_issue_arg.valid)
1849 self._changelist.issue = parsed_issue_arg.issue
1850 if parsed_issue_arg.hostname:
1851 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1852
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001853 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1854 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001855 assert parsed_issue_arg.patchset
1856 patchset = parsed_issue_arg.patchset
1857 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1858 else:
1859 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1860 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1861
1862 # Switch up to the top-level directory, if necessary, in preparation for
1863 # applying the patch.
1864 top = settings.GetRelativeRoot()
1865 if top:
1866 os.chdir(top)
1867
1868 # Git patches have a/ at the beginning of source paths. We strip that out
1869 # with a sed script rather than the -p flag to patch so we can feed either
1870 # Git or svn-style patches into the same apply command.
1871 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1872 try:
1873 patch_data = subprocess2.check_output(
1874 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1875 except subprocess2.CalledProcessError:
1876 DieWithError('Git patch mungling failed.')
1877 logging.info(patch_data)
1878
1879 # We use "git apply" to apply the patch instead of "patch" so that we can
1880 # pick up file adds.
1881 # The --index flag means: also insert into the index (so we catch adds).
1882 cmd = ['git', 'apply', '--index', '-p0']
1883 if directory:
1884 cmd.extend(('--directory', directory))
1885 if reject:
1886 cmd.append('--reject')
1887 elif IsGitVersionAtLeast('1.7.12'):
1888 cmd.append('--3way')
1889 try:
1890 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1891 stdin=patch_data, stdout=subprocess2.VOID)
1892 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001893 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001894 return 1
1895
1896 # If we had an issue, commit the current state and register the issue.
1897 if not nocommit:
1898 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1899 'patch from issue %(i)s at patchset '
1900 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1901 % {'i': self.GetIssue(), 'p': patchset})])
1902 self.SetIssue(self.GetIssue())
1903 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001904 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001905 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001906 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001907 return 0
1908
1909 @staticmethod
1910 def ParseIssueURL(parsed_url):
1911 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1912 return None
wychen3c1c1722016-08-04 11:46:36 -07001913 # Rietveld patch: https://domain/<number>/#ps<patchset>
1914 match = re.match(r'/(\d+)/$', parsed_url.path)
1915 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1916 if match and match2:
1917 return _RietveldParsedIssueNumberArgument(
1918 issue=int(match.group(1)),
1919 patchset=int(match2.group(1)),
1920 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001921 # Typical url: https://domain/<issue_number>[/[other]]
1922 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1923 if match:
1924 return _RietveldParsedIssueNumberArgument(
1925 issue=int(match.group(1)),
1926 hostname=parsed_url.netloc)
1927 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1928 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1929 if match:
1930 return _RietveldParsedIssueNumberArgument(
1931 issue=int(match.group(1)),
1932 patchset=int(match.group(2)),
1933 hostname=parsed_url.netloc,
1934 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1935 return None
1936
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001937 def CMDUploadChange(self, options, args, change):
1938 """Upload the patch to Rietveld."""
1939 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1940 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001941 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1942 if options.emulate_svn_auto_props:
1943 upload_args.append('--emulate_svn_auto_props')
1944
1945 change_desc = None
1946
1947 if options.email is not None:
1948 upload_args.extend(['--email', options.email])
1949
1950 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001951 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001952 upload_args.extend(['--title', options.title])
1953 if options.message:
1954 upload_args.extend(['--message', options.message])
1955 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001956 print('This branch is associated with issue %s. '
1957 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001958 else:
nodirca166002016-06-27 10:59:51 -07001959 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001960 upload_args.extend(['--title', options.title])
1961 message = (options.title or options.message or
1962 CreateDescriptionFromLog(args))
1963 change_desc = ChangeDescription(message)
1964 if options.reviewers or options.tbr_owners:
1965 change_desc.update_reviewers(options.reviewers,
1966 options.tbr_owners,
1967 change)
1968 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001969 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001970
1971 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001972 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001973 return 1
1974
1975 upload_args.extend(['--message', change_desc.description])
1976 if change_desc.get_reviewers():
1977 upload_args.append('--reviewers=%s' % ','.join(
1978 change_desc.get_reviewers()))
1979 if options.send_mail:
1980 if not change_desc.get_reviewers():
1981 DieWithError("Must specify reviewers to send email.")
1982 upload_args.append('--send_mail')
1983
1984 # We check this before applying rietveld.private assuming that in
1985 # rietveld.cc only addresses which we can send private CLs to are listed
1986 # if rietveld.private is set, and so we should ignore rietveld.cc only
1987 # when --private is specified explicitly on the command line.
1988 if options.private:
1989 logging.warn('rietveld.cc is ignored since private flag is specified. '
1990 'You need to review and add them manually if necessary.')
1991 cc = self.GetCCListWithoutDefault()
1992 else:
1993 cc = self.GetCCList()
1994 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1995 if cc:
1996 upload_args.extend(['--cc', cc])
1997
1998 if options.private or settings.GetDefaultPrivateFlag() == "True":
1999 upload_args.append('--private')
2000
2001 upload_args.extend(['--git_similarity', str(options.similarity)])
2002 if not options.find_copies:
2003 upload_args.extend(['--git_no_find_copies'])
2004
2005 # Include the upstream repo's URL in the change -- this is useful for
2006 # projects that have their source spread across multiple repos.
2007 remote_url = self.GetGitBaseUrlFromConfig()
2008 if not remote_url:
2009 if settings.GetIsGitSvn():
2010 remote_url = self.GetGitSvnRemoteUrl()
2011 else:
2012 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2013 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2014 self.GetUpstreamBranch().split('/')[-1])
2015 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002016 remote, remote_branch = self.GetRemoteBranch()
2017 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2018 settings.GetPendingRefPrefix())
2019 if target_ref:
2020 upload_args.extend(['--target_ref', target_ref])
2021
2022 # Look for dependent patchsets. See crbug.com/480453 for more details.
2023 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2024 upstream_branch = ShortBranchName(upstream_branch)
2025 if remote is '.':
2026 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002027 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002028 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002029 print()
2030 print('Skipping dependency patchset upload because git config '
2031 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2032 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002033 else:
2034 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002035 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002036 auth_config=auth_config)
2037 branch_cl_issue_url = branch_cl.GetIssueURL()
2038 branch_cl_issue = branch_cl.GetIssue()
2039 branch_cl_patchset = branch_cl.GetPatchset()
2040 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2041 upload_args.extend(
2042 ['--depends_on_patchset', '%s:%s' % (
2043 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002044 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002045 '\n'
2046 'The current branch (%s) is tracking a local branch (%s) with '
2047 'an associated CL.\n'
2048 'Adding %s/#ps%s as a dependency patchset.\n'
2049 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2050 branch_cl_patchset))
2051
2052 project = settings.GetProject()
2053 if project:
2054 upload_args.extend(['--project', project])
2055
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002056 try:
2057 upload_args = ['upload'] + upload_args + args
2058 logging.info('upload.RealMain(%s)', upload_args)
2059 issue, patchset = upload.RealMain(upload_args)
2060 issue = int(issue)
2061 patchset = int(patchset)
2062 except KeyboardInterrupt:
2063 sys.exit(1)
2064 except:
2065 # If we got an exception after the user typed a description for their
2066 # change, back up the description before re-raising.
2067 if change_desc:
2068 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2069 print('\nGot exception while uploading -- saving description to %s\n' %
2070 backup_path)
2071 backup_file = open(backup_path, 'w')
2072 backup_file.write(change_desc.description)
2073 backup_file.close()
2074 raise
2075
2076 if not self.GetIssue():
2077 self.SetIssue(issue)
2078 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002079 return 0
2080
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081
2082class _GerritChangelistImpl(_ChangelistCodereviewBase):
2083 def __init__(self, changelist, auth_config=None):
2084 # auth_config is Rietveld thing, kept here to preserve interface only.
2085 super(_GerritChangelistImpl, self).__init__(changelist)
2086 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002087 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002088 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002089 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002090
2091 def _GetGerritHost(self):
2092 # Lazy load of configs.
2093 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002094 if self._gerrit_host and '.' not in self._gerrit_host:
2095 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2096 # This happens for internal stuff http://crbug.com/614312.
2097 parsed = urlparse.urlparse(self.GetRemoteUrl())
2098 if parsed.scheme == 'sso':
2099 print('WARNING: using non https URLs for remote is likely broken\n'
2100 ' Your current remote is: %s' % self.GetRemoteUrl())
2101 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2102 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002103 return self._gerrit_host
2104
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002105 def _GetGitHost(self):
2106 """Returns git host to be used when uploading change to Gerrit."""
2107 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2108
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002109 def GetCodereviewServer(self):
2110 if not self._gerrit_server:
2111 # If we're on a branch then get the server potentially associated
2112 # with that branch.
2113 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002114 self._gerrit_server = self._GitGetBranchConfigValue(
2115 self.CodereviewServerConfigKey())
2116 if self._gerrit_server:
2117 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002118 if not self._gerrit_server:
2119 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2120 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002121 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002122 parts[0] = parts[0] + '-review'
2123 self._gerrit_host = '.'.join(parts)
2124 self._gerrit_server = 'https://%s' % self._gerrit_host
2125 return self._gerrit_server
2126
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002127 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002128 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002129 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002130
tandrii5d48c322016-08-18 16:19:37 -07002131 @classmethod
2132 def PatchsetConfigKey(cls):
2133 return 'gerritpatchset'
2134
2135 @classmethod
2136 def CodereviewServerConfigKey(cls):
2137 return 'gerritserver'
2138
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002139 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002140 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002141 if settings.GetGerritSkipEnsureAuthenticated():
2142 # For projects with unusual authentication schemes.
2143 # See http://crbug.com/603378.
2144 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002145 # Lazy-loader to identify Gerrit and Git hosts.
2146 if gerrit_util.GceAuthenticator.is_gce():
2147 return
2148 self.GetCodereviewServer()
2149 git_host = self._GetGitHost()
2150 assert self._gerrit_server and self._gerrit_host
2151 cookie_auth = gerrit_util.CookiesAuthenticator()
2152
2153 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2154 git_auth = cookie_auth.get_auth_header(git_host)
2155 if gerrit_auth and git_auth:
2156 if gerrit_auth == git_auth:
2157 return
2158 print((
2159 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2160 ' Check your %s or %s file for credentials of hosts:\n'
2161 ' %s\n'
2162 ' %s\n'
2163 ' %s') %
2164 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2165 git_host, self._gerrit_host,
2166 cookie_auth.get_new_password_message(git_host)))
2167 if not force:
2168 ask_for_data('If you know what you are doing, press Enter to continue, '
2169 'Ctrl+C to abort.')
2170 return
2171 else:
2172 missing = (
2173 [] if gerrit_auth else [self._gerrit_host] +
2174 [] if git_auth else [git_host])
2175 DieWithError('Credentials for the following hosts are required:\n'
2176 ' %s\n'
2177 'These are read from %s (or legacy %s)\n'
2178 '%s' % (
2179 '\n '.join(missing),
2180 cookie_auth.get_gitcookies_path(),
2181 cookie_auth.get_netrc_path(),
2182 cookie_auth.get_new_password_message(git_host)))
2183
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002184 def _PostUnsetIssueProperties(self):
2185 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002186 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002187
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002188 def GetRieveldObjForPresubmit(self):
2189 class ThisIsNotRietveldIssue(object):
2190 def __nonzero__(self):
2191 # This is a hack to make presubmit_support think that rietveld is not
2192 # defined, yet still ensure that calls directly result in a decent
2193 # exception message below.
2194 return False
2195
2196 def __getattr__(self, attr):
2197 print(
2198 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2199 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2200 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2201 'or use Rietveld for codereview.\n'
2202 'See also http://crbug.com/579160.' % attr)
2203 raise NotImplementedError()
2204 return ThisIsNotRietveldIssue()
2205
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002206 def GetGerritObjForPresubmit(self):
2207 return presubmit_support.GerritAccessor(self._GetGerritHost())
2208
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002209 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002210 """Apply a rough heuristic to give a simple summary of an issue's review
2211 or CQ status, assuming adherence to a common workflow.
2212
2213 Returns None if no issue for this branch, or one of the following keywords:
2214 * 'error' - error from review tool (including deleted issues)
2215 * 'unsent' - no reviewers added
2216 * 'waiting' - waiting for review
2217 * 'reply' - waiting for owner to reply to review
2218 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2219 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2220 * 'commit' - in the commit queue
2221 * 'closed' - abandoned
2222 """
2223 if not self.GetIssue():
2224 return None
2225
2226 try:
2227 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2228 except httplib.HTTPException:
2229 return 'error'
2230
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002231 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002232 return 'closed'
2233
2234 cq_label = data['labels'].get('Commit-Queue', {})
2235 if cq_label:
2236 # Vote value is a stringified integer, which we expect from 0 to 2.
2237 vote_value = cq_label.get('value', '0')
2238 vote_text = cq_label.get('values', {}).get(vote_value, '')
2239 if vote_text.lower() == 'commit':
2240 return 'commit'
2241
2242 lgtm_label = data['labels'].get('Code-Review', {})
2243 if lgtm_label:
2244 if 'rejected' in lgtm_label:
2245 return 'not lgtm'
2246 if 'approved' in lgtm_label:
2247 return 'lgtm'
2248
2249 if not data.get('reviewers', {}).get('REVIEWER', []):
2250 return 'unsent'
2251
2252 messages = data.get('messages', [])
2253 if messages:
2254 owner = data['owner'].get('_account_id')
2255 last_message_author = messages[-1].get('author', {}).get('_account_id')
2256 if owner != last_message_author:
2257 # Some reply from non-owner.
2258 return 'reply'
2259
2260 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002261
2262 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002263 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002264 return data['revisions'][data['current_revision']]['_number']
2265
2266 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002267 data = self._GetChangeDetail(['CURRENT_REVISION'])
2268 current_rev = data['current_revision']
2269 url = data['revisions'][current_rev]['fetch']['http']['url']
2270 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002271
2272 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002273 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2274 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002275
2276 def CloseIssue(self):
2277 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2278
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002279 def GetApprovingReviewers(self):
2280 """Returns a list of reviewers approving the change.
2281
2282 Note: not necessarily committers.
2283 """
2284 raise NotImplementedError()
2285
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002286 def SubmitIssue(self, wait_for_merge=True):
2287 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2288 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002289
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002290 def _GetChangeDetail(self, options=None, issue=None):
2291 options = options or []
2292 issue = issue or self.GetIssue()
2293 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002294 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2295 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002296
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002297 def CMDLand(self, force, bypass_hooks, verbose):
2298 if git_common.is_dirty_git_tree('land'):
2299 return 1
tandriid60367b2016-06-22 05:25:12 -07002300 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2301 if u'Commit-Queue' in detail.get('labels', {}):
2302 if not force:
2303 ask_for_data('\nIt seems this repository has a Commit Queue, '
2304 'which can test and land changes for you. '
2305 'Are you sure you wish to bypass it?\n'
2306 'Press Enter to continue, Ctrl+C to abort.')
2307
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002308 differs = True
tandriic4344b52016-08-29 06:04:54 -07002309 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002310 # Note: git diff outputs nothing if there is no diff.
2311 if not last_upload or RunGit(['diff', last_upload]).strip():
2312 print('WARNING: some changes from local branch haven\'t been uploaded')
2313 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002314 if detail['current_revision'] == last_upload:
2315 differs = False
2316 else:
2317 print('WARNING: local branch contents differ from latest uploaded '
2318 'patchset')
2319 if differs:
2320 if not force:
2321 ask_for_data(
2322 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2323 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2324 elif not bypass_hooks:
2325 hook_results = self.RunHook(
2326 committing=True,
2327 may_prompt=not force,
2328 verbose=verbose,
2329 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2330 if not hook_results.should_continue():
2331 return 1
2332
2333 self.SubmitIssue(wait_for_merge=True)
2334 print('Issue %s has been submitted.' % self.GetIssueURL())
2335 return 0
2336
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002337 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2338 directory):
2339 assert not reject
2340 assert not nocommit
2341 assert not directory
2342 assert parsed_issue_arg.valid
2343
2344 self._changelist.issue = parsed_issue_arg.issue
2345
2346 if parsed_issue_arg.hostname:
2347 self._gerrit_host = parsed_issue_arg.hostname
2348 self._gerrit_server = 'https://%s' % self._gerrit_host
2349
2350 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2351
2352 if not parsed_issue_arg.patchset:
2353 # Use current revision by default.
2354 revision_info = detail['revisions'][detail['current_revision']]
2355 patchset = int(revision_info['_number'])
2356 else:
2357 patchset = parsed_issue_arg.patchset
2358 for revision_info in detail['revisions'].itervalues():
2359 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2360 break
2361 else:
2362 DieWithError('Couldn\'t find patchset %i in issue %i' %
2363 (parsed_issue_arg.patchset, self.GetIssue()))
2364
2365 fetch_info = revision_info['fetch']['http']
2366 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2367 RunGit(['cherry-pick', 'FETCH_HEAD'])
2368 self.SetIssue(self.GetIssue())
2369 self.SetPatchset(patchset)
2370 print('Committed patch for issue %i pathset %i locally' %
2371 (self.GetIssue(), self.GetPatchset()))
2372 return 0
2373
2374 @staticmethod
2375 def ParseIssueURL(parsed_url):
2376 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2377 return None
2378 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2379 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2380 # Short urls like https://domain/<issue_number> can be used, but don't allow
2381 # specifying the patchset (you'd 404), but we allow that here.
2382 if parsed_url.path == '/':
2383 part = parsed_url.fragment
2384 else:
2385 part = parsed_url.path
2386 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2387 if match:
2388 return _ParsedIssueNumberArgument(
2389 issue=int(match.group(2)),
2390 patchset=int(match.group(4)) if match.group(4) else None,
2391 hostname=parsed_url.netloc)
2392 return None
2393
tandrii16e0b4e2016-06-07 10:34:28 -07002394 def _GerritCommitMsgHookCheck(self, offer_removal):
2395 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2396 if not os.path.exists(hook):
2397 return
2398 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2399 # custom developer made one.
2400 data = gclient_utils.FileRead(hook)
2401 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2402 return
2403 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002404 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002405 'and may interfere with it in subtle ways.\n'
2406 'We recommend you remove the commit-msg hook.')
2407 if offer_removal:
2408 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2409 if reply.lower().startswith('y'):
2410 gclient_utils.rm_file_or_tree(hook)
2411 print('Gerrit commit-msg hook removed.')
2412 else:
2413 print('OK, will keep Gerrit commit-msg hook in place.')
2414
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002415 def CMDUploadChange(self, options, args, change):
2416 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002417 if options.squash and options.no_squash:
2418 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002419
2420 if not options.squash and not options.no_squash:
2421 # Load default for user, repo, squash=true, in this order.
2422 options.squash = settings.GetSquashGerritUploads()
2423 elif options.no_squash:
2424 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002425
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002426 # We assume the remote called "origin" is the one we want.
2427 # It is probably not worthwhile to support different workflows.
2428 gerrit_remote = 'origin'
2429
2430 remote, remote_branch = self.GetRemoteBranch()
2431 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2432 pending_prefix='')
2433
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002434 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002435 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002436 if self.GetIssue():
2437 # Try to get the message from a previous upload.
2438 message = self.GetDescription()
2439 if not message:
2440 DieWithError(
2441 'failed to fetch description from current Gerrit issue %d\n'
2442 '%s' % (self.GetIssue(), self.GetIssueURL()))
2443 change_id = self._GetChangeDetail()['change_id']
2444 while True:
2445 footer_change_ids = git_footers.get_footer_change_id(message)
2446 if footer_change_ids == [change_id]:
2447 break
2448 if not footer_change_ids:
2449 message = git_footers.add_footer_change_id(message, change_id)
2450 print('WARNING: appended missing Change-Id to issue description')
2451 continue
2452 # There is already a valid footer but with different or several ids.
2453 # Doing this automatically is non-trivial as we don't want to lose
2454 # existing other footers, yet we want to append just 1 desired
2455 # Change-Id. Thus, just create a new footer, but let user verify the
2456 # new description.
2457 message = '%s\n\nChange-Id: %s' % (message, change_id)
2458 print(
2459 'WARNING: issue %s has Change-Id footer(s):\n'
2460 ' %s\n'
2461 'but issue has Change-Id %s, according to Gerrit.\n'
2462 'Please, check the proposed correction to the description, '
2463 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2464 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2465 change_id))
2466 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2467 if not options.force:
2468 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002469 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 message = change_desc.description
2471 if not message:
2472 DieWithError("Description is empty. Aborting...")
2473 # Continue the while loop.
2474 # Sanity check of this code - we should end up with proper message
2475 # footer.
2476 assert [change_id] == git_footers.get_footer_change_id(message)
2477 change_desc = ChangeDescription(message)
2478 else:
2479 change_desc = ChangeDescription(
2480 options.message or CreateDescriptionFromLog(args))
2481 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002482 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002483 if not change_desc.description:
2484 DieWithError("Description is empty. Aborting...")
2485 message = change_desc.description
2486 change_ids = git_footers.get_footer_change_id(message)
2487 if len(change_ids) > 1:
2488 DieWithError('too many Change-Id footers, at most 1 allowed.')
2489 if not change_ids:
2490 # Generate the Change-Id automatically.
2491 message = git_footers.add_footer_change_id(
2492 message, GenerateGerritChangeId(message))
2493 change_desc.set_description(message)
2494 change_ids = git_footers.get_footer_change_id(message)
2495 assert len(change_ids) == 1
2496 change_id = change_ids[0]
2497
2498 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2499 if remote is '.':
2500 # If our upstream branch is local, we base our squashed commit on its
2501 # squashed version.
2502 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2503 # Check the squashed hash of the parent.
2504 parent = RunGit(['config',
2505 'branch.%s.gerritsquashhash' % upstream_branch_name],
2506 error_ok=True).strip()
2507 # Verify that the upstream branch has been uploaded too, otherwise
2508 # Gerrit will create additional CLs when uploading.
2509 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2510 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002511 DieWithError(
2512 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002513 'Note: maybe you\'ve uploaded it with --no-squash. '
2514 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002515 ' git cl upload --squash\n' % upstream_branch_name)
2516 else:
2517 parent = self.GetCommonAncestorWithUpstream()
2518
2519 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2520 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2521 '-m', message]).strip()
2522 else:
2523 change_desc = ChangeDescription(
2524 options.message or CreateDescriptionFromLog(args))
2525 if not change_desc.description:
2526 DieWithError("Description is empty. Aborting...")
2527
2528 if not git_footers.get_footer_change_id(change_desc.description):
2529 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002530 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2531 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002532 ref_to_push = 'HEAD'
2533 parent = '%s/%s' % (gerrit_remote, branch)
2534 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2535
2536 assert change_desc
2537 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2538 ref_to_push)]).splitlines()
2539 if len(commits) > 1:
2540 print('WARNING: This will upload %d commits. Run the following command '
2541 'to see which commits will be uploaded: ' % len(commits))
2542 print('git log %s..%s' % (parent, ref_to_push))
2543 print('You can also use `git squash-branch` to squash these into a '
2544 'single commit.')
2545 ask_for_data('About to upload; enter to confirm.')
2546
2547 if options.reviewers or options.tbr_owners:
2548 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2549 change)
2550
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002551 # Extra options that can be specified at push time. Doc:
2552 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2553 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002554 if change_desc.get_reviewers(tbr_only=True):
2555 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2556 refspec_opts.append('l=Code-Review+1')
2557
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002558 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002559 if not re.match(r'^[\w ]+$', options.title):
2560 options.title = re.sub(r'[^\w ]', '', options.title)
2561 print('WARNING: Patchset title may only contain alphanumeric chars '
2562 'and spaces. Cleaned up title:\n%s' % options.title)
2563 if not options.force:
2564 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002565 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2566 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002567 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2568
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002569 if options.send_mail:
2570 if not change_desc.get_reviewers():
2571 DieWithError('Must specify reviewers to send email.')
2572 refspec_opts.append('notify=ALL')
2573 else:
2574 refspec_opts.append('notify=NONE')
2575
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002576 cc = self.GetCCList().split(',')
2577 if options.cc:
2578 cc.extend(options.cc)
2579 cc = filter(None, cc)
2580 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002581 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002582
tandrii99a72f22016-08-17 14:33:24 -07002583 reviewers = change_desc.get_reviewers()
2584 if reviewers:
2585 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002586
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002587 refspec_suffix = ''
2588 if refspec_opts:
2589 refspec_suffix = '%' + ','.join(refspec_opts)
2590 assert ' ' not in refspec_suffix, (
2591 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002592 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002593
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002594 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002595 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002596 print_stdout=True,
2597 # Flush after every line: useful for seeing progress when running as
2598 # recipe.
2599 filter_fn=lambda _: sys.stdout.flush())
2600
2601 if options.squash:
2602 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2603 change_numbers = [m.group(1)
2604 for m in map(regex.match, push_stdout.splitlines())
2605 if m]
2606 if len(change_numbers) != 1:
2607 DieWithError(
2608 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2609 'Change-Id: %s') % (len(change_numbers), change_id))
2610 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002611 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002612 return 0
2613
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002614 def _AddChangeIdToCommitMessage(self, options, args):
2615 """Re-commits using the current message, assumes the commit hook is in
2616 place.
2617 """
2618 log_desc = options.message or CreateDescriptionFromLog(args)
2619 git_command = ['commit', '--amend', '-m', log_desc]
2620 RunGit(git_command)
2621 new_log_desc = CreateDescriptionFromLog(args)
2622 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002623 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002624 return new_log_desc
2625 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002626 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002627
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002628 def SetCQState(self, new_state):
2629 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002630 vote_map = {
2631 _CQState.NONE: 0,
2632 _CQState.DRY_RUN: 1,
2633 _CQState.COMMIT : 2,
2634 }
2635 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2636 labels={'Commit-Queue': vote_map[new_state]})
2637
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002638
2639_CODEREVIEW_IMPLEMENTATIONS = {
2640 'rietveld': _RietveldChangelistImpl,
2641 'gerrit': _GerritChangelistImpl,
2642}
2643
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002644
iannuccie53c9352016-08-17 14:40:40 -07002645def _add_codereview_issue_select_options(parser, extra=""):
2646 _add_codereview_select_options(parser)
2647
2648 text = ('Operate on this issue number instead of the current branch\'s '
2649 'implicit issue.')
2650 if extra:
2651 text += ' '+extra
2652 parser.add_option('-i', '--issue', type=int, help=text)
2653
2654
2655def _process_codereview_issue_select_options(parser, options):
2656 _process_codereview_select_options(parser, options)
2657 if options.issue is not None and not options.forced_codereview:
2658 parser.error('--issue must be specified with either --rietveld or --gerrit')
2659
2660
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002661def _add_codereview_select_options(parser):
2662 """Appends --gerrit and --rietveld options to force specific codereview."""
2663 parser.codereview_group = optparse.OptionGroup(
2664 parser, 'EXPERIMENTAL! Codereview override options')
2665 parser.add_option_group(parser.codereview_group)
2666 parser.codereview_group.add_option(
2667 '--gerrit', action='store_true',
2668 help='Force the use of Gerrit for codereview')
2669 parser.codereview_group.add_option(
2670 '--rietveld', action='store_true',
2671 help='Force the use of Rietveld for codereview')
2672
2673
2674def _process_codereview_select_options(parser, options):
2675 if options.gerrit and options.rietveld:
2676 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2677 options.forced_codereview = None
2678 if options.gerrit:
2679 options.forced_codereview = 'gerrit'
2680 elif options.rietveld:
2681 options.forced_codereview = 'rietveld'
2682
2683
tandriif9aefb72016-07-01 09:06:51 -07002684def _get_bug_line_values(default_project, bugs):
2685 """Given default_project and comma separated list of bugs, yields bug line
2686 values.
2687
2688 Each bug can be either:
2689 * a number, which is combined with default_project
2690 * string, which is left as is.
2691
2692 This function may produce more than one line, because bugdroid expects one
2693 project per line.
2694
2695 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2696 ['v8:123', 'chromium:789']
2697 """
2698 default_bugs = []
2699 others = []
2700 for bug in bugs.split(','):
2701 bug = bug.strip()
2702 if bug:
2703 try:
2704 default_bugs.append(int(bug))
2705 except ValueError:
2706 others.append(bug)
2707
2708 if default_bugs:
2709 default_bugs = ','.join(map(str, default_bugs))
2710 if default_project:
2711 yield '%s:%s' % (default_project, default_bugs)
2712 else:
2713 yield default_bugs
2714 for other in sorted(others):
2715 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2716 yield other
2717
2718
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002719class ChangeDescription(object):
2720 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002721 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002722 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002723
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002724 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002725 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002726
agable@chromium.org42c20792013-09-12 17:34:49 +00002727 @property # www.logilab.org/ticket/89786
2728 def description(self): # pylint: disable=E0202
2729 return '\n'.join(self._description_lines)
2730
2731 def set_description(self, desc):
2732 if isinstance(desc, basestring):
2733 lines = desc.splitlines()
2734 else:
2735 lines = [line.rstrip() for line in desc]
2736 while lines and not lines[0]:
2737 lines.pop(0)
2738 while lines and not lines[-1]:
2739 lines.pop(-1)
2740 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002741
piman@chromium.org336f9122014-09-04 02:16:55 +00002742 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002743 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002744 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002745 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002746 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002747 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002748
agable@chromium.org42c20792013-09-12 17:34:49 +00002749 # Get the set of R= and TBR= lines and remove them from the desciption.
2750 regexp = re.compile(self.R_LINE)
2751 matches = [regexp.match(line) for line in self._description_lines]
2752 new_desc = [l for i, l in enumerate(self._description_lines)
2753 if not matches[i]]
2754 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002755
agable@chromium.org42c20792013-09-12 17:34:49 +00002756 # Construct new unified R= and TBR= lines.
2757 r_names = []
2758 tbr_names = []
2759 for match in matches:
2760 if not match:
2761 continue
2762 people = cleanup_list([match.group(2).strip()])
2763 if match.group(1) == 'TBR':
2764 tbr_names.extend(people)
2765 else:
2766 r_names.extend(people)
2767 for name in r_names:
2768 if name not in reviewers:
2769 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002770 if add_owners_tbr:
2771 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002772 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002773 all_reviewers = set(tbr_names + reviewers)
2774 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2775 all_reviewers)
2776 tbr_names.extend(owners_db.reviewers_for(missing_files,
2777 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002778 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2779 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2780
2781 # Put the new lines in the description where the old first R= line was.
2782 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2783 if 0 <= line_loc < len(self._description_lines):
2784 if new_tbr_line:
2785 self._description_lines.insert(line_loc, new_tbr_line)
2786 if new_r_line:
2787 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002788 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002789 if new_r_line:
2790 self.append_footer(new_r_line)
2791 if new_tbr_line:
2792 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002793
tandriif9aefb72016-07-01 09:06:51 -07002794 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002795 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002796 self.set_description([
2797 '# Enter a description of the change.',
2798 '# This will be displayed on the codereview site.',
2799 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002800 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002801 '--------------------',
2802 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002803
agable@chromium.org42c20792013-09-12 17:34:49 +00002804 regexp = re.compile(self.BUG_LINE)
2805 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002806 prefix = settings.GetBugPrefix()
2807 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2808 for value in values:
2809 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2810 self.append_footer('BUG=%s' % value)
2811
agable@chromium.org42c20792013-09-12 17:34:49 +00002812 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002813 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002814 if not content:
2815 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002816 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002817
2818 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002819 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2820 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002821 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002822 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002823
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002824 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002825 """Adds a footer line to the description.
2826
2827 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2828 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2829 that Gerrit footers are always at the end.
2830 """
2831 parsed_footer_line = git_footers.parse_footer(line)
2832 if parsed_footer_line:
2833 # Line is a gerrit footer in the form: Footer-Key: any value.
2834 # Thus, must be appended observing Gerrit footer rules.
2835 self.set_description(
2836 git_footers.add_footer(self.description,
2837 key=parsed_footer_line[0],
2838 value=parsed_footer_line[1]))
2839 return
2840
2841 if not self._description_lines:
2842 self._description_lines.append(line)
2843 return
2844
2845 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2846 if gerrit_footers:
2847 # git_footers.split_footers ensures that there is an empty line before
2848 # actual (gerrit) footers, if any. We have to keep it that way.
2849 assert top_lines and top_lines[-1] == ''
2850 top_lines, separator = top_lines[:-1], top_lines[-1:]
2851 else:
2852 separator = [] # No need for separator if there are no gerrit_footers.
2853
2854 prev_line = top_lines[-1] if top_lines else ''
2855 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2856 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2857 top_lines.append('')
2858 top_lines.append(line)
2859 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002860
tandrii99a72f22016-08-17 14:33:24 -07002861 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002862 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002863 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002864 reviewers = [match.group(2).strip()
2865 for match in matches
2866 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002867 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002868
2869
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002870def get_approving_reviewers(props):
2871 """Retrieves the reviewers that approved a CL from the issue properties with
2872 messages.
2873
2874 Note that the list may contain reviewers that are not committer, thus are not
2875 considered by the CQ.
2876 """
2877 return sorted(
2878 set(
2879 message['sender']
2880 for message in props['messages']
2881 if message['approval'] and message['sender'] in props['reviewers']
2882 )
2883 )
2884
2885
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002886def FindCodereviewSettingsFile(filename='codereview.settings'):
2887 """Finds the given file starting in the cwd and going up.
2888
2889 Only looks up to the top of the repository unless an
2890 'inherit-review-settings-ok' file exists in the root of the repository.
2891 """
2892 inherit_ok_file = 'inherit-review-settings-ok'
2893 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002894 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2896 root = '/'
2897 while True:
2898 if filename in os.listdir(cwd):
2899 if os.path.isfile(os.path.join(cwd, filename)):
2900 return open(os.path.join(cwd, filename))
2901 if cwd == root:
2902 break
2903 cwd = os.path.dirname(cwd)
2904
2905
2906def LoadCodereviewSettingsFromFile(fileobj):
2907 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002908 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002909
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002910 def SetProperty(name, setting, unset_error_ok=False):
2911 fullname = 'rietveld.' + name
2912 if setting in keyvals:
2913 RunGit(['config', fullname, keyvals[setting]])
2914 else:
2915 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2916
2917 SetProperty('server', 'CODE_REVIEW_SERVER')
2918 # Only server setting is required. Other settings can be absent.
2919 # In that case, we ignore errors raised during option deletion attempt.
2920 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002921 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002922 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2923 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002924 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002925 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002926 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2927 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002928 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002929 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002930 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002931 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2932 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002933
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002934 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002935 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002936
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002937 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002938 RunGit(['config', 'gerrit.squash-uploads',
2939 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002940
tandrii@chromium.org28253532016-04-14 13:46:56 +00002941 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002942 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002943 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002945 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2946 #should be of the form
2947 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2948 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2949 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2950 keyvals['ORIGIN_URL_CONFIG']])
2951
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002952
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002953def urlretrieve(source, destination):
2954 """urllib is broken for SSL connections via a proxy therefore we
2955 can't use urllib.urlretrieve()."""
2956 with open(destination, 'w') as f:
2957 f.write(urllib2.urlopen(source).read())
2958
2959
ukai@chromium.org712d6102013-11-27 00:52:58 +00002960def hasSheBang(fname):
2961 """Checks fname is a #! script."""
2962 with open(fname) as f:
2963 return f.read(2).startswith('#!')
2964
2965
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002966# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2967def DownloadHooks(*args, **kwargs):
2968 pass
2969
2970
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002971def DownloadGerritHook(force):
2972 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002973
2974 Args:
2975 force: True to update hooks. False to install hooks if not present.
2976 """
2977 if not settings.GetIsGerrit():
2978 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002979 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002980 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2981 if not os.access(dst, os.X_OK):
2982 if os.path.exists(dst):
2983 if not force:
2984 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002985 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002986 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002987 if not hasSheBang(dst):
2988 DieWithError('Not a script: %s\n'
2989 'You need to download from\n%s\n'
2990 'into .git/hooks/commit-msg and '
2991 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002992 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2993 except Exception:
2994 if os.path.exists(dst):
2995 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002996 DieWithError('\nFailed to download hooks.\n'
2997 'You need to download from\n%s\n'
2998 'into .git/hooks/commit-msg and '
2999 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003000
3001
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003002
3003def GetRietveldCodereviewSettingsInteractively():
3004 """Prompt the user for settings."""
3005 server = settings.GetDefaultServerUrl(error_ok=True)
3006 prompt = 'Rietveld server (host[:port])'
3007 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3008 newserver = ask_for_data(prompt + ':')
3009 if not server and not newserver:
3010 newserver = DEFAULT_SERVER
3011 if newserver:
3012 newserver = gclient_utils.UpgradeToHttps(newserver)
3013 if newserver != server:
3014 RunGit(['config', 'rietveld.server', newserver])
3015
3016 def SetProperty(initial, caption, name, is_url):
3017 prompt = caption
3018 if initial:
3019 prompt += ' ("x" to clear) [%s]' % initial
3020 new_val = ask_for_data(prompt + ':')
3021 if new_val == 'x':
3022 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3023 elif new_val:
3024 if is_url:
3025 new_val = gclient_utils.UpgradeToHttps(new_val)
3026 if new_val != initial:
3027 RunGit(['config', 'rietveld.' + name, new_val])
3028
3029 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3030 SetProperty(settings.GetDefaultPrivateFlag(),
3031 'Private flag (rietveld only)', 'private', False)
3032 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3033 'tree-status-url', False)
3034 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3035 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3036 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3037 'run-post-upload-hook', False)
3038
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003039@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003040def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003041 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003043 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003044 'For Gerrit, see http://crbug.com/603116.')
3045 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003046 parser.add_option('--activate-update', action='store_true',
3047 help='activate auto-updating [rietveld] section in '
3048 '.git/config')
3049 parser.add_option('--deactivate-update', action='store_true',
3050 help='deactivate auto-updating [rietveld] section in '
3051 '.git/config')
3052 options, args = parser.parse_args(args)
3053
3054 if options.deactivate_update:
3055 RunGit(['config', 'rietveld.autoupdate', 'false'])
3056 return
3057
3058 if options.activate_update:
3059 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3060 return
3061
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003062 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003063 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003064 return 0
3065
3066 url = args[0]
3067 if not url.endswith('codereview.settings'):
3068 url = os.path.join(url, 'codereview.settings')
3069
3070 # Load code review settings and download hooks (if available).
3071 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3072 return 0
3073
3074
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003075def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003076 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003077 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3078 branch = ShortBranchName(branchref)
3079 _, args = parser.parse_args(args)
3080 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003081 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003082 return RunGit(['config', 'branch.%s.base-url' % branch],
3083 error_ok=False).strip()
3084 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003085 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003086 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3087 error_ok=False).strip()
3088
3089
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003090def color_for_status(status):
3091 """Maps a Changelist status to color, for CMDstatus and other tools."""
3092 return {
3093 'unsent': Fore.RED,
3094 'waiting': Fore.BLUE,
3095 'reply': Fore.YELLOW,
3096 'lgtm': Fore.GREEN,
3097 'commit': Fore.MAGENTA,
3098 'closed': Fore.CYAN,
3099 'error': Fore.WHITE,
3100 }.get(status, Fore.WHITE)
3101
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003102
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003103def get_cl_statuses(changes, fine_grained, max_processes=None):
3104 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003105
3106 If fine_grained is true, this will fetch CL statuses from the server.
3107 Otherwise, simply indicate if there's a matching url for the given branches.
3108
3109 If max_processes is specified, it is used as the maximum number of processes
3110 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3111 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003112
3113 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003114 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003115 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003116 upload.verbosity = 0
3117
3118 if fine_grained:
3119 # Process one branch synchronously to work through authentication, then
3120 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003121 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003122 def fetch(cl):
3123 try:
3124 return (cl, cl.GetStatus())
3125 except:
3126 # See http://crbug.com/629863.
3127 logging.exception('failed to fetch status for %s:', cl)
3128 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003129 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003130
tandriiea9514a2016-08-17 12:32:37 -07003131 changes_to_fetch = changes[1:]
3132 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003133 # Exit early if there was only one branch to fetch.
3134 return
3135
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003136 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003137 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003138 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003139 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003140
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003141 fetched_cls = set()
3142 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003143 while True:
3144 try:
3145 row = it.next(timeout=5)
3146 except multiprocessing.TimeoutError:
3147 break
3148
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003149 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003150 yield row
3151
3152 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003153 for cl in set(changes_to_fetch) - fetched_cls:
3154 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003155
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003156 else:
3157 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003158 for cl in changes:
3159 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003160
rmistry@google.com2dd99862015-06-22 12:22:18 +00003161
3162def upload_branch_deps(cl, args):
3163 """Uploads CLs of local branches that are dependents of the current branch.
3164
3165 If the local branch dependency tree looks like:
3166 test1 -> test2.1 -> test3.1
3167 -> test3.2
3168 -> test2.2 -> test3.3
3169
3170 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3171 run on the dependent branches in this order:
3172 test2.1, test3.1, test3.2, test2.2, test3.3
3173
3174 Note: This function does not rebase your local dependent branches. Use it when
3175 you make a change to the parent branch that will not conflict with its
3176 dependent branches, and you would like their dependencies updated in
3177 Rietveld.
3178 """
3179 if git_common.is_dirty_git_tree('upload-branch-deps'):
3180 return 1
3181
3182 root_branch = cl.GetBranch()
3183 if root_branch is None:
3184 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3185 'Get on a branch!')
3186 if not cl.GetIssue() or not cl.GetPatchset():
3187 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3188 'patchset dependencies without an uploaded CL.')
3189
3190 branches = RunGit(['for-each-ref',
3191 '--format=%(refname:short) %(upstream:short)',
3192 'refs/heads'])
3193 if not branches:
3194 print('No local branches found.')
3195 return 0
3196
3197 # Create a dictionary of all local branches to the branches that are dependent
3198 # on it.
3199 tracked_to_dependents = collections.defaultdict(list)
3200 for b in branches.splitlines():
3201 tokens = b.split()
3202 if len(tokens) == 2:
3203 branch_name, tracked = tokens
3204 tracked_to_dependents[tracked].append(branch_name)
3205
vapiera7fbd5a2016-06-16 09:17:49 -07003206 print()
3207 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003208 dependents = []
3209 def traverse_dependents_preorder(branch, padding=''):
3210 dependents_to_process = tracked_to_dependents.get(branch, [])
3211 padding += ' '
3212 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003213 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003214 dependents.append(dependent)
3215 traverse_dependents_preorder(dependent, padding)
3216 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003217 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003218
3219 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003220 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003221 return 0
3222
vapiera7fbd5a2016-06-16 09:17:49 -07003223 print('This command will checkout all dependent branches and run '
3224 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003225 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3226
andybons@chromium.org962f9462016-02-03 20:00:42 +00003227 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003228 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003229 args.extend(['-t', 'Updated patchset dependency'])
3230
rmistry@google.com2dd99862015-06-22 12:22:18 +00003231 # Record all dependents that failed to upload.
3232 failures = {}
3233 # Go through all dependents, checkout the branch and upload.
3234 try:
3235 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003236 print()
3237 print('--------------------------------------')
3238 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003239 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003240 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003241 try:
3242 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003243 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003244 failures[dependent_branch] = 1
3245 except: # pylint: disable=W0702
3246 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003247 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003248 finally:
3249 # Swap back to the original root branch.
3250 RunGit(['checkout', '-q', root_branch])
3251
vapiera7fbd5a2016-06-16 09:17:49 -07003252 print()
3253 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003254 for dependent_branch in dependents:
3255 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003256 print(' %s : %s' % (dependent_branch, upload_status))
3257 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003258
3259 return 0
3260
3261
kmarshall3bff56b2016-06-06 18:31:47 -07003262def CMDarchive(parser, args):
3263 """Archives and deletes branches associated with closed changelists."""
3264 parser.add_option(
3265 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003266 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003267 parser.add_option(
3268 '-f', '--force', action='store_true',
3269 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003270 parser.add_option(
3271 '-d', '--dry-run', action='store_true',
3272 help='Skip the branch tagging and removal steps.')
3273 parser.add_option(
3274 '-t', '--notags', action='store_true',
3275 help='Do not tag archived branches. '
3276 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003277
3278 auth.add_auth_options(parser)
3279 options, args = parser.parse_args(args)
3280 if args:
3281 parser.error('Unsupported args: %s' % ' '.join(args))
3282 auth_config = auth.extract_auth_config_from_options(options)
3283
3284 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3285 if not branches:
3286 return 0
3287
vapiera7fbd5a2016-06-16 09:17:49 -07003288 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003289 changes = [Changelist(branchref=b, auth_config=auth_config)
3290 for b in branches.splitlines()]
3291 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3292 statuses = get_cl_statuses(changes,
3293 fine_grained=True,
3294 max_processes=options.maxjobs)
3295 proposal = [(cl.GetBranch(),
3296 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3297 for cl, status in statuses
3298 if status == 'closed']
3299 proposal.sort()
3300
3301 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003302 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003303 return 0
3304
3305 current_branch = GetCurrentBranch()
3306
vapiera7fbd5a2016-06-16 09:17:49 -07003307 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003308 if options.notags:
3309 for next_item in proposal:
3310 print(' ' + next_item[0])
3311 else:
3312 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3313 for next_item in proposal:
3314 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003315
kmarshall9249e012016-08-23 12:02:16 -07003316 # Quit now on precondition failure or if instructed by the user, either
3317 # via an interactive prompt or by command line flags.
3318 if options.dry_run:
3319 print('\nNo changes were made (dry run).\n')
3320 return 0
3321 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003322 print('You are currently on a branch \'%s\' which is associated with a '
3323 'closed codereview issue, so archive cannot proceed. Please '
3324 'checkout another branch and run this command again.' %
3325 current_branch)
3326 return 1
kmarshall9249e012016-08-23 12:02:16 -07003327 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003328 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3329 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003330 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003331 return 1
3332
3333 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003334 if not options.notags:
3335 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003336 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003337
vapiera7fbd5a2016-06-16 09:17:49 -07003338 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003339
3340 return 0
3341
3342
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003343def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003344 """Show status of changelists.
3345
3346 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003347 - Red not sent for review or broken
3348 - Blue waiting for review
3349 - Yellow waiting for you to reply to review
3350 - Green LGTM'ed
3351 - Magenta in the commit queue
3352 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003353
3354 Also see 'git cl comments'.
3355 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003357 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003358 parser.add_option('-f', '--fast', action='store_true',
3359 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003360 parser.add_option(
3361 '-j', '--maxjobs', action='store', type=int,
3362 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003363
3364 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003365 _add_codereview_issue_select_options(
3366 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003367 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003368 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003369 if args:
3370 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003371 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003372
iannuccie53c9352016-08-17 14:40:40 -07003373 if options.issue is not None and not options.field:
3374 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003376 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003377 cl = Changelist(auth_config=auth_config, issue=options.issue,
3378 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003380 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 elif options.field == 'id':
3382 issueid = cl.GetIssue()
3383 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003384 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003385 elif options.field == 'patch':
3386 patchset = cl.GetPatchset()
3387 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003388 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003389 elif options.field == 'status':
3390 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391 elif options.field == 'url':
3392 url = cl.GetIssueURL()
3393 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003394 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003395 return 0
3396
3397 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3398 if not branches:
3399 print('No local branch found.')
3400 return 0
3401
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003402 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003403 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003404 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003405 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003406 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003407 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003408 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003409
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003410 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003411 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3412 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3413 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003414 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003415 c, status = output.next()
3416 branch_statuses[c.GetBranch()] = status
3417 status = branch_statuses.pop(branch)
3418 url = cl.GetIssueURL()
3419 if url and (not status or status == 'error'):
3420 # The issue probably doesn't exist anymore.
3421 url += ' (broken)'
3422
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003423 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003424 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003425 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003426 color = ''
3427 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003428 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003429 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003430 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003431 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003432
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003433 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003434 print()
3435 print('Current branch:',)
3436 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003437 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003438 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003439 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003440 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003441 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003442 print('Issue description:')
3443 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003444 return 0
3445
3446
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003447def colorize_CMDstatus_doc():
3448 """To be called once in main() to add colors to git cl status help."""
3449 colors = [i for i in dir(Fore) if i[0].isupper()]
3450
3451 def colorize_line(line):
3452 for color in colors:
3453 if color in line.upper():
3454 # Extract whitespaces first and the leading '-'.
3455 indent = len(line) - len(line.lstrip(' ')) + 1
3456 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3457 return line
3458
3459 lines = CMDstatus.__doc__.splitlines()
3460 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3461
3462
phajdan.jre328cf92016-08-22 04:12:17 -07003463def write_json(path, contents):
3464 with open(path, 'w') as f:
3465 json.dump(contents, f)
3466
3467
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003468@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003469def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003470 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003471
3472 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003473 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003474 parser.add_option('-r', '--reverse', action='store_true',
3475 help='Lookup the branch(es) for the specified issues. If '
3476 'no issues are specified, all branches with mapped '
3477 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003478 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003479 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003480 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003481 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003482
dnj@chromium.org406c4402015-03-03 17:22:28 +00003483 if options.reverse:
3484 branches = RunGit(['for-each-ref', 'refs/heads',
3485 '--format=%(refname:short)']).splitlines()
3486
3487 # Reverse issue lookup.
3488 issue_branch_map = {}
3489 for branch in branches:
3490 cl = Changelist(branchref=branch)
3491 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3492 if not args:
3493 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003494 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003495 for issue in args:
3496 if not issue:
3497 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003498 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print('Branch for issue number %s: %s' % (
3500 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003501 if options.json:
3502 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003503 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003504 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003505 if len(args) > 0:
3506 try:
3507 issue = int(args[0])
3508 except ValueError:
3509 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003510 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003511 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003512 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003513 if options.json:
3514 write_json(options.json, {
3515 'issue': cl.GetIssue(),
3516 'issue_url': cl.GetIssueURL(),
3517 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518 return 0
3519
3520
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003521def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003522 """Shows or posts review comments for any changelist."""
3523 parser.add_option('-a', '--add-comment', dest='comment',
3524 help='comment to add to an issue')
3525 parser.add_option('-i', dest='issue',
3526 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003527 parser.add_option('-j', '--json-file',
3528 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003529 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003530 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003531 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003532
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003533 issue = None
3534 if options.issue:
3535 try:
3536 issue = int(options.issue)
3537 except ValueError:
3538 DieWithError('A review issue id is expected to be a number')
3539
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003540 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003541
3542 if options.comment:
3543 cl.AddComment(options.comment)
3544 return 0
3545
3546 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003547 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003548 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003549 summary.append({
3550 'date': message['date'],
3551 'lgtm': False,
3552 'message': message['text'],
3553 'not_lgtm': False,
3554 'sender': message['sender'],
3555 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003556 if message['disapproval']:
3557 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003558 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003559 elif message['approval']:
3560 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003561 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003562 elif message['sender'] == data['owner_email']:
3563 color = Fore.MAGENTA
3564 else:
3565 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003566 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003567 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003568 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003569 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003570 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003571 if options.json_file:
3572 with open(options.json_file, 'wb') as f:
3573 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003574 return 0
3575
3576
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003577@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003578def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003579 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003580 parser.add_option('-d', '--display', action='store_true',
3581 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003582 parser.add_option('-n', '--new-description',
3583 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003584
3585 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003586 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003587 options, args = parser.parse_args(args)
3588 _process_codereview_select_options(parser, options)
3589
3590 target_issue = None
3591 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003592 target_issue = ParseIssueNumberArgument(args[0])
3593 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003594 parser.print_help()
3595 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003596
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003597 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003598
martiniss6eda05f2016-06-30 10:18:35 -07003599 kwargs = {
3600 'auth_config': auth_config,
3601 'codereview': options.forced_codereview,
3602 }
3603 if target_issue:
3604 kwargs['issue'] = target_issue.issue
3605 if options.forced_codereview == 'rietveld':
3606 kwargs['rietveld_server'] = target_issue.hostname
3607
3608 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003609
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003610 if not cl.GetIssue():
3611 DieWithError('This branch has no associated changelist.')
3612 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003613
smut@google.com34fb6b12015-07-13 20:03:26 +00003614 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003615 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003616 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003617
3618 if options.new_description:
3619 text = options.new_description
3620 if text == '-':
3621 text = '\n'.join(l.rstrip() for l in sys.stdin)
3622
3623 description.set_description(text)
3624 else:
3625 description.prompt()
3626
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003627 if cl.GetDescription() != description.description:
3628 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003629 return 0
3630
3631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632def CreateDescriptionFromLog(args):
3633 """Pulls out the commit log to use as a base for the CL description."""
3634 log_args = []
3635 if len(args) == 1 and not args[0].endswith('.'):
3636 log_args = [args[0] + '..']
3637 elif len(args) == 1 and args[0].endswith('...'):
3638 log_args = [args[0][:-1]]
3639 elif len(args) == 2:
3640 log_args = [args[0] + '..' + args[1]]
3641 else:
3642 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003643 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644
3645
thestig@chromium.org44202a22014-03-11 19:22:18 +00003646def CMDlint(parser, args):
3647 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003648 parser.add_option('--filter', action='append', metavar='-x,+y',
3649 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003650 auth.add_auth_options(parser)
3651 options, args = parser.parse_args(args)
3652 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003653
3654 # Access to a protected member _XX of a client class
3655 # pylint: disable=W0212
3656 try:
3657 import cpplint
3658 import cpplint_chromium
3659 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003660 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003661 return 1
3662
3663 # Change the current working directory before calling lint so that it
3664 # shows the correct base.
3665 previous_cwd = os.getcwd()
3666 os.chdir(settings.GetRoot())
3667 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003668 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003669 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3670 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003671 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003673 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003674
3675 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003676 command = args + files
3677 if options.filter:
3678 command = ['--filter=' + ','.join(options.filter)] + command
3679 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003680
3681 white_regex = re.compile(settings.GetLintRegex())
3682 black_regex = re.compile(settings.GetLintIgnoreRegex())
3683 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3684 for filename in filenames:
3685 if white_regex.match(filename):
3686 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003687 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003688 else:
3689 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3690 extra_check_functions)
3691 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003692 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003693 finally:
3694 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003695 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003696 if cpplint._cpplint_state.error_count != 0:
3697 return 1
3698 return 0
3699
3700
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003702 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003703 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003705 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003706 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003707 auth.add_auth_options(parser)
3708 options, args = parser.parse_args(args)
3709 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710
sbc@chromium.org71437c02015-04-09 19:29:40 +00003711 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 return 1
3714
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003715 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003716 if args:
3717 base_branch = args[0]
3718 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003719 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003720 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003722 cl.RunHook(
3723 committing=not options.upload,
3724 may_prompt=False,
3725 verbose=options.verbose,
3726 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003727 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728
3729
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003730def GenerateGerritChangeId(message):
3731 """Returns Ixxxxxx...xxx change id.
3732
3733 Works the same way as
3734 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3735 but can be called on demand on all platforms.
3736
3737 The basic idea is to generate git hash of a state of the tree, original commit
3738 message, author/committer info and timestamps.
3739 """
3740 lines = []
3741 tree_hash = RunGitSilent(['write-tree'])
3742 lines.append('tree %s' % tree_hash.strip())
3743 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3744 if code == 0:
3745 lines.append('parent %s' % parent.strip())
3746 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3747 lines.append('author %s' % author.strip())
3748 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3749 lines.append('committer %s' % committer.strip())
3750 lines.append('')
3751 # Note: Gerrit's commit-hook actually cleans message of some lines and
3752 # whitespace. This code is not doing this, but it clearly won't decrease
3753 # entropy.
3754 lines.append(message)
3755 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3756 stdin='\n'.join(lines))
3757 return 'I%s' % change_hash.strip()
3758
3759
wittman@chromium.org455dc922015-01-26 20:15:50 +00003760def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3761 """Computes the remote branch ref to use for the CL.
3762
3763 Args:
3764 remote (str): The git remote for the CL.
3765 remote_branch (str): The git remote branch for the CL.
3766 target_branch (str): The target branch specified by the user.
3767 pending_prefix (str): The pending prefix from the settings.
3768 """
3769 if not (remote and remote_branch):
3770 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003771
wittman@chromium.org455dc922015-01-26 20:15:50 +00003772 if target_branch:
3773 # Cannonicalize branch references to the equivalent local full symbolic
3774 # refs, which are then translated into the remote full symbolic refs
3775 # below.
3776 if '/' not in target_branch:
3777 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3778 else:
3779 prefix_replacements = (
3780 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3781 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3782 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3783 )
3784 match = None
3785 for regex, replacement in prefix_replacements:
3786 match = re.search(regex, target_branch)
3787 if match:
3788 remote_branch = target_branch.replace(match.group(0), replacement)
3789 break
3790 if not match:
3791 # This is a branch path but not one we recognize; use as-is.
3792 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003793 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3794 # Handle the refs that need to land in different refs.
3795 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003796
wittman@chromium.org455dc922015-01-26 20:15:50 +00003797 # Create the true path to the remote branch.
3798 # Does the following translation:
3799 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3800 # * refs/remotes/origin/master -> refs/heads/master
3801 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3802 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3803 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3804 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3805 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3806 'refs/heads/')
3807 elif remote_branch.startswith('refs/remotes/branch-heads'):
3808 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3809 # If a pending prefix exists then replace refs/ with it.
3810 if pending_prefix:
3811 remote_branch = remote_branch.replace('refs/', pending_prefix)
3812 return remote_branch
3813
3814
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003815def cleanup_list(l):
3816 """Fixes a list so that comma separated items are put as individual items.
3817
3818 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3819 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3820 """
3821 items = sum((i.split(',') for i in l), [])
3822 stripped_items = (i.strip() for i in items)
3823 return sorted(filter(None, stripped_items))
3824
3825
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003826@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003827def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003828 """Uploads the current changelist to codereview.
3829
3830 Can skip dependency patchset uploads for a branch by running:
3831 git config branch.branch_name.skip-deps-uploads True
3832 To unset run:
3833 git config --unset branch.branch_name.skip-deps-uploads
3834 Can also set the above globally by using the --global flag.
3835 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003836 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3837 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003838 parser.add_option('--bypass-watchlists', action='store_true',
3839 dest='bypass_watchlists',
3840 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003841 parser.add_option('-f', action='store_true', dest='force',
3842 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003843 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003844 parser.add_option('-b', '--bug',
3845 help='pre-populate the bug number(s) for this issue. '
3846 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003847 parser.add_option('--message-file', dest='message_file',
3848 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003849 parser.add_option('-t', dest='title',
3850 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003851 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003852 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003853 help='reviewer email addresses')
3854 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003855 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003856 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003857 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003858 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003859 parser.add_option('--emulate_svn_auto_props',
3860 '--emulate-svn-auto-props',
3861 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003862 dest="emulate_svn_auto_props",
3863 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003864 parser.add_option('-c', '--use-commit-queue', action='store_true',
3865 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003866 parser.add_option('--private', action='store_true',
3867 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003868 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003869 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003870 metavar='TARGET',
3871 help='Apply CL to remote ref TARGET. ' +
3872 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003873 parser.add_option('--squash', action='store_true',
3874 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003875 parser.add_option('--no-squash', action='store_true',
3876 help='Don\'t squash multiple commits into one ' +
3877 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003878 parser.add_option('--email', default=None,
3879 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003880 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3881 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003882 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3883 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003884 help='Send the patchset to do a CQ dry run right after '
3885 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003886 parser.add_option('--dependencies', action='store_true',
3887 help='Uploads CLs of all the local branches that depend on '
3888 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003889
rmistry@google.com2dd99862015-06-22 12:22:18 +00003890 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003891 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003892 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003893 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003894 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003895 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003896 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003897
sbc@chromium.org71437c02015-04-09 19:29:40 +00003898 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003899 return 1
3900
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003901 options.reviewers = cleanup_list(options.reviewers)
3902 options.cc = cleanup_list(options.cc)
3903
tandriib80458a2016-06-23 12:20:07 -07003904 if options.message_file:
3905 if options.message:
3906 parser.error('only one of --message and --message-file allowed.')
3907 options.message = gclient_utils.FileRead(options.message_file)
3908 options.message_file = None
3909
tandrii4d0545a2016-07-06 03:56:49 -07003910 if options.cq_dry_run and options.use_commit_queue:
3911 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3912
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003913 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3914 settings.GetIsGerrit()
3915
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003916 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003917 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003918
3919
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003920def IsSubmoduleMergeCommit(ref):
3921 # When submodules are added to the repo, we expect there to be a single
3922 # non-git-svn merge commit at remote HEAD with a signature comment.
3923 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003924 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003925 return RunGit(cmd) != ''
3926
3927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003929 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003930
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003931 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3932 upstream and closes the issue automatically and atomically.
3933
3934 Otherwise (in case of Rietveld):
3935 Squashes branch into a single commit.
3936 Updates changelog with metadata (e.g. pointer to review).
3937 Pushes/dcommits the code upstream.
3938 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003939 """
3940 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3941 help='bypass upload presubmit hook')
3942 parser.add_option('-m', dest='message',
3943 help="override review description")
3944 parser.add_option('-f', action='store_true', dest='force',
3945 help="force yes to questions (don't prompt)")
3946 parser.add_option('-c', dest='contributor',
3947 help="external contributor for patch (appended to " +
3948 "description and used as author for git). Should be " +
3949 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003950 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003951 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003953 auth_config = auth.extract_auth_config_from_options(options)
3954
3955 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003956
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003957 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3958 if cl.IsGerrit():
3959 if options.message:
3960 # This could be implemented, but it requires sending a new patch to
3961 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3962 # Besides, Gerrit has the ability to change the commit message on submit
3963 # automatically, thus there is no need to support this option (so far?).
3964 parser.error('-m MESSAGE option is not supported for Gerrit.')
3965 if options.contributor:
3966 parser.error(
3967 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3968 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3969 'the contributor\'s "name <email>". If you can\'t upload such a '
3970 'commit for review, contact your repository admin and request'
3971 '"Forge-Author" permission.')
3972 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3973 options.verbose)
3974
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003975 current = cl.GetBranch()
3976 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3977 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003978 print()
3979 print('Attempting to push branch %r into another local branch!' % current)
3980 print()
3981 print('Either reparent this branch on top of origin/master:')
3982 print(' git reparent-branch --root')
3983 print()
3984 print('OR run `git rebase-update` if you think the parent branch is ')
3985 print('already committed.')
3986 print()
3987 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003988 return 1
3989
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003990 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991 # Default to merging against our best guess of the upstream branch.
3992 args = [cl.GetUpstreamBranch()]
3993
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003994 if options.contributor:
3995 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003996 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003997 return 1
3998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004000 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001
sbc@chromium.org71437c02015-04-09 19:29:40 +00004002 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003 return 1
4004
4005 # This rev-list syntax means "show all commits not in my branch that
4006 # are in base_branch".
4007 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4008 base_branch]).splitlines()
4009 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004010 print('Base branch "%s" has %d commits '
4011 'not in this branch.' % (base_branch, len(upstream_commits)))
4012 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013 return 1
4014
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004015 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004016 svn_head = None
4017 if cmd == 'dcommit' or base_has_submodules:
4018 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4019 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004020
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004021 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004022 # If the base_head is a submodule merge commit, the first parent of the
4023 # base_head should be a git-svn commit, which is what we're interested in.
4024 base_svn_head = base_branch
4025 if base_has_submodules:
4026 base_svn_head += '^1'
4027
4028 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004029 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004030 print('This branch has %d additional commits not upstreamed yet.'
4031 % len(extra_commits.splitlines()))
4032 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4033 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004034 return 1
4035
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004036 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004037 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004038 author = None
4039 if options.contributor:
4040 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004041 hook_results = cl.RunHook(
4042 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004043 may_prompt=not options.force,
4044 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004045 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004046 if not hook_results.should_continue():
4047 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004049 # Check the tree status if the tree status URL is set.
4050 status = GetTreeStatus()
4051 if 'closed' == status:
4052 print('The tree is closed. Please wait for it to reopen. Use '
4053 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4054 return 1
4055 elif 'unknown' == status:
4056 print('Unable to determine tree status. Please verify manually and '
4057 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4058 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004060 change_desc = ChangeDescription(options.message)
4061 if not change_desc.description and cl.GetIssue():
4062 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004064 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004065 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004066 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004067 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004068 print('No description set.')
4069 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004070 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004071
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004072 # Keep a separate copy for the commit message, because the commit message
4073 # contains the link to the Rietveld issue, while the Rietveld message contains
4074 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004075 # Keep a separate copy for the commit message.
4076 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004077 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004078
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004079 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004080 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004081 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004082 # after it. Add a period on a new line to circumvent this. Also add a space
4083 # before the period to make sure that Gitiles continues to correctly resolve
4084 # the URL.
4085 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004087 commit_desc.append_footer('Patch from %s.' % options.contributor)
4088
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004089 print('Description:')
4090 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004091
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004092 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004094 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004096 # We want to squash all this branch's commits into one commit with the proper
4097 # description. We do this by doing a "reset --soft" to the base branch (which
4098 # keeps the working copy the same), then dcommitting that. If origin/master
4099 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4100 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004101 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004102 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4103 # Delete the branches if they exist.
4104 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4105 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4106 result = RunGitWithCode(showref_cmd)
4107 if result[0] == 0:
4108 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109
4110 # We might be in a directory that's present in this branch but not in the
4111 # trunk. Move up to the top of the tree so that git commands that expect a
4112 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004113 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 if rel_base_path:
4115 os.chdir(rel_base_path)
4116
4117 # Stuff our change into the merge branch.
4118 # We wrap in a try...finally block so if anything goes wrong,
4119 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004120 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004121 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004122 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004123 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004124 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004125 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004126 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004127 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004128 RunGit(
4129 [
4130 'commit', '--author', options.contributor,
4131 '-m', commit_desc.description,
4132 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004133 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004134 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004135 if base_has_submodules:
4136 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4137 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4138 RunGit(['checkout', CHERRY_PICK_BRANCH])
4139 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004140 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004141 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004142 mirror = settings.GetGitMirror(remote)
4143 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004144 pending_prefix = settings.GetPendingRefPrefix()
4145 if not pending_prefix or branch.startswith(pending_prefix):
4146 # If not using refs/pending/heads/* at all, or target ref is already set
4147 # to pending, then push to the target ref directly.
4148 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004149 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004150 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004151 else:
4152 # Cherry-pick the change on top of pending ref and then push it.
4153 assert branch.startswith('refs/'), branch
4154 assert pending_prefix[-1] == '/', pending_prefix
4155 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004156 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004157 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004158 if retcode == 0:
4159 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004160 else:
4161 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004162 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004163 'svn', 'dcommit',
4164 '-C%s' % options.similarity,
4165 '--no-rebase', '--rmdir',
4166 ]
4167 if settings.GetForceHttpsCommitUrl():
4168 # Allow forcing https commit URLs for some projects that don't allow
4169 # committing to http URLs (like Google Code).
4170 remote_url = cl.GetGitSvnRemoteUrl()
4171 if urlparse.urlparse(remote_url).scheme == 'http':
4172 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004173 cmd_args.append('--commit-url=%s' % remote_url)
4174 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004175 if 'Committed r' in output:
4176 revision = re.match(
4177 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4178 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004179 finally:
4180 # And then swap back to the original branch and clean up.
4181 RunGit(['checkout', '-q', cl.GetBranch()])
4182 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004183 if base_has_submodules:
4184 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004186 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004187 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004188 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004189
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004190 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004191 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004192 try:
4193 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4194 # We set pushed_to_pending to False, since it made it all the way to the
4195 # real ref.
4196 pushed_to_pending = False
4197 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004198 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004201 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004203 if not to_pending:
4204 if viewvc_url and revision:
4205 change_desc.append_footer(
4206 'Committed: %s%s' % (viewvc_url, revision))
4207 elif revision:
4208 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004209 print('Closing issue '
4210 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004211 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004213 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004214 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004215 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004216 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004217 if options.bypass_hooks:
4218 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4219 else:
4220 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004221 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004222
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004223 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004224 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004225 print('The commit is in the pending queue (%s).' % pending_ref)
4226 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4227 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004228
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004229 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4230 if os.path.isfile(hook):
4231 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004232
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004233 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004234
4235
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004236def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004237 print()
4238 print('Waiting for commit to be landed on %s...' % real_ref)
4239 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004240 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4241 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004242 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004243
4244 loop = 0
4245 while True:
4246 sys.stdout.write('fetching (%d)... \r' % loop)
4247 sys.stdout.flush()
4248 loop += 1
4249
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004250 if mirror:
4251 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004252 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4253 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4254 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4255 for commit in commits.splitlines():
4256 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004257 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004258 return commit
4259
4260 current_rev = to_rev
4261
4262
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004263def PushToGitPending(remote, pending_ref, upstream_ref):
4264 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4265
4266 Returns:
4267 (retcode of last operation, output log of last operation).
4268 """
4269 assert pending_ref.startswith('refs/'), pending_ref
4270 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4271 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4272 code = 0
4273 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004274 max_attempts = 3
4275 attempts_left = max_attempts
4276 while attempts_left:
4277 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004278 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004279 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004280
4281 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004282 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004283 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004284 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004285 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004286 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004287 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004288 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004289 continue
4290
4291 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004293 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004294 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004295 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4297 'the following files have merge conflicts:' % pending_ref)
4298 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4299 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004300 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004301 return code, out
4302
4303 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004305 code, out = RunGitWithCode(
4306 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4307 if code == 0:
4308 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004310 return code, out
4311
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004313 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004315 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004316 print('Fatal push error. Make sure your .netrc credentials and git '
4317 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004318 return code, out
4319
vapiera7fbd5a2016-06-16 09:17:49 -07004320 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004321 return code, out
4322
4323
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004324def IsFatalPushFailure(push_stdout):
4325 """True if retrying push won't help."""
4326 return '(prohibited by Gerrit)' in push_stdout
4327
4328
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004329@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004331 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004333 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004334 # If it looks like previous commits were mirrored with git-svn.
4335 message = """This repository appears to be a git-svn mirror, but no
4336upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4337 else:
4338 message = """This doesn't appear to be an SVN repository.
4339If your project has a true, writeable git repository, you probably want to run
4340'git cl land' instead.
4341If your project has a git mirror of an upstream SVN master, you probably need
4342to run 'git svn init'.
4343
4344Using the wrong command might cause your commit to appear to succeed, and the
4345review to be closed, without actually landing upstream. If you choose to
4346proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004347 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004348 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004349 # TODO(tandrii): kill this post SVN migration with
4350 # https://codereview.chromium.org/2076683002
4351 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4352 'Please let us know of this project you are committing to:'
4353 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004354 return SendUpstream(parser, args, 'dcommit')
4355
4356
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004357@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004358def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004359 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004360 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004361 print('This appears to be an SVN repository.')
4362 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004363 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004364 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004365 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366
4367
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004368@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004370 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004371 parser.add_option('-b', dest='newbranch',
4372 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004373 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004375 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4376 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004377 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004378 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004379 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004380 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004382 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004383
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004384
4385 group = optparse.OptionGroup(
4386 parser,
4387 'Options for continuing work on the current issue uploaded from a '
4388 'different clone (e.g. different machine). Must be used independently '
4389 'from the other options. No issue number should be specified, and the '
4390 'branch must have an issue number associated with it')
4391 group.add_option('--reapply', action='store_true', dest='reapply',
4392 help='Reset the branch and reapply the issue.\n'
4393 'CAUTION: This will undo any local changes in this '
4394 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004395
4396 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004397 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004398 parser.add_option_group(group)
4399
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004400 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004401 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004402 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004403 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004404 auth_config = auth.extract_auth_config_from_options(options)
4405
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004406
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004407 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004408 if options.newbranch:
4409 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004410 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004411 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004412
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004413 cl = Changelist(auth_config=auth_config,
4414 codereview=options.forced_codereview)
4415 if not cl.GetIssue():
4416 parser.error('current branch must have an associated issue')
4417
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004418 upstream = cl.GetUpstreamBranch()
4419 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004420 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004421
4422 RunGit(['reset', '--hard', upstream])
4423 if options.pull:
4424 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004425
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004426 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4427 options.directory)
4428
4429 if len(args) != 1 or not args[0]:
4430 parser.error('Must specify issue number or url')
4431
4432 # We don't want uncommitted changes mixed up with the patch.
4433 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004434 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004436 if options.newbranch:
4437 if options.force:
4438 RunGit(['branch', '-D', options.newbranch],
4439 stderr=subprocess2.PIPE, error_ok=True)
4440 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004441 elif not GetCurrentBranch():
4442 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004443
4444 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4445
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004446 if cl.IsGerrit():
4447 if options.reject:
4448 parser.error('--reject is not supported with Gerrit codereview.')
4449 if options.nocommit:
4450 parser.error('--nocommit is not supported with Gerrit codereview.')
4451 if options.directory:
4452 parser.error('--directory is not supported with Gerrit codereview.')
4453
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004454 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004455 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456
4457
4458def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004459 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004460 # Provide a wrapper for git svn rebase to help avoid accidental
4461 # git svn dcommit.
4462 # It's the only command that doesn't use parser at all since we just defer
4463 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004464
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004465 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466
4467
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004468def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004469 """Fetches the tree status and returns either 'open', 'closed',
4470 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004471 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472 if url:
4473 status = urllib2.urlopen(url).read().lower()
4474 if status.find('closed') != -1 or status == '0':
4475 return 'closed'
4476 elif status.find('open') != -1 or status == '1':
4477 return 'open'
4478 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479 return 'unset'
4480
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482def GetTreeStatusReason():
4483 """Fetches the tree status from a json url and returns the message
4484 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004485 url = settings.GetTreeStatusUrl()
4486 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004487 connection = urllib2.urlopen(json_url)
4488 status = json.loads(connection.read())
4489 connection.close()
4490 return status['message']
4491
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004492
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004493def GetBuilderMaster(bot_list):
4494 """For a given builder, fetch the master from AE if available."""
4495 map_url = 'https://builders-map.appspot.com/'
4496 try:
4497 master_map = json.load(urllib2.urlopen(map_url))
4498 except urllib2.URLError as e:
4499 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4500 (map_url, e))
4501 except ValueError as e:
4502 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4503 if not master_map:
4504 return None, 'Failed to build master map.'
4505
4506 result_master = ''
4507 for bot in bot_list:
4508 builder = bot.split(':', 1)[0]
4509 master_list = master_map.get(builder, [])
4510 if not master_list:
4511 return None, ('No matching master for builder %s.' % builder)
4512 elif len(master_list) > 1:
4513 return None, ('The builder name %s exists in multiple masters %s.' %
4514 (builder, master_list))
4515 else:
4516 cur_master = master_list[0]
4517 if not result_master:
4518 result_master = cur_master
4519 elif result_master != cur_master:
4520 return None, 'The builders do not belong to the same master.'
4521 return result_master, None
4522
4523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004524def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004525 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004526 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004527 status = GetTreeStatus()
4528 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004529 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530 return 2
4531
vapiera7fbd5a2016-06-16 09:17:49 -07004532 print('The tree is %s' % status)
4533 print()
4534 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535 if status != 'open':
4536 return 1
4537 return 0
4538
4539
maruel@chromium.org15192402012-09-06 12:38:29 +00004540def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004541 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004542 group = optparse.OptionGroup(parser, "Try job options")
4543 group.add_option(
4544 "-b", "--bot", action="append",
4545 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4546 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004547 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004548 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004549 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004550 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004551 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004552 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004553 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004554 "-r", "--revision",
4555 help="Revision to use for the try job; default: the "
4556 "revision will be determined by the try server; see "
4557 "its waterfall for more info")
4558 group.add_option(
4559 "-c", "--clobber", action="store_true", default=False,
4560 help="Force a clobber before building; e.g. don't do an "
4561 "incremental build")
4562 group.add_option(
4563 "--project",
4564 help="Override which project to use. Projects are defined "
4565 "server-side to define what default bot set to use")
4566 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004567 "-p", "--property", dest="properties", action="append", default=[],
4568 help="Specify generic properties in the form -p key1=value1 -p "
4569 "key2=value2 etc (buildbucket only). The value will be treated as "
4570 "json if decodable, or as string otherwise.")
4571 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004572 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004573 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004574 "--use-rietveld", action="store_true", default=False,
4575 help="Use Rietveld to trigger try jobs.")
4576 group.add_option(
4577 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4578 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004579 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004580 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004581 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004582 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004583
machenbach@chromium.org45453142015-09-15 08:45:22 +00004584 if options.use_rietveld and options.properties:
4585 parser.error('Properties can only be specified with buildbucket')
4586
4587 # Make sure that all properties are prop=value pairs.
4588 bad_params = [x for x in options.properties if '=' not in x]
4589 if bad_params:
4590 parser.error('Got properties with missing "=": %s' % bad_params)
4591
maruel@chromium.org15192402012-09-06 12:38:29 +00004592 if args:
4593 parser.error('Unknown arguments: %s' % args)
4594
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004595 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004596 if not cl.GetIssue():
4597 parser.error('Need to upload first')
4598
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004599 if cl.IsGerrit():
4600 parser.error(
4601 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4602 'If your project has Commit Queue, dry run is a workaround:\n'
4603 ' git cl set-commit --dry-run')
4604 # Code below assumes Rietveld issue.
4605 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4606
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004607 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004608 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004609 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004610
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004611 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004612 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004613
maruel@chromium.org15192402012-09-06 12:38:29 +00004614 if not options.name:
4615 options.name = cl.GetBranch()
4616
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004617 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004618 options.master, err_msg = GetBuilderMaster(options.bot)
4619 if err_msg:
4620 parser.error('Tryserver master cannot be found because: %s\n'
4621 'Please manually specify the tryserver master'
4622 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004623
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004624 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004625 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004626 if not options.bot:
4627 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004628
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004629 # Get try masters from PRESUBMIT.py files.
4630 masters = presubmit_support.DoGetTryMasters(
4631 change,
4632 change.LocalPaths(),
4633 settings.GetRoot(),
4634 None,
4635 None,
4636 options.verbose,
4637 sys.stdout)
4638 if masters:
4639 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004640
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004641 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4642 options.bot = presubmit_support.DoGetTrySlaves(
4643 change,
4644 change.LocalPaths(),
4645 settings.GetRoot(),
4646 None,
4647 None,
4648 options.verbose,
4649 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004650
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004651 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004652 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004653
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004654 builders_and_tests = {}
4655 # TODO(machenbach): The old style command-line options don't support
4656 # multiple try masters yet.
4657 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4658 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4659
4660 for bot in old_style:
4661 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004662 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004663 elif ',' in bot:
4664 parser.error('Specify one bot per --bot flag')
4665 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004666 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004667
4668 for bot, tests in new_style:
4669 builders_and_tests.setdefault(bot, []).extend(tests)
4670
4671 # Return a master map with one master to be backwards compatible. The
4672 # master name defaults to an empty string, which will cause the master
4673 # not to be set on rietveld (deprecated).
4674 return {options.master: builders_and_tests}
4675
4676 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004677 if not masters:
4678 # Default to triggering Dry Run (see http://crbug.com/625697).
4679 if options.verbose:
4680 print('git cl try with no bots now defaults to CQ Dry Run.')
4681 try:
4682 cl.SetCQState(_CQState.DRY_RUN)
4683 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4684 return 0
4685 except KeyboardInterrupt:
4686 raise
4687 except:
4688 print('WARNING: failed to trigger CQ Dry Run.\n'
4689 'Either:\n'
4690 ' * your project has no CQ\n'
4691 ' * you don\'t have permission to trigger Dry Run\n'
4692 ' * bug in this code (see stack trace below).\n'
4693 'Consider specifying which bots to trigger manually '
4694 'or asking your project owners for permissions '
4695 'or contacting Chrome Infrastructure team at '
4696 'https://www.chromium.org/infra\n\n')
4697 # Still raise exception so that stack trace is printed.
4698 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004699
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004700 for builders in masters.itervalues():
4701 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004702 print('ERROR You are trying to send a job to a triggered bot. This type '
4703 'of bot requires an\ninitial job from a parent (usually a builder).'
4704 ' Instead send your job to the parent.\n'
4705 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004706 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004707
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004708 patchset = cl.GetMostRecentPatchset()
4709 if patchset and patchset != cl.GetPatchset():
4710 print(
4711 '\nWARNING Mismatch between local config and server. Did a previous '
4712 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4713 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004714 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004715 try:
4716 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4717 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004718 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004719 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004720 except Exception as e:
4721 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004722 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004723 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004724 return 1
4725 else:
4726 try:
4727 cl.RpcServer().trigger_distributed_try_jobs(
4728 cl.GetIssue(), patchset, options.name, options.clobber,
4729 options.revision, masters)
4730 except urllib2.HTTPError as e:
4731 if e.code == 404:
4732 print('404 from rietveld; '
4733 'did you mean to use "git try" instead of "git cl try"?')
4734 return 1
4735 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004736
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004737 for (master, builders) in sorted(masters.iteritems()):
4738 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004739 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004740 length = max(len(builder) for builder in builders)
4741 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004742 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004743 return 0
4744
4745
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004746def CMDtry_results(parser, args):
4747 group = optparse.OptionGroup(parser, "Try job results options")
4748 group.add_option(
4749 "-p", "--patchset", type=int, help="patchset number if not current.")
4750 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004751 "--print-master", action='store_true', help="print master name as well.")
4752 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004753 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004754 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004755 group.add_option(
4756 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4757 help="Host of buildbucket. The default host is %default.")
4758 parser.add_option_group(group)
4759 auth.add_auth_options(parser)
4760 options, args = parser.parse_args(args)
4761 if args:
4762 parser.error('Unrecognized args: %s' % ' '.join(args))
4763
4764 auth_config = auth.extract_auth_config_from_options(options)
4765 cl = Changelist(auth_config=auth_config)
4766 if not cl.GetIssue():
4767 parser.error('Need to upload first')
4768
4769 if not options.patchset:
4770 options.patchset = cl.GetMostRecentPatchset()
4771 if options.patchset and options.patchset != cl.GetPatchset():
4772 print(
4773 '\nWARNING Mismatch between local config and server. Did a previous '
4774 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4775 'Continuing using\npatchset %s.\n' % options.patchset)
4776 try:
4777 jobs = fetch_try_jobs(auth_config, cl, options)
4778 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004779 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004780 return 1
4781 except Exception as e:
4782 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004783 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004784 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004785 return 1
qyearsleyeab3c042016-08-24 09:18:28 -07004786 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004787 return 0
4788
4789
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004790@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004791def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004792 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004793 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004794 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004795 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004797 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004798 if args:
4799 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004800 branch = cl.GetBranch()
4801 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004802 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004803 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004804
4805 # Clear configured merge-base, if there is one.
4806 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004807 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004808 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809 return 0
4810
4811
thestig@chromium.org00858c82013-12-02 23:08:03 +00004812def CMDweb(parser, args):
4813 """Opens the current CL in the web browser."""
4814 _, args = parser.parse_args(args)
4815 if args:
4816 parser.error('Unrecognized args: %s' % ' '.join(args))
4817
4818 issue_url = Changelist().GetIssueURL()
4819 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004820 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004821 return 1
4822
4823 webbrowser.open(issue_url)
4824 return 0
4825
4826
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004827def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004828 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004829 parser.add_option('-d', '--dry-run', action='store_true',
4830 help='trigger in dry run mode')
4831 parser.add_option('-c', '--clear', action='store_true',
4832 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004833 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004834 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004835 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004836 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004837 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004838 if args:
4839 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004840 if options.dry_run and options.clear:
4841 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4842
iannuccie53c9352016-08-17 14:40:40 -07004843 cl = Changelist(auth_config=auth_config, issue=options.issue,
4844 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004845 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004846 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004847 elif options.dry_run:
4848 state = _CQState.DRY_RUN
4849 else:
4850 state = _CQState.COMMIT
4851 if not cl.GetIssue():
4852 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004853 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004854 return 0
4855
4856
groby@chromium.org411034a2013-02-26 15:12:01 +00004857def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004858 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004859 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004860 auth.add_auth_options(parser)
4861 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004862 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004863 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004864 if args:
4865 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004866 cl = Changelist(auth_config=auth_config, issue=options.issue,
4867 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004868 # Ensure there actually is an issue to close.
4869 cl.GetDescription()
4870 cl.CloseIssue()
4871 return 0
4872
4873
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004874def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004875 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004876 auth.add_auth_options(parser)
4877 options, args = parser.parse_args(args)
4878 auth_config = auth.extract_auth_config_from_options(options)
4879 if args:
4880 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004881
4882 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004883 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004884 # Staged changes would be committed along with the patch from last
4885 # upload, hence counted toward the "last upload" side in the final
4886 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004887 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004888 return 1
4889
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004890 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004891 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004892 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004893 if not issue:
4894 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004895 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004896 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004897
4898 # Create a new branch based on the merge-base
4899 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004900 # Clear cached branch in cl object, to avoid overwriting original CL branch
4901 # properties.
4902 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004903 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004904 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004905 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004906 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004907 return rtn
4908
wychen@chromium.org06928532015-02-03 02:11:29 +00004909 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004910 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004911 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004912 finally:
4913 RunGit(['checkout', '-q', branch])
4914 RunGit(['branch', '-D', TMP_BRANCH])
4915
4916 return 0
4917
4918
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004919def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004920 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004921 parser.add_option(
4922 '--no-color',
4923 action='store_true',
4924 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004925 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004926 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004927 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004928
4929 author = RunGit(['config', 'user.email']).strip() or None
4930
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004931 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004932
4933 if args:
4934 if len(args) > 1:
4935 parser.error('Unknown args')
4936 base_branch = args[0]
4937 else:
4938 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004939 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004940
4941 change = cl.GetChange(base_branch, None)
4942 return owners_finder.OwnersFinder(
4943 [f.LocalPath() for f in
4944 cl.GetChange(base_branch, None).AffectedFiles()],
4945 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004946 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004947 disable_color=options.no_color).run()
4948
4949
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004950def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004951 """Generates a diff command."""
4952 # Generate diff for the current branch's changes.
4953 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4954 upstream_commit, '--' ]
4955
4956 if args:
4957 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004958 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004959 diff_cmd.append(arg)
4960 else:
4961 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004962
4963 return diff_cmd
4964
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004965def MatchingFileType(file_name, extensions):
4966 """Returns true if the file name ends with one of the given extensions."""
4967 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004968
enne@chromium.org555cfe42014-01-29 18:21:39 +00004969@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004970def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004971 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004972 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004973 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004974 parser.add_option('--full', action='store_true',
4975 help='Reformat the full content of all touched files')
4976 parser.add_option('--dry-run', action='store_true',
4977 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004978 parser.add_option('--python', action='store_true',
4979 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004980 parser.add_option('--diff', action='store_true',
4981 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004982 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004983
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004984 # git diff generates paths against the root of the repository. Change
4985 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004986 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004987 if rel_base_path:
4988 os.chdir(rel_base_path)
4989
digit@chromium.org29e47272013-05-17 17:01:46 +00004990 # Grab the merge-base commit, i.e. the upstream commit of the current
4991 # branch when it was created or the last time it was rebased. This is
4992 # to cover the case where the user may have called "git fetch origin",
4993 # moving the origin branch to a newer commit, but hasn't rebased yet.
4994 upstream_commit = None
4995 cl = Changelist()
4996 upstream_branch = cl.GetUpstreamBranch()
4997 if upstream_branch:
4998 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4999 upstream_commit = upstream_commit.strip()
5000
5001 if not upstream_commit:
5002 DieWithError('Could not find base commit for this branch. '
5003 'Are you in detached state?')
5004
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005005 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5006 diff_output = RunGit(changed_files_cmd)
5007 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005008 # Filter out files deleted by this CL
5009 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005010
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005011 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5012 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5013 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005014 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005015
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005016 top_dir = os.path.normpath(
5017 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5018
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005019 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5020 # formatted. This is used to block during the presubmit.
5021 return_value = 0
5022
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005023 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005024 # Locate the clang-format binary in the checkout
5025 try:
5026 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005027 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005028 DieWithError(e)
5029
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005030 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005031 cmd = [clang_format_tool]
5032 if not opts.dry_run and not opts.diff:
5033 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005034 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005035 if opts.diff:
5036 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005037 else:
5038 env = os.environ.copy()
5039 env['PATH'] = str(os.path.dirname(clang_format_tool))
5040 try:
5041 script = clang_format.FindClangFormatScriptInChromiumTree(
5042 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005043 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005044 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005045
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005046 cmd = [sys.executable, script, '-p0']
5047 if not opts.dry_run and not opts.diff:
5048 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005049
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005050 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5051 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005052
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005053 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5054 if opts.diff:
5055 sys.stdout.write(stdout)
5056 if opts.dry_run and len(stdout) > 0:
5057 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005058
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005059 # Similar code to above, but using yapf on .py files rather than clang-format
5060 # on C/C++ files
5061 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005062 yapf_tool = gclient_utils.FindExecutable('yapf')
5063 if yapf_tool is None:
5064 DieWithError('yapf not found in PATH')
5065
5066 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005067 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005068 cmd = [yapf_tool]
5069 if not opts.dry_run and not opts.diff:
5070 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005071 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005072 if opts.diff:
5073 sys.stdout.write(stdout)
5074 else:
5075 # TODO(sbc): yapf --lines mode still has some issues.
5076 # https://github.com/google/yapf/issues/154
5077 DieWithError('--python currently only works with --full')
5078
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005079 # Dart's formatter does not have the nice property of only operating on
5080 # modified chunks, so hard code full.
5081 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005082 try:
5083 command = [dart_format.FindDartFmtToolInChromiumTree()]
5084 if not opts.dry_run and not opts.diff:
5085 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005086 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005087
ppi@chromium.org6593d932016-03-03 15:41:15 +00005088 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005089 if opts.dry_run and stdout:
5090 return_value = 2
5091 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005092 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5093 'found in this checkout. Files in other languages are still '
5094 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005095
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005096 # Format GN build files. Always run on full build files for canonical form.
5097 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005098 cmd = ['gn', 'format' ]
5099 if opts.dry_run or opts.diff:
5100 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005101 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005102 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5103 shell=sys.platform == 'win32',
5104 cwd=top_dir)
5105 if opts.dry_run and gn_ret == 2:
5106 return_value = 2 # Not formatted.
5107 elif opts.diff and gn_ret == 2:
5108 # TODO this should compute and print the actual diff.
5109 print("This change has GN build file diff for " + gn_diff_file)
5110 elif gn_ret != 0:
5111 # For non-dry run cases (and non-2 return values for dry-run), a
5112 # nonzero error code indicates a failure, probably because the file
5113 # doesn't parse.
5114 DieWithError("gn format failed on " + gn_diff_file +
5115 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005116
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005117 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005118
5119
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005120@subcommand.usage('<codereview url or issue id>')
5121def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005122 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005123 _, args = parser.parse_args(args)
5124
5125 if len(args) != 1:
5126 parser.print_help()
5127 return 1
5128
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005129 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005130 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005131 parser.print_help()
5132 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005133 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005134
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005135 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005136 output = RunGit(['config', '--local', '--get-regexp',
5137 r'branch\..*\.%s' % issueprefix],
5138 error_ok=True)
5139 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005140 if issue == target_issue:
5141 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005142
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005143 branches = []
5144 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005145 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005146 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005147 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005148 return 1
5149 if len(branches) == 1:
5150 RunGit(['checkout', branches[0]])
5151 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005152 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005153 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005154 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005155 which = raw_input('Choose by index: ')
5156 try:
5157 RunGit(['checkout', branches[int(which)]])
5158 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005159 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005160 return 1
5161
5162 return 0
5163
5164
maruel@chromium.org29404b52014-09-08 22:58:00 +00005165def CMDlol(parser, args):
5166 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005167 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005168 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5169 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5170 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005171 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005172 return 0
5173
5174
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005175class OptionParser(optparse.OptionParser):
5176 """Creates the option parse and add --verbose support."""
5177 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005178 optparse.OptionParser.__init__(
5179 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005180 self.add_option(
5181 '-v', '--verbose', action='count', default=0,
5182 help='Use 2 times for more debugging info')
5183
5184 def parse_args(self, args=None, values=None):
5185 options, args = optparse.OptionParser.parse_args(self, args, values)
5186 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5187 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5188 return options, args
5189
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005191def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005192 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005193 print('\nYour python version %s is unsupported, please upgrade.\n' %
5194 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005195 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005196
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005197 # Reload settings.
5198 global settings
5199 settings = Settings()
5200
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005201 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005202 dispatcher = subcommand.CommandDispatcher(__name__)
5203 try:
5204 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005205 except auth.AuthenticationError as e:
5206 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005207 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005208 if e.code != 500:
5209 raise
5210 DieWithError(
5211 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5212 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005213 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005214
5215
5216if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005217 # These affect sys.stdout so do it outside of main() to simplify mocks in
5218 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005219 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005220 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005221 try:
5222 sys.exit(main(sys.argv[1:]))
5223 except KeyboardInterrupt:
5224 sys.stderr.write('interrupted\n')
5225 sys.exit(1)