blob: 1f62dc055b2ff25693dce38d836403806e641707 [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
tandrii5d48c322016-08-18 16:19:37 -07002309 last_upload = RunGit(['config', self._GitBranchSetting('gerritsquashhash')],
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002310 error_ok=True).strip()
2311 # Note: git diff outputs nothing if there is no diff.
2312 if not last_upload or RunGit(['diff', last_upload]).strip():
2313 print('WARNING: some changes from local branch haven\'t been uploaded')
2314 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002315 if detail['current_revision'] == last_upload:
2316 differs = False
2317 else:
2318 print('WARNING: local branch contents differ from latest uploaded '
2319 'patchset')
2320 if differs:
2321 if not force:
2322 ask_for_data(
2323 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2324 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2325 elif not bypass_hooks:
2326 hook_results = self.RunHook(
2327 committing=True,
2328 may_prompt=not force,
2329 verbose=verbose,
2330 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2331 if not hook_results.should_continue():
2332 return 1
2333
2334 self.SubmitIssue(wait_for_merge=True)
2335 print('Issue %s has been submitted.' % self.GetIssueURL())
2336 return 0
2337
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002338 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2339 directory):
2340 assert not reject
2341 assert not nocommit
2342 assert not directory
2343 assert parsed_issue_arg.valid
2344
2345 self._changelist.issue = parsed_issue_arg.issue
2346
2347 if parsed_issue_arg.hostname:
2348 self._gerrit_host = parsed_issue_arg.hostname
2349 self._gerrit_server = 'https://%s' % self._gerrit_host
2350
2351 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2352
2353 if not parsed_issue_arg.patchset:
2354 # Use current revision by default.
2355 revision_info = detail['revisions'][detail['current_revision']]
2356 patchset = int(revision_info['_number'])
2357 else:
2358 patchset = parsed_issue_arg.patchset
2359 for revision_info in detail['revisions'].itervalues():
2360 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2361 break
2362 else:
2363 DieWithError('Couldn\'t find patchset %i in issue %i' %
2364 (parsed_issue_arg.patchset, self.GetIssue()))
2365
2366 fetch_info = revision_info['fetch']['http']
2367 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2368 RunGit(['cherry-pick', 'FETCH_HEAD'])
2369 self.SetIssue(self.GetIssue())
2370 self.SetPatchset(patchset)
2371 print('Committed patch for issue %i pathset %i locally' %
2372 (self.GetIssue(), self.GetPatchset()))
2373 return 0
2374
2375 @staticmethod
2376 def ParseIssueURL(parsed_url):
2377 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2378 return None
2379 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2380 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2381 # Short urls like https://domain/<issue_number> can be used, but don't allow
2382 # specifying the patchset (you'd 404), but we allow that here.
2383 if parsed_url.path == '/':
2384 part = parsed_url.fragment
2385 else:
2386 part = parsed_url.path
2387 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2388 if match:
2389 return _ParsedIssueNumberArgument(
2390 issue=int(match.group(2)),
2391 patchset=int(match.group(4)) if match.group(4) else None,
2392 hostname=parsed_url.netloc)
2393 return None
2394
tandrii16e0b4e2016-06-07 10:34:28 -07002395 def _GerritCommitMsgHookCheck(self, offer_removal):
2396 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2397 if not os.path.exists(hook):
2398 return
2399 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2400 # custom developer made one.
2401 data = gclient_utils.FileRead(hook)
2402 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2403 return
2404 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002405 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002406 'and may interfere with it in subtle ways.\n'
2407 'We recommend you remove the commit-msg hook.')
2408 if offer_removal:
2409 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2410 if reply.lower().startswith('y'):
2411 gclient_utils.rm_file_or_tree(hook)
2412 print('Gerrit commit-msg hook removed.')
2413 else:
2414 print('OK, will keep Gerrit commit-msg hook in place.')
2415
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002416 def CMDUploadChange(self, options, args, change):
2417 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002418 if options.squash and options.no_squash:
2419 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002420
2421 if not options.squash and not options.no_squash:
2422 # Load default for user, repo, squash=true, in this order.
2423 options.squash = settings.GetSquashGerritUploads()
2424 elif options.no_squash:
2425 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002426
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002427 # We assume the remote called "origin" is the one we want.
2428 # It is probably not worthwhile to support different workflows.
2429 gerrit_remote = 'origin'
2430
2431 remote, remote_branch = self.GetRemoteBranch()
2432 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2433 pending_prefix='')
2434
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002435 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002436 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002437 if self.GetIssue():
2438 # Try to get the message from a previous upload.
2439 message = self.GetDescription()
2440 if not message:
2441 DieWithError(
2442 'failed to fetch description from current Gerrit issue %d\n'
2443 '%s' % (self.GetIssue(), self.GetIssueURL()))
2444 change_id = self._GetChangeDetail()['change_id']
2445 while True:
2446 footer_change_ids = git_footers.get_footer_change_id(message)
2447 if footer_change_ids == [change_id]:
2448 break
2449 if not footer_change_ids:
2450 message = git_footers.add_footer_change_id(message, change_id)
2451 print('WARNING: appended missing Change-Id to issue description')
2452 continue
2453 # There is already a valid footer but with different or several ids.
2454 # Doing this automatically is non-trivial as we don't want to lose
2455 # existing other footers, yet we want to append just 1 desired
2456 # Change-Id. Thus, just create a new footer, but let user verify the
2457 # new description.
2458 message = '%s\n\nChange-Id: %s' % (message, change_id)
2459 print(
2460 'WARNING: issue %s has Change-Id footer(s):\n'
2461 ' %s\n'
2462 'but issue has Change-Id %s, according to Gerrit.\n'
2463 'Please, check the proposed correction to the description, '
2464 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2465 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2466 change_id))
2467 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2468 if not options.force:
2469 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002470 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002471 message = change_desc.description
2472 if not message:
2473 DieWithError("Description is empty. Aborting...")
2474 # Continue the while loop.
2475 # Sanity check of this code - we should end up with proper message
2476 # footer.
2477 assert [change_id] == git_footers.get_footer_change_id(message)
2478 change_desc = ChangeDescription(message)
2479 else:
2480 change_desc = ChangeDescription(
2481 options.message or CreateDescriptionFromLog(args))
2482 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002483 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002484 if not change_desc.description:
2485 DieWithError("Description is empty. Aborting...")
2486 message = change_desc.description
2487 change_ids = git_footers.get_footer_change_id(message)
2488 if len(change_ids) > 1:
2489 DieWithError('too many Change-Id footers, at most 1 allowed.')
2490 if not change_ids:
2491 # Generate the Change-Id automatically.
2492 message = git_footers.add_footer_change_id(
2493 message, GenerateGerritChangeId(message))
2494 change_desc.set_description(message)
2495 change_ids = git_footers.get_footer_change_id(message)
2496 assert len(change_ids) == 1
2497 change_id = change_ids[0]
2498
2499 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2500 if remote is '.':
2501 # If our upstream branch is local, we base our squashed commit on its
2502 # squashed version.
2503 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2504 # Check the squashed hash of the parent.
2505 parent = RunGit(['config',
2506 'branch.%s.gerritsquashhash' % upstream_branch_name],
2507 error_ok=True).strip()
2508 # Verify that the upstream branch has been uploaded too, otherwise
2509 # Gerrit will create additional CLs when uploading.
2510 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2511 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002512 DieWithError(
2513 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002514 'Note: maybe you\'ve uploaded it with --no-squash. '
2515 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002516 ' git cl upload --squash\n' % upstream_branch_name)
2517 else:
2518 parent = self.GetCommonAncestorWithUpstream()
2519
2520 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2521 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2522 '-m', message]).strip()
2523 else:
2524 change_desc = ChangeDescription(
2525 options.message or CreateDescriptionFromLog(args))
2526 if not change_desc.description:
2527 DieWithError("Description is empty. Aborting...")
2528
2529 if not git_footers.get_footer_change_id(change_desc.description):
2530 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002531 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2532 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002533 ref_to_push = 'HEAD'
2534 parent = '%s/%s' % (gerrit_remote, branch)
2535 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2536
2537 assert change_desc
2538 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2539 ref_to_push)]).splitlines()
2540 if len(commits) > 1:
2541 print('WARNING: This will upload %d commits. Run the following command '
2542 'to see which commits will be uploaded: ' % len(commits))
2543 print('git log %s..%s' % (parent, ref_to_push))
2544 print('You can also use `git squash-branch` to squash these into a '
2545 'single commit.')
2546 ask_for_data('About to upload; enter to confirm.')
2547
2548 if options.reviewers or options.tbr_owners:
2549 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2550 change)
2551
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002552 # Extra options that can be specified at push time. Doc:
2553 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2554 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002555 if change_desc.get_reviewers(tbr_only=True):
2556 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2557 refspec_opts.append('l=Code-Review+1')
2558
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002559 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002560 if not re.match(r'^[\w ]+$', options.title):
2561 options.title = re.sub(r'[^\w ]', '', options.title)
2562 print('WARNING: Patchset title may only contain alphanumeric chars '
2563 'and spaces. Cleaned up title:\n%s' % options.title)
2564 if not options.force:
2565 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002566 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2567 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002568 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2569
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002570 if options.send_mail:
2571 if not change_desc.get_reviewers():
2572 DieWithError('Must specify reviewers to send email.')
2573 refspec_opts.append('notify=ALL')
2574 else:
2575 refspec_opts.append('notify=NONE')
2576
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002577 cc = self.GetCCList().split(',')
2578 if options.cc:
2579 cc.extend(options.cc)
2580 cc = filter(None, cc)
2581 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002582 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002583
tandrii99a72f22016-08-17 14:33:24 -07002584 reviewers = change_desc.get_reviewers()
2585 if reviewers:
2586 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002587
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002588 refspec_suffix = ''
2589 if refspec_opts:
2590 refspec_suffix = '%' + ','.join(refspec_opts)
2591 assert ' ' not in refspec_suffix, (
2592 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002593 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002594
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002595 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002596 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002597 print_stdout=True,
2598 # Flush after every line: useful for seeing progress when running as
2599 # recipe.
2600 filter_fn=lambda _: sys.stdout.flush())
2601
2602 if options.squash:
2603 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2604 change_numbers = [m.group(1)
2605 for m in map(regex.match, push_stdout.splitlines())
2606 if m]
2607 if len(change_numbers) != 1:
2608 DieWithError(
2609 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2610 'Change-Id: %s') % (len(change_numbers), change_id))
2611 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002612 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002613 return 0
2614
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002615 def _AddChangeIdToCommitMessage(self, options, args):
2616 """Re-commits using the current message, assumes the commit hook is in
2617 place.
2618 """
2619 log_desc = options.message or CreateDescriptionFromLog(args)
2620 git_command = ['commit', '--amend', '-m', log_desc]
2621 RunGit(git_command)
2622 new_log_desc = CreateDescriptionFromLog(args)
2623 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002624 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002625 return new_log_desc
2626 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002627 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002628
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002629 def SetCQState(self, new_state):
2630 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002631 vote_map = {
2632 _CQState.NONE: 0,
2633 _CQState.DRY_RUN: 1,
2634 _CQState.COMMIT : 2,
2635 }
2636 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2637 labels={'Commit-Queue': vote_map[new_state]})
2638
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002639
2640_CODEREVIEW_IMPLEMENTATIONS = {
2641 'rietveld': _RietveldChangelistImpl,
2642 'gerrit': _GerritChangelistImpl,
2643}
2644
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002645
iannuccie53c9352016-08-17 14:40:40 -07002646def _add_codereview_issue_select_options(parser, extra=""):
2647 _add_codereview_select_options(parser)
2648
2649 text = ('Operate on this issue number instead of the current branch\'s '
2650 'implicit issue.')
2651 if extra:
2652 text += ' '+extra
2653 parser.add_option('-i', '--issue', type=int, help=text)
2654
2655
2656def _process_codereview_issue_select_options(parser, options):
2657 _process_codereview_select_options(parser, options)
2658 if options.issue is not None and not options.forced_codereview:
2659 parser.error('--issue must be specified with either --rietveld or --gerrit')
2660
2661
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002662def _add_codereview_select_options(parser):
2663 """Appends --gerrit and --rietveld options to force specific codereview."""
2664 parser.codereview_group = optparse.OptionGroup(
2665 parser, 'EXPERIMENTAL! Codereview override options')
2666 parser.add_option_group(parser.codereview_group)
2667 parser.codereview_group.add_option(
2668 '--gerrit', action='store_true',
2669 help='Force the use of Gerrit for codereview')
2670 parser.codereview_group.add_option(
2671 '--rietveld', action='store_true',
2672 help='Force the use of Rietveld for codereview')
2673
2674
2675def _process_codereview_select_options(parser, options):
2676 if options.gerrit and options.rietveld:
2677 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2678 options.forced_codereview = None
2679 if options.gerrit:
2680 options.forced_codereview = 'gerrit'
2681 elif options.rietveld:
2682 options.forced_codereview = 'rietveld'
2683
2684
tandriif9aefb72016-07-01 09:06:51 -07002685def _get_bug_line_values(default_project, bugs):
2686 """Given default_project and comma separated list of bugs, yields bug line
2687 values.
2688
2689 Each bug can be either:
2690 * a number, which is combined with default_project
2691 * string, which is left as is.
2692
2693 This function may produce more than one line, because bugdroid expects one
2694 project per line.
2695
2696 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2697 ['v8:123', 'chromium:789']
2698 """
2699 default_bugs = []
2700 others = []
2701 for bug in bugs.split(','):
2702 bug = bug.strip()
2703 if bug:
2704 try:
2705 default_bugs.append(int(bug))
2706 except ValueError:
2707 others.append(bug)
2708
2709 if default_bugs:
2710 default_bugs = ','.join(map(str, default_bugs))
2711 if default_project:
2712 yield '%s:%s' % (default_project, default_bugs)
2713 else:
2714 yield default_bugs
2715 for other in sorted(others):
2716 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2717 yield other
2718
2719
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002720class ChangeDescription(object):
2721 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002722 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002723 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002724
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002725 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002726 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002727
agable@chromium.org42c20792013-09-12 17:34:49 +00002728 @property # www.logilab.org/ticket/89786
2729 def description(self): # pylint: disable=E0202
2730 return '\n'.join(self._description_lines)
2731
2732 def set_description(self, desc):
2733 if isinstance(desc, basestring):
2734 lines = desc.splitlines()
2735 else:
2736 lines = [line.rstrip() for line in desc]
2737 while lines and not lines[0]:
2738 lines.pop(0)
2739 while lines and not lines[-1]:
2740 lines.pop(-1)
2741 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002742
piman@chromium.org336f9122014-09-04 02:16:55 +00002743 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002744 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002745 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002746 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002747 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002748 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002749
agable@chromium.org42c20792013-09-12 17:34:49 +00002750 # Get the set of R= and TBR= lines and remove them from the desciption.
2751 regexp = re.compile(self.R_LINE)
2752 matches = [regexp.match(line) for line in self._description_lines]
2753 new_desc = [l for i, l in enumerate(self._description_lines)
2754 if not matches[i]]
2755 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002756
agable@chromium.org42c20792013-09-12 17:34:49 +00002757 # Construct new unified R= and TBR= lines.
2758 r_names = []
2759 tbr_names = []
2760 for match in matches:
2761 if not match:
2762 continue
2763 people = cleanup_list([match.group(2).strip()])
2764 if match.group(1) == 'TBR':
2765 tbr_names.extend(people)
2766 else:
2767 r_names.extend(people)
2768 for name in r_names:
2769 if name not in reviewers:
2770 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002771 if add_owners_tbr:
2772 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002773 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002774 all_reviewers = set(tbr_names + reviewers)
2775 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2776 all_reviewers)
2777 tbr_names.extend(owners_db.reviewers_for(missing_files,
2778 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002779 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2780 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2781
2782 # Put the new lines in the description where the old first R= line was.
2783 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2784 if 0 <= line_loc < len(self._description_lines):
2785 if new_tbr_line:
2786 self._description_lines.insert(line_loc, new_tbr_line)
2787 if new_r_line:
2788 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002789 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002790 if new_r_line:
2791 self.append_footer(new_r_line)
2792 if new_tbr_line:
2793 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002794
tandriif9aefb72016-07-01 09:06:51 -07002795 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002796 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002797 self.set_description([
2798 '# Enter a description of the change.',
2799 '# This will be displayed on the codereview site.',
2800 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002801 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002802 '--------------------',
2803 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002804
agable@chromium.org42c20792013-09-12 17:34:49 +00002805 regexp = re.compile(self.BUG_LINE)
2806 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002807 prefix = settings.GetBugPrefix()
2808 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2809 for value in values:
2810 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2811 self.append_footer('BUG=%s' % value)
2812
agable@chromium.org42c20792013-09-12 17:34:49 +00002813 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002814 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002815 if not content:
2816 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002817 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002818
2819 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002820 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2821 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002822 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002823 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002824
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002825 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002826 """Adds a footer line to the description.
2827
2828 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2829 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2830 that Gerrit footers are always at the end.
2831 """
2832 parsed_footer_line = git_footers.parse_footer(line)
2833 if parsed_footer_line:
2834 # Line is a gerrit footer in the form: Footer-Key: any value.
2835 # Thus, must be appended observing Gerrit footer rules.
2836 self.set_description(
2837 git_footers.add_footer(self.description,
2838 key=parsed_footer_line[0],
2839 value=parsed_footer_line[1]))
2840 return
2841
2842 if not self._description_lines:
2843 self._description_lines.append(line)
2844 return
2845
2846 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2847 if gerrit_footers:
2848 # git_footers.split_footers ensures that there is an empty line before
2849 # actual (gerrit) footers, if any. We have to keep it that way.
2850 assert top_lines and top_lines[-1] == ''
2851 top_lines, separator = top_lines[:-1], top_lines[-1:]
2852 else:
2853 separator = [] # No need for separator if there are no gerrit_footers.
2854
2855 prev_line = top_lines[-1] if top_lines else ''
2856 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2857 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2858 top_lines.append('')
2859 top_lines.append(line)
2860 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002861
tandrii99a72f22016-08-17 14:33:24 -07002862 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002863 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002864 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002865 reviewers = [match.group(2).strip()
2866 for match in matches
2867 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002868 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002869
2870
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002871def get_approving_reviewers(props):
2872 """Retrieves the reviewers that approved a CL from the issue properties with
2873 messages.
2874
2875 Note that the list may contain reviewers that are not committer, thus are not
2876 considered by the CQ.
2877 """
2878 return sorted(
2879 set(
2880 message['sender']
2881 for message in props['messages']
2882 if message['approval'] and message['sender'] in props['reviewers']
2883 )
2884 )
2885
2886
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002887def FindCodereviewSettingsFile(filename='codereview.settings'):
2888 """Finds the given file starting in the cwd and going up.
2889
2890 Only looks up to the top of the repository unless an
2891 'inherit-review-settings-ok' file exists in the root of the repository.
2892 """
2893 inherit_ok_file = 'inherit-review-settings-ok'
2894 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002895 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002896 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2897 root = '/'
2898 while True:
2899 if filename in os.listdir(cwd):
2900 if os.path.isfile(os.path.join(cwd, filename)):
2901 return open(os.path.join(cwd, filename))
2902 if cwd == root:
2903 break
2904 cwd = os.path.dirname(cwd)
2905
2906
2907def LoadCodereviewSettingsFromFile(fileobj):
2908 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002909 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911 def SetProperty(name, setting, unset_error_ok=False):
2912 fullname = 'rietveld.' + name
2913 if setting in keyvals:
2914 RunGit(['config', fullname, keyvals[setting]])
2915 else:
2916 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2917
2918 SetProperty('server', 'CODE_REVIEW_SERVER')
2919 # Only server setting is required. Other settings can be absent.
2920 # In that case, we ignore errors raised during option deletion attempt.
2921 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002922 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002923 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2924 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002925 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002926 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002927 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2928 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002929 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002930 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002931 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002932 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2933 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002935 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002936 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002937
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002938 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002939 RunGit(['config', 'gerrit.squash-uploads',
2940 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002941
tandrii@chromium.org28253532016-04-14 13:46:56 +00002942 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002943 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002944 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2947 #should be of the form
2948 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2949 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2950 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2951 keyvals['ORIGIN_URL_CONFIG']])
2952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002953
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002954def urlretrieve(source, destination):
2955 """urllib is broken for SSL connections via a proxy therefore we
2956 can't use urllib.urlretrieve()."""
2957 with open(destination, 'w') as f:
2958 f.write(urllib2.urlopen(source).read())
2959
2960
ukai@chromium.org712d6102013-11-27 00:52:58 +00002961def hasSheBang(fname):
2962 """Checks fname is a #! script."""
2963 with open(fname) as f:
2964 return f.read(2).startswith('#!')
2965
2966
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002967# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2968def DownloadHooks(*args, **kwargs):
2969 pass
2970
2971
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002972def DownloadGerritHook(force):
2973 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002974
2975 Args:
2976 force: True to update hooks. False to install hooks if not present.
2977 """
2978 if not settings.GetIsGerrit():
2979 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002980 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002981 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2982 if not os.access(dst, os.X_OK):
2983 if os.path.exists(dst):
2984 if not force:
2985 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002986 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002987 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002988 if not hasSheBang(dst):
2989 DieWithError('Not a script: %s\n'
2990 'You need to download from\n%s\n'
2991 'into .git/hooks/commit-msg and '
2992 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002993 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2994 except Exception:
2995 if os.path.exists(dst):
2996 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002997 DieWithError('\nFailed to download hooks.\n'
2998 'You need to download from\n%s\n'
2999 'into .git/hooks/commit-msg and '
3000 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003001
3002
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003003
3004def GetRietveldCodereviewSettingsInteractively():
3005 """Prompt the user for settings."""
3006 server = settings.GetDefaultServerUrl(error_ok=True)
3007 prompt = 'Rietveld server (host[:port])'
3008 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3009 newserver = ask_for_data(prompt + ':')
3010 if not server and not newserver:
3011 newserver = DEFAULT_SERVER
3012 if newserver:
3013 newserver = gclient_utils.UpgradeToHttps(newserver)
3014 if newserver != server:
3015 RunGit(['config', 'rietveld.server', newserver])
3016
3017 def SetProperty(initial, caption, name, is_url):
3018 prompt = caption
3019 if initial:
3020 prompt += ' ("x" to clear) [%s]' % initial
3021 new_val = ask_for_data(prompt + ':')
3022 if new_val == 'x':
3023 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3024 elif new_val:
3025 if is_url:
3026 new_val = gclient_utils.UpgradeToHttps(new_val)
3027 if new_val != initial:
3028 RunGit(['config', 'rietveld.' + name, new_val])
3029
3030 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3031 SetProperty(settings.GetDefaultPrivateFlag(),
3032 'Private flag (rietveld only)', 'private', False)
3033 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3034 'tree-status-url', False)
3035 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3036 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3037 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3038 'run-post-upload-hook', False)
3039
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003040@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003041def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003042 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003043
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003044 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003045 'For Gerrit, see http://crbug.com/603116.')
3046 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003047 parser.add_option('--activate-update', action='store_true',
3048 help='activate auto-updating [rietveld] section in '
3049 '.git/config')
3050 parser.add_option('--deactivate-update', action='store_true',
3051 help='deactivate auto-updating [rietveld] section in '
3052 '.git/config')
3053 options, args = parser.parse_args(args)
3054
3055 if options.deactivate_update:
3056 RunGit(['config', 'rietveld.autoupdate', 'false'])
3057 return
3058
3059 if options.activate_update:
3060 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3061 return
3062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003063 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003064 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065 return 0
3066
3067 url = args[0]
3068 if not url.endswith('codereview.settings'):
3069 url = os.path.join(url, 'codereview.settings')
3070
3071 # Load code review settings and download hooks (if available).
3072 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3073 return 0
3074
3075
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003076def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003077 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003078 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3079 branch = ShortBranchName(branchref)
3080 _, args = parser.parse_args(args)
3081 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003082 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003083 return RunGit(['config', 'branch.%s.base-url' % branch],
3084 error_ok=False).strip()
3085 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003086 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003087 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3088 error_ok=False).strip()
3089
3090
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003091def color_for_status(status):
3092 """Maps a Changelist status to color, for CMDstatus and other tools."""
3093 return {
3094 'unsent': Fore.RED,
3095 'waiting': Fore.BLUE,
3096 'reply': Fore.YELLOW,
3097 'lgtm': Fore.GREEN,
3098 'commit': Fore.MAGENTA,
3099 'closed': Fore.CYAN,
3100 'error': Fore.WHITE,
3101 }.get(status, Fore.WHITE)
3102
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003103
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003104def get_cl_statuses(changes, fine_grained, max_processes=None):
3105 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003106
3107 If fine_grained is true, this will fetch CL statuses from the server.
3108 Otherwise, simply indicate if there's a matching url for the given branches.
3109
3110 If max_processes is specified, it is used as the maximum number of processes
3111 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3112 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003113
3114 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003115 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003116 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003117 upload.verbosity = 0
3118
3119 if fine_grained:
3120 # Process one branch synchronously to work through authentication, then
3121 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003122 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003123 def fetch(cl):
3124 try:
3125 return (cl, cl.GetStatus())
3126 except:
3127 # See http://crbug.com/629863.
3128 logging.exception('failed to fetch status for %s:', cl)
3129 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003130 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003131
tandriiea9514a2016-08-17 12:32:37 -07003132 changes_to_fetch = changes[1:]
3133 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003134 # Exit early if there was only one branch to fetch.
3135 return
3136
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003137 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003138 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003139 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003140 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003141
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003142 fetched_cls = set()
3143 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003144 while True:
3145 try:
3146 row = it.next(timeout=5)
3147 except multiprocessing.TimeoutError:
3148 break
3149
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003150 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003151 yield row
3152
3153 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003154 for cl in set(changes_to_fetch) - fetched_cls:
3155 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003156
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003157 else:
3158 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003159 for cl in changes:
3160 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003161
rmistry@google.com2dd99862015-06-22 12:22:18 +00003162
3163def upload_branch_deps(cl, args):
3164 """Uploads CLs of local branches that are dependents of the current branch.
3165
3166 If the local branch dependency tree looks like:
3167 test1 -> test2.1 -> test3.1
3168 -> test3.2
3169 -> test2.2 -> test3.3
3170
3171 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3172 run on the dependent branches in this order:
3173 test2.1, test3.1, test3.2, test2.2, test3.3
3174
3175 Note: This function does not rebase your local dependent branches. Use it when
3176 you make a change to the parent branch that will not conflict with its
3177 dependent branches, and you would like their dependencies updated in
3178 Rietveld.
3179 """
3180 if git_common.is_dirty_git_tree('upload-branch-deps'):
3181 return 1
3182
3183 root_branch = cl.GetBranch()
3184 if root_branch is None:
3185 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3186 'Get on a branch!')
3187 if not cl.GetIssue() or not cl.GetPatchset():
3188 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3189 'patchset dependencies without an uploaded CL.')
3190
3191 branches = RunGit(['for-each-ref',
3192 '--format=%(refname:short) %(upstream:short)',
3193 'refs/heads'])
3194 if not branches:
3195 print('No local branches found.')
3196 return 0
3197
3198 # Create a dictionary of all local branches to the branches that are dependent
3199 # on it.
3200 tracked_to_dependents = collections.defaultdict(list)
3201 for b in branches.splitlines():
3202 tokens = b.split()
3203 if len(tokens) == 2:
3204 branch_name, tracked = tokens
3205 tracked_to_dependents[tracked].append(branch_name)
3206
vapiera7fbd5a2016-06-16 09:17:49 -07003207 print()
3208 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003209 dependents = []
3210 def traverse_dependents_preorder(branch, padding=''):
3211 dependents_to_process = tracked_to_dependents.get(branch, [])
3212 padding += ' '
3213 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003214 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003215 dependents.append(dependent)
3216 traverse_dependents_preorder(dependent, padding)
3217 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003218 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003219
3220 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003221 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003222 return 0
3223
vapiera7fbd5a2016-06-16 09:17:49 -07003224 print('This command will checkout all dependent branches and run '
3225 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003226 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3227
andybons@chromium.org962f9462016-02-03 20:00:42 +00003228 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003229 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003230 args.extend(['-t', 'Updated patchset dependency'])
3231
rmistry@google.com2dd99862015-06-22 12:22:18 +00003232 # Record all dependents that failed to upload.
3233 failures = {}
3234 # Go through all dependents, checkout the branch and upload.
3235 try:
3236 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003237 print()
3238 print('--------------------------------------')
3239 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003240 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003241 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003242 try:
3243 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003244 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003245 failures[dependent_branch] = 1
3246 except: # pylint: disable=W0702
3247 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003248 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003249 finally:
3250 # Swap back to the original root branch.
3251 RunGit(['checkout', '-q', root_branch])
3252
vapiera7fbd5a2016-06-16 09:17:49 -07003253 print()
3254 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003255 for dependent_branch in dependents:
3256 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003257 print(' %s : %s' % (dependent_branch, upload_status))
3258 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003259
3260 return 0
3261
3262
kmarshall3bff56b2016-06-06 18:31:47 -07003263def CMDarchive(parser, args):
3264 """Archives and deletes branches associated with closed changelists."""
3265 parser.add_option(
3266 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003267 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003268 parser.add_option(
3269 '-f', '--force', action='store_true',
3270 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003271 parser.add_option(
3272 '-d', '--dry-run', action='store_true',
3273 help='Skip the branch tagging and removal steps.')
3274 parser.add_option(
3275 '-t', '--notags', action='store_true',
3276 help='Do not tag archived branches. '
3277 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003278
3279 auth.add_auth_options(parser)
3280 options, args = parser.parse_args(args)
3281 if args:
3282 parser.error('Unsupported args: %s' % ' '.join(args))
3283 auth_config = auth.extract_auth_config_from_options(options)
3284
3285 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3286 if not branches:
3287 return 0
3288
vapiera7fbd5a2016-06-16 09:17:49 -07003289 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003290 changes = [Changelist(branchref=b, auth_config=auth_config)
3291 for b in branches.splitlines()]
3292 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3293 statuses = get_cl_statuses(changes,
3294 fine_grained=True,
3295 max_processes=options.maxjobs)
3296 proposal = [(cl.GetBranch(),
3297 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3298 for cl, status in statuses
3299 if status == 'closed']
3300 proposal.sort()
3301
3302 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003303 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003304 return 0
3305
3306 current_branch = GetCurrentBranch()
3307
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003309 if options.notags:
3310 for next_item in proposal:
3311 print(' ' + next_item[0])
3312 else:
3313 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3314 for next_item in proposal:
3315 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003316
kmarshall9249e012016-08-23 12:02:16 -07003317 # Quit now on precondition failure or if instructed by the user, either
3318 # via an interactive prompt or by command line flags.
3319 if options.dry_run:
3320 print('\nNo changes were made (dry run).\n')
3321 return 0
3322 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003323 print('You are currently on a branch \'%s\' which is associated with a '
3324 'closed codereview issue, so archive cannot proceed. Please '
3325 'checkout another branch and run this command again.' %
3326 current_branch)
3327 return 1
kmarshall9249e012016-08-23 12:02:16 -07003328 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003329 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3330 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003331 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003332 return 1
3333
3334 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003335 if not options.notags:
3336 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003337 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003338
vapiera7fbd5a2016-06-16 09:17:49 -07003339 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003340
3341 return 0
3342
3343
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003344def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003345 """Show status of changelists.
3346
3347 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003348 - Red not sent for review or broken
3349 - Blue waiting for review
3350 - Yellow waiting for you to reply to review
3351 - Green LGTM'ed
3352 - Magenta in the commit queue
3353 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003354
3355 Also see 'git cl comments'.
3356 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003357 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003358 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003359 parser.add_option('-f', '--fast', action='store_true',
3360 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003361 parser.add_option(
3362 '-j', '--maxjobs', action='store', type=int,
3363 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003364
3365 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003366 _add_codereview_issue_select_options(
3367 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003368 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003369 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003370 if args:
3371 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003372 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003373
iannuccie53c9352016-08-17 14:40:40 -07003374 if options.issue is not None and not options.field:
3375 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003378 cl = Changelist(auth_config=auth_config, issue=options.issue,
3379 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003380 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003381 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382 elif options.field == 'id':
3383 issueid = cl.GetIssue()
3384 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003385 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003386 elif options.field == 'patch':
3387 patchset = cl.GetPatchset()
3388 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003389 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003390 elif options.field == 'status':
3391 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392 elif options.field == 'url':
3393 url = cl.GetIssueURL()
3394 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003395 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003396 return 0
3397
3398 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3399 if not branches:
3400 print('No local branch found.')
3401 return 0
3402
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003403 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003404 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003405 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003406 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003407 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003408 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003409 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003410
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003411 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003412 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3413 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3414 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003415 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003416 c, status = output.next()
3417 branch_statuses[c.GetBranch()] = status
3418 status = branch_statuses.pop(branch)
3419 url = cl.GetIssueURL()
3420 if url and (not status or status == 'error'):
3421 # The issue probably doesn't exist anymore.
3422 url += ' (broken)'
3423
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003424 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003425 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003426 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003427 color = ''
3428 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003429 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003430 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003431 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003432 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003433
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003434 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003435 print()
3436 print('Current branch:',)
3437 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003438 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003439 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003440 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003441 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003442 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003443 print('Issue description:')
3444 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003445 return 0
3446
3447
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003448def colorize_CMDstatus_doc():
3449 """To be called once in main() to add colors to git cl status help."""
3450 colors = [i for i in dir(Fore) if i[0].isupper()]
3451
3452 def colorize_line(line):
3453 for color in colors:
3454 if color in line.upper():
3455 # Extract whitespaces first and the leading '-'.
3456 indent = len(line) - len(line.lstrip(' ')) + 1
3457 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3458 return line
3459
3460 lines = CMDstatus.__doc__.splitlines()
3461 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3462
3463
phajdan.jre328cf92016-08-22 04:12:17 -07003464def write_json(path, contents):
3465 with open(path, 'w') as f:
3466 json.dump(contents, f)
3467
3468
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003469@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003470def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003471 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003472
3473 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003474 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003475 parser.add_option('-r', '--reverse', action='store_true',
3476 help='Lookup the branch(es) for the specified issues. If '
3477 'no issues are specified, all branches with mapped '
3478 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003479 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003480 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003481 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003482 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003483
dnj@chromium.org406c4402015-03-03 17:22:28 +00003484 if options.reverse:
3485 branches = RunGit(['for-each-ref', 'refs/heads',
3486 '--format=%(refname:short)']).splitlines()
3487
3488 # Reverse issue lookup.
3489 issue_branch_map = {}
3490 for branch in branches:
3491 cl = Changelist(branchref=branch)
3492 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3493 if not args:
3494 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003495 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003496 for issue in args:
3497 if not issue:
3498 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003499 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003500 print('Branch for issue number %s: %s' % (
3501 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003502 if options.json:
3503 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003504 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003505 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003506 if len(args) > 0:
3507 try:
3508 issue = int(args[0])
3509 except ValueError:
3510 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003511 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003512 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003513 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003514 if options.json:
3515 write_json(options.json, {
3516 'issue': cl.GetIssue(),
3517 'issue_url': cl.GetIssueURL(),
3518 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003519 return 0
3520
3521
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003522def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003523 """Shows or posts review comments for any changelist."""
3524 parser.add_option('-a', '--add-comment', dest='comment',
3525 help='comment to add to an issue')
3526 parser.add_option('-i', dest='issue',
3527 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003528 parser.add_option('-j', '--json-file',
3529 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003530 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003531 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003532 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003533
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003534 issue = None
3535 if options.issue:
3536 try:
3537 issue = int(options.issue)
3538 except ValueError:
3539 DieWithError('A review issue id is expected to be a number')
3540
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003541 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003542
3543 if options.comment:
3544 cl.AddComment(options.comment)
3545 return 0
3546
3547 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003548 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003549 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003550 summary.append({
3551 'date': message['date'],
3552 'lgtm': False,
3553 'message': message['text'],
3554 'not_lgtm': False,
3555 'sender': message['sender'],
3556 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003557 if message['disapproval']:
3558 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003559 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003560 elif message['approval']:
3561 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003562 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003563 elif message['sender'] == data['owner_email']:
3564 color = Fore.MAGENTA
3565 else:
3566 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003568 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003569 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003570 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003572 if options.json_file:
3573 with open(options.json_file, 'wb') as f:
3574 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003575 return 0
3576
3577
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003578@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003579def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003580 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003581 parser.add_option('-d', '--display', action='store_true',
3582 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003583 parser.add_option('-n', '--new-description',
3584 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003585
3586 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003587 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003588 options, args = parser.parse_args(args)
3589 _process_codereview_select_options(parser, options)
3590
3591 target_issue = None
3592 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003593 target_issue = ParseIssueNumberArgument(args[0])
3594 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003595 parser.print_help()
3596 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003597
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003598 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003599
martiniss6eda05f2016-06-30 10:18:35 -07003600 kwargs = {
3601 'auth_config': auth_config,
3602 'codereview': options.forced_codereview,
3603 }
3604 if target_issue:
3605 kwargs['issue'] = target_issue.issue
3606 if options.forced_codereview == 'rietveld':
3607 kwargs['rietveld_server'] = target_issue.hostname
3608
3609 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003610
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003611 if not cl.GetIssue():
3612 DieWithError('This branch has no associated changelist.')
3613 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003614
smut@google.com34fb6b12015-07-13 20:03:26 +00003615 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003616 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003617 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003618
3619 if options.new_description:
3620 text = options.new_description
3621 if text == '-':
3622 text = '\n'.join(l.rstrip() for l in sys.stdin)
3623
3624 description.set_description(text)
3625 else:
3626 description.prompt()
3627
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003628 if cl.GetDescription() != description.description:
3629 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003630 return 0
3631
3632
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003633def CreateDescriptionFromLog(args):
3634 """Pulls out the commit log to use as a base for the CL description."""
3635 log_args = []
3636 if len(args) == 1 and not args[0].endswith('.'):
3637 log_args = [args[0] + '..']
3638 elif len(args) == 1 and args[0].endswith('...'):
3639 log_args = [args[0][:-1]]
3640 elif len(args) == 2:
3641 log_args = [args[0] + '..' + args[1]]
3642 else:
3643 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003644 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645
3646
thestig@chromium.org44202a22014-03-11 19:22:18 +00003647def CMDlint(parser, args):
3648 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003649 parser.add_option('--filter', action='append', metavar='-x,+y',
3650 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003651 auth.add_auth_options(parser)
3652 options, args = parser.parse_args(args)
3653 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003654
3655 # Access to a protected member _XX of a client class
3656 # pylint: disable=W0212
3657 try:
3658 import cpplint
3659 import cpplint_chromium
3660 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003661 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003662 return 1
3663
3664 # Change the current working directory before calling lint so that it
3665 # shows the correct base.
3666 previous_cwd = os.getcwd()
3667 os.chdir(settings.GetRoot())
3668 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003669 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003670 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3671 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003672 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003673 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003674 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003675
3676 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003677 command = args + files
3678 if options.filter:
3679 command = ['--filter=' + ','.join(options.filter)] + command
3680 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003681
3682 white_regex = re.compile(settings.GetLintRegex())
3683 black_regex = re.compile(settings.GetLintIgnoreRegex())
3684 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3685 for filename in filenames:
3686 if white_regex.match(filename):
3687 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003688 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003689 else:
3690 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3691 extra_check_functions)
3692 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003693 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003694 finally:
3695 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003696 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003697 if cpplint._cpplint_state.error_count != 0:
3698 return 1
3699 return 0
3700
3701
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003703 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003704 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003705 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003706 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003707 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003708 auth.add_auth_options(parser)
3709 options, args = parser.parse_args(args)
3710 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711
sbc@chromium.org71437c02015-04-09 19:29:40 +00003712 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003713 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714 return 1
3715
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003716 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 if args:
3718 base_branch = args[0]
3719 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003720 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003721 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003722
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003723 cl.RunHook(
3724 committing=not options.upload,
3725 may_prompt=False,
3726 verbose=options.verbose,
3727 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003728 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729
3730
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003731def GenerateGerritChangeId(message):
3732 """Returns Ixxxxxx...xxx change id.
3733
3734 Works the same way as
3735 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3736 but can be called on demand on all platforms.
3737
3738 The basic idea is to generate git hash of a state of the tree, original commit
3739 message, author/committer info and timestamps.
3740 """
3741 lines = []
3742 tree_hash = RunGitSilent(['write-tree'])
3743 lines.append('tree %s' % tree_hash.strip())
3744 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3745 if code == 0:
3746 lines.append('parent %s' % parent.strip())
3747 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3748 lines.append('author %s' % author.strip())
3749 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3750 lines.append('committer %s' % committer.strip())
3751 lines.append('')
3752 # Note: Gerrit's commit-hook actually cleans message of some lines and
3753 # whitespace. This code is not doing this, but it clearly won't decrease
3754 # entropy.
3755 lines.append(message)
3756 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3757 stdin='\n'.join(lines))
3758 return 'I%s' % change_hash.strip()
3759
3760
wittman@chromium.org455dc922015-01-26 20:15:50 +00003761def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3762 """Computes the remote branch ref to use for the CL.
3763
3764 Args:
3765 remote (str): The git remote for the CL.
3766 remote_branch (str): The git remote branch for the CL.
3767 target_branch (str): The target branch specified by the user.
3768 pending_prefix (str): The pending prefix from the settings.
3769 """
3770 if not (remote and remote_branch):
3771 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003772
wittman@chromium.org455dc922015-01-26 20:15:50 +00003773 if target_branch:
3774 # Cannonicalize branch references to the equivalent local full symbolic
3775 # refs, which are then translated into the remote full symbolic refs
3776 # below.
3777 if '/' not in target_branch:
3778 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3779 else:
3780 prefix_replacements = (
3781 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3782 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3783 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3784 )
3785 match = None
3786 for regex, replacement in prefix_replacements:
3787 match = re.search(regex, target_branch)
3788 if match:
3789 remote_branch = target_branch.replace(match.group(0), replacement)
3790 break
3791 if not match:
3792 # This is a branch path but not one we recognize; use as-is.
3793 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003794 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3795 # Handle the refs that need to land in different refs.
3796 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003797
wittman@chromium.org455dc922015-01-26 20:15:50 +00003798 # Create the true path to the remote branch.
3799 # Does the following translation:
3800 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3801 # * refs/remotes/origin/master -> refs/heads/master
3802 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3803 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3804 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3805 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3806 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3807 'refs/heads/')
3808 elif remote_branch.startswith('refs/remotes/branch-heads'):
3809 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3810 # If a pending prefix exists then replace refs/ with it.
3811 if pending_prefix:
3812 remote_branch = remote_branch.replace('refs/', pending_prefix)
3813 return remote_branch
3814
3815
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003816def cleanup_list(l):
3817 """Fixes a list so that comma separated items are put as individual items.
3818
3819 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3820 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3821 """
3822 items = sum((i.split(',') for i in l), [])
3823 stripped_items = (i.strip() for i in items)
3824 return sorted(filter(None, stripped_items))
3825
3826
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003827@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003828def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003829 """Uploads the current changelist to codereview.
3830
3831 Can skip dependency patchset uploads for a branch by running:
3832 git config branch.branch_name.skip-deps-uploads True
3833 To unset run:
3834 git config --unset branch.branch_name.skip-deps-uploads
3835 Can also set the above globally by using the --global flag.
3836 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003837 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3838 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003839 parser.add_option('--bypass-watchlists', action='store_true',
3840 dest='bypass_watchlists',
3841 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003842 parser.add_option('-f', action='store_true', dest='force',
3843 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003844 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003845 parser.add_option('-b', '--bug',
3846 help='pre-populate the bug number(s) for this issue. '
3847 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003848 parser.add_option('--message-file', dest='message_file',
3849 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003850 parser.add_option('-t', dest='title',
3851 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003852 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003853 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003854 help='reviewer email addresses')
3855 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003856 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003857 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003858 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003859 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003860 parser.add_option('--emulate_svn_auto_props',
3861 '--emulate-svn-auto-props',
3862 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003863 dest="emulate_svn_auto_props",
3864 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003865 parser.add_option('-c', '--use-commit-queue', action='store_true',
3866 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003867 parser.add_option('--private', action='store_true',
3868 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003869 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003870 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003871 metavar='TARGET',
3872 help='Apply CL to remote ref TARGET. ' +
3873 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003874 parser.add_option('--squash', action='store_true',
3875 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003876 parser.add_option('--no-squash', action='store_true',
3877 help='Don\'t squash multiple commits into one ' +
3878 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003879 parser.add_option('--email', default=None,
3880 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003881 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3882 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003883 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3884 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003885 help='Send the patchset to do a CQ dry run right after '
3886 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003887 parser.add_option('--dependencies', action='store_true',
3888 help='Uploads CLs of all the local branches that depend on '
3889 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003890
rmistry@google.com2dd99862015-06-22 12:22:18 +00003891 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003892 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003893 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003894 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003895 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003896 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003897 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003898
sbc@chromium.org71437c02015-04-09 19:29:40 +00003899 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003900 return 1
3901
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003902 options.reviewers = cleanup_list(options.reviewers)
3903 options.cc = cleanup_list(options.cc)
3904
tandriib80458a2016-06-23 12:20:07 -07003905 if options.message_file:
3906 if options.message:
3907 parser.error('only one of --message and --message-file allowed.')
3908 options.message = gclient_utils.FileRead(options.message_file)
3909 options.message_file = None
3910
tandrii4d0545a2016-07-06 03:56:49 -07003911 if options.cq_dry_run and options.use_commit_queue:
3912 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3913
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003914 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3915 settings.GetIsGerrit()
3916
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003917 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003918 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003919
3920
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003921def IsSubmoduleMergeCommit(ref):
3922 # When submodules are added to the repo, we expect there to be a single
3923 # non-git-svn merge commit at remote HEAD with a signature comment.
3924 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003925 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003926 return RunGit(cmd) != ''
3927
3928
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003929def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003930 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003931
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003932 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3933 upstream and closes the issue automatically and atomically.
3934
3935 Otherwise (in case of Rietveld):
3936 Squashes branch into a single commit.
3937 Updates changelog with metadata (e.g. pointer to review).
3938 Pushes/dcommits the code upstream.
3939 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940 """
3941 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3942 help='bypass upload presubmit hook')
3943 parser.add_option('-m', dest='message',
3944 help="override review description")
3945 parser.add_option('-f', action='store_true', dest='force',
3946 help="force yes to questions (don't prompt)")
3947 parser.add_option('-c', dest='contributor',
3948 help="external contributor for patch (appended to " +
3949 "description and used as author for git). Should be " +
3950 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003951 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003952 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003953 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003954 auth_config = auth.extract_auth_config_from_options(options)
3955
3956 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003958 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3959 if cl.IsGerrit():
3960 if options.message:
3961 # This could be implemented, but it requires sending a new patch to
3962 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3963 # Besides, Gerrit has the ability to change the commit message on submit
3964 # automatically, thus there is no need to support this option (so far?).
3965 parser.error('-m MESSAGE option is not supported for Gerrit.')
3966 if options.contributor:
3967 parser.error(
3968 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3969 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3970 'the contributor\'s "name <email>". If you can\'t upload such a '
3971 'commit for review, contact your repository admin and request'
3972 '"Forge-Author" permission.')
3973 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3974 options.verbose)
3975
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003976 current = cl.GetBranch()
3977 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3978 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003979 print()
3980 print('Attempting to push branch %r into another local branch!' % current)
3981 print()
3982 print('Either reparent this branch on top of origin/master:')
3983 print(' git reparent-branch --root')
3984 print()
3985 print('OR run `git rebase-update` if you think the parent branch is ')
3986 print('already committed.')
3987 print()
3988 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003989 return 1
3990
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003991 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992 # Default to merging against our best guess of the upstream branch.
3993 args = [cl.GetUpstreamBranch()]
3994
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003995 if options.contributor:
3996 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003997 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003998 return 1
3999
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004001 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004002
sbc@chromium.org71437c02015-04-09 19:29:40 +00004003 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004 return 1
4005
4006 # This rev-list syntax means "show all commits not in my branch that
4007 # are in base_branch".
4008 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4009 base_branch]).splitlines()
4010 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004011 print('Base branch "%s" has %d commits '
4012 'not in this branch.' % (base_branch, len(upstream_commits)))
4013 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004014 return 1
4015
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004016 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004017 svn_head = None
4018 if cmd == 'dcommit' or base_has_submodules:
4019 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4020 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004022 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004023 # If the base_head is a submodule merge commit, the first parent of the
4024 # base_head should be a git-svn commit, which is what we're interested in.
4025 base_svn_head = base_branch
4026 if base_has_submodules:
4027 base_svn_head += '^1'
4028
4029 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004031 print('This branch has %d additional commits not upstreamed yet.'
4032 % len(extra_commits.splitlines()))
4033 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4034 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035 return 1
4036
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004037 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004038 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004039 author = None
4040 if options.contributor:
4041 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004042 hook_results = cl.RunHook(
4043 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004044 may_prompt=not options.force,
4045 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004046 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004047 if not hook_results.should_continue():
4048 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004049
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004050 # Check the tree status if the tree status URL is set.
4051 status = GetTreeStatus()
4052 if 'closed' == status:
4053 print('The tree is closed. Please wait for it to reopen. Use '
4054 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4055 return 1
4056 elif 'unknown' == status:
4057 print('Unable to determine tree status. Please verify manually and '
4058 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4059 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004061 change_desc = ChangeDescription(options.message)
4062 if not change_desc.description and cl.GetIssue():
4063 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004065 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004066 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004067 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004068 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004069 print('No description set.')
4070 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004071 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004073 # Keep a separate copy for the commit message, because the commit message
4074 # contains the link to the Rietveld issue, while the Rietveld message contains
4075 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004076 # Keep a separate copy for the commit message.
4077 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004078 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004079
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004080 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004081 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004082 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004083 # after it. Add a period on a new line to circumvent this. Also add a space
4084 # before the period to make sure that Gitiles continues to correctly resolve
4085 # the URL.
4086 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004088 commit_desc.append_footer('Patch from %s.' % options.contributor)
4089
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004090 print('Description:')
4091 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004093 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004095 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004096
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004097 # We want to squash all this branch's commits into one commit with the proper
4098 # description. We do this by doing a "reset --soft" to the base branch (which
4099 # keeps the working copy the same), then dcommitting that. If origin/master
4100 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4101 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004103 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4104 # Delete the branches if they exist.
4105 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4106 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4107 result = RunGitWithCode(showref_cmd)
4108 if result[0] == 0:
4109 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004110
4111 # We might be in a directory that's present in this branch but not in the
4112 # trunk. Move up to the top of the tree so that git commands that expect a
4113 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004114 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115 if rel_base_path:
4116 os.chdir(rel_base_path)
4117
4118 # Stuff our change into the merge branch.
4119 # We wrap in a try...finally block so if anything goes wrong,
4120 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004121 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004122 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004123 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004124 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004125 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004126 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004127 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004128 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004129 RunGit(
4130 [
4131 'commit', '--author', options.contributor,
4132 '-m', commit_desc.description,
4133 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004135 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004136 if base_has_submodules:
4137 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4138 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4139 RunGit(['checkout', CHERRY_PICK_BRANCH])
4140 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004141 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004142 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004143 mirror = settings.GetGitMirror(remote)
4144 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004145 pending_prefix = settings.GetPendingRefPrefix()
4146 if not pending_prefix or branch.startswith(pending_prefix):
4147 # If not using refs/pending/heads/* at all, or target ref is already set
4148 # to pending, then push to the target ref directly.
4149 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004150 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004151 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004152 else:
4153 # Cherry-pick the change on top of pending ref and then push it.
4154 assert branch.startswith('refs/'), branch
4155 assert pending_prefix[-1] == '/', pending_prefix
4156 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004157 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004158 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004159 if retcode == 0:
4160 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004161 else:
4162 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004163 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004164 'svn', 'dcommit',
4165 '-C%s' % options.similarity,
4166 '--no-rebase', '--rmdir',
4167 ]
4168 if settings.GetForceHttpsCommitUrl():
4169 # Allow forcing https commit URLs for some projects that don't allow
4170 # committing to http URLs (like Google Code).
4171 remote_url = cl.GetGitSvnRemoteUrl()
4172 if urlparse.urlparse(remote_url).scheme == 'http':
4173 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004174 cmd_args.append('--commit-url=%s' % remote_url)
4175 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004176 if 'Committed r' in output:
4177 revision = re.match(
4178 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4179 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180 finally:
4181 # And then swap back to the original branch and clean up.
4182 RunGit(['checkout', '-q', cl.GetBranch()])
4183 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004184 if base_has_submodules:
4185 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004186
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004187 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004189 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004190
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004191 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004192 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004193 try:
4194 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4195 # We set pushed_to_pending to False, since it made it all the way to the
4196 # real ref.
4197 pushed_to_pending = False
4198 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004199 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004200
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004201 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004202 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004204 if not to_pending:
4205 if viewvc_url and revision:
4206 change_desc.append_footer(
4207 'Committed: %s%s' % (viewvc_url, revision))
4208 elif revision:
4209 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004210 print('Closing issue '
4211 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004212 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004214 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004215 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004216 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004217 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004218 if options.bypass_hooks:
4219 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4220 else:
4221 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004222 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004223
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004224 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004225 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004226 print('The commit is in the pending queue (%s).' % pending_ref)
4227 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4228 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004229
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004230 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4231 if os.path.isfile(hook):
4232 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004233
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004234 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235
4236
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004237def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004238 print()
4239 print('Waiting for commit to be landed on %s...' % real_ref)
4240 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004241 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4242 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004243 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004244
4245 loop = 0
4246 while True:
4247 sys.stdout.write('fetching (%d)... \r' % loop)
4248 sys.stdout.flush()
4249 loop += 1
4250
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004251 if mirror:
4252 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004253 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4254 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4255 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4256 for commit in commits.splitlines():
4257 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004259 return commit
4260
4261 current_rev = to_rev
4262
4263
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004264def PushToGitPending(remote, pending_ref, upstream_ref):
4265 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4266
4267 Returns:
4268 (retcode of last operation, output log of last operation).
4269 """
4270 assert pending_ref.startswith('refs/'), pending_ref
4271 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4272 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4273 code = 0
4274 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004275 max_attempts = 3
4276 attempts_left = max_attempts
4277 while attempts_left:
4278 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004279 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004280 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004281
4282 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004283 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004284 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004285 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004286 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004287 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004288 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004289 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004290 continue
4291
4292 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004293 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004294 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004295 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004296 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4298 'the following files have merge conflicts:' % pending_ref)
4299 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4300 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004301 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004302 return code, out
4303
4304 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004306 code, out = RunGitWithCode(
4307 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4308 if code == 0:
4309 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004311 return code, out
4312
vapiera7fbd5a2016-06-16 09:17:49 -07004313 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004314 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004315 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004316 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('Fatal push error. Make sure your .netrc credentials and git '
4318 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004319 return code, out
4320
vapiera7fbd5a2016-06-16 09:17:49 -07004321 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004322 return code, out
4323
4324
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004325def IsFatalPushFailure(push_stdout):
4326 """True if retrying push won't help."""
4327 return '(prohibited by Gerrit)' in push_stdout
4328
4329
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004330@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004332 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004333 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004334 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004335 # If it looks like previous commits were mirrored with git-svn.
4336 message = """This repository appears to be a git-svn mirror, but no
4337upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4338 else:
4339 message = """This doesn't appear to be an SVN repository.
4340If your project has a true, writeable git repository, you probably want to run
4341'git cl land' instead.
4342If your project has a git mirror of an upstream SVN master, you probably need
4343to run 'git svn init'.
4344
4345Using the wrong command might cause your commit to appear to succeed, and the
4346review to be closed, without actually landing upstream. If you choose to
4347proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004348 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004349 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004350 # TODO(tandrii): kill this post SVN migration with
4351 # https://codereview.chromium.org/2076683002
4352 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4353 'Please let us know of this project you are committing to:'
4354 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355 return SendUpstream(parser, args, 'dcommit')
4356
4357
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004358@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004359def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004360 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004361 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362 print('This appears to be an SVN repository.')
4363 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004364 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004365 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004366 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004367
4368
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004369@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004370def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004371 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004372 parser.add_option('-b', dest='newbranch',
4373 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004374 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004375 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004376 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4377 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004378 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004379 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004380 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004381 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004383 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004384
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004385
4386 group = optparse.OptionGroup(
4387 parser,
4388 'Options for continuing work on the current issue uploaded from a '
4389 'different clone (e.g. different machine). Must be used independently '
4390 'from the other options. No issue number should be specified, and the '
4391 'branch must have an issue number associated with it')
4392 group.add_option('--reapply', action='store_true', dest='reapply',
4393 help='Reset the branch and reapply the issue.\n'
4394 'CAUTION: This will undo any local changes in this '
4395 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004396
4397 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004398 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004399 parser.add_option_group(group)
4400
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004401 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004402 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004404 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004405 auth_config = auth.extract_auth_config_from_options(options)
4406
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004407
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004408 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004409 if options.newbranch:
4410 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004411 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004412 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004413
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004414 cl = Changelist(auth_config=auth_config,
4415 codereview=options.forced_codereview)
4416 if not cl.GetIssue():
4417 parser.error('current branch must have an associated issue')
4418
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004419 upstream = cl.GetUpstreamBranch()
4420 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004421 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004422
4423 RunGit(['reset', '--hard', upstream])
4424 if options.pull:
4425 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004426
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004427 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4428 options.directory)
4429
4430 if len(args) != 1 or not args[0]:
4431 parser.error('Must specify issue number or url')
4432
4433 # We don't want uncommitted changes mixed up with the patch.
4434 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004435 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004437 if options.newbranch:
4438 if options.force:
4439 RunGit(['branch', '-D', options.newbranch],
4440 stderr=subprocess2.PIPE, error_ok=True)
4441 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004442 elif not GetCurrentBranch():
4443 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004444
4445 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4446
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004447 if cl.IsGerrit():
4448 if options.reject:
4449 parser.error('--reject is not supported with Gerrit codereview.')
4450 if options.nocommit:
4451 parser.error('--nocommit is not supported with Gerrit codereview.')
4452 if options.directory:
4453 parser.error('--directory is not supported with Gerrit codereview.')
4454
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004455 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004456 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004457
4458
4459def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004460 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004461 # Provide a wrapper for git svn rebase to help avoid accidental
4462 # git svn dcommit.
4463 # It's the only command that doesn't use parser at all since we just defer
4464 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004465
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004466 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467
4468
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004469def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004470 """Fetches the tree status and returns either 'open', 'closed',
4471 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004472 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004473 if url:
4474 status = urllib2.urlopen(url).read().lower()
4475 if status.find('closed') != -1 or status == '0':
4476 return 'closed'
4477 elif status.find('open') != -1 or status == '1':
4478 return 'open'
4479 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004480 return 'unset'
4481
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004482
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004483def GetTreeStatusReason():
4484 """Fetches the tree status from a json url and returns the message
4485 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004486 url = settings.GetTreeStatusUrl()
4487 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004488 connection = urllib2.urlopen(json_url)
4489 status = json.loads(connection.read())
4490 connection.close()
4491 return status['message']
4492
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004493
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004494def GetBuilderMaster(bot_list):
4495 """For a given builder, fetch the master from AE if available."""
4496 map_url = 'https://builders-map.appspot.com/'
4497 try:
4498 master_map = json.load(urllib2.urlopen(map_url))
4499 except urllib2.URLError as e:
4500 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4501 (map_url, e))
4502 except ValueError as e:
4503 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4504 if not master_map:
4505 return None, 'Failed to build master map.'
4506
4507 result_master = ''
4508 for bot in bot_list:
4509 builder = bot.split(':', 1)[0]
4510 master_list = master_map.get(builder, [])
4511 if not master_list:
4512 return None, ('No matching master for builder %s.' % builder)
4513 elif len(master_list) > 1:
4514 return None, ('The builder name %s exists in multiple masters %s.' %
4515 (builder, master_list))
4516 else:
4517 cur_master = master_list[0]
4518 if not result_master:
4519 result_master = cur_master
4520 elif result_master != cur_master:
4521 return None, 'The builders do not belong to the same master.'
4522 return result_master, None
4523
4524
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004525def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004526 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004527 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004528 status = GetTreeStatus()
4529 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004530 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531 return 2
4532
vapiera7fbd5a2016-06-16 09:17:49 -07004533 print('The tree is %s' % status)
4534 print()
4535 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536 if status != 'open':
4537 return 1
4538 return 0
4539
4540
maruel@chromium.org15192402012-09-06 12:38:29 +00004541def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004542 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004543 group = optparse.OptionGroup(parser, "Try job options")
4544 group.add_option(
4545 "-b", "--bot", action="append",
4546 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4547 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004548 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004549 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004550 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004551 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004552 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004553 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004554 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004555 "-r", "--revision",
4556 help="Revision to use for the try job; default: the "
4557 "revision will be determined by the try server; see "
4558 "its waterfall for more info")
4559 group.add_option(
4560 "-c", "--clobber", action="store_true", default=False,
4561 help="Force a clobber before building; e.g. don't do an "
4562 "incremental build")
4563 group.add_option(
4564 "--project",
4565 help="Override which project to use. Projects are defined "
4566 "server-side to define what default bot set to use")
4567 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004568 "-p", "--property", dest="properties", action="append", default=[],
4569 help="Specify generic properties in the form -p key1=value1 -p "
4570 "key2=value2 etc (buildbucket only). The value will be treated as "
4571 "json if decodable, or as string otherwise.")
4572 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004573 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004574 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004575 "--use-rietveld", action="store_true", default=False,
4576 help="Use Rietveld to trigger try jobs.")
4577 group.add_option(
4578 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4579 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004580 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004581 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004582 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004583 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004584
machenbach@chromium.org45453142015-09-15 08:45:22 +00004585 if options.use_rietveld and options.properties:
4586 parser.error('Properties can only be specified with buildbucket')
4587
4588 # Make sure that all properties are prop=value pairs.
4589 bad_params = [x for x in options.properties if '=' not in x]
4590 if bad_params:
4591 parser.error('Got properties with missing "=": %s' % bad_params)
4592
maruel@chromium.org15192402012-09-06 12:38:29 +00004593 if args:
4594 parser.error('Unknown arguments: %s' % args)
4595
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004596 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004597 if not cl.GetIssue():
4598 parser.error('Need to upload first')
4599
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004600 if cl.IsGerrit():
4601 parser.error(
4602 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4603 'If your project has Commit Queue, dry run is a workaround:\n'
4604 ' git cl set-commit --dry-run')
4605 # Code below assumes Rietveld issue.
4606 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4607
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004608 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004609 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004610 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004611
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004612 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004613 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004614
maruel@chromium.org15192402012-09-06 12:38:29 +00004615 if not options.name:
4616 options.name = cl.GetBranch()
4617
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004618 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004619 options.master, err_msg = GetBuilderMaster(options.bot)
4620 if err_msg:
4621 parser.error('Tryserver master cannot be found because: %s\n'
4622 'Please manually specify the tryserver master'
4623 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004624
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004625 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004626 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004627 if not options.bot:
4628 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004629
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004630 # Get try masters from PRESUBMIT.py files.
4631 masters = presubmit_support.DoGetTryMasters(
4632 change,
4633 change.LocalPaths(),
4634 settings.GetRoot(),
4635 None,
4636 None,
4637 options.verbose,
4638 sys.stdout)
4639 if masters:
4640 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004641
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004642 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4643 options.bot = presubmit_support.DoGetTrySlaves(
4644 change,
4645 change.LocalPaths(),
4646 settings.GetRoot(),
4647 None,
4648 None,
4649 options.verbose,
4650 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004651
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004652 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004653 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004654
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004655 builders_and_tests = {}
4656 # TODO(machenbach): The old style command-line options don't support
4657 # multiple try masters yet.
4658 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4659 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4660
4661 for bot in old_style:
4662 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004663 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004664 elif ',' in bot:
4665 parser.error('Specify one bot per --bot flag')
4666 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004667 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004668
4669 for bot, tests in new_style:
4670 builders_and_tests.setdefault(bot, []).extend(tests)
4671
4672 # Return a master map with one master to be backwards compatible. The
4673 # master name defaults to an empty string, which will cause the master
4674 # not to be set on rietveld (deprecated).
4675 return {options.master: builders_and_tests}
4676
4677 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004678 if not masters:
4679 # Default to triggering Dry Run (see http://crbug.com/625697).
4680 if options.verbose:
4681 print('git cl try with no bots now defaults to CQ Dry Run.')
4682 try:
4683 cl.SetCQState(_CQState.DRY_RUN)
4684 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4685 return 0
4686 except KeyboardInterrupt:
4687 raise
4688 except:
4689 print('WARNING: failed to trigger CQ Dry Run.\n'
4690 'Either:\n'
4691 ' * your project has no CQ\n'
4692 ' * you don\'t have permission to trigger Dry Run\n'
4693 ' * bug in this code (see stack trace below).\n'
4694 'Consider specifying which bots to trigger manually '
4695 'or asking your project owners for permissions '
4696 'or contacting Chrome Infrastructure team at '
4697 'https://www.chromium.org/infra\n\n')
4698 # Still raise exception so that stack trace is printed.
4699 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004700
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004701 for builders in masters.itervalues():
4702 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004703 print('ERROR You are trying to send a job to a triggered bot. This type '
4704 'of bot requires an\ninitial job from a parent (usually a builder).'
4705 ' Instead send your job to the parent.\n'
4706 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004707 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004708
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004709 patchset = cl.GetMostRecentPatchset()
4710 if patchset and patchset != cl.GetPatchset():
4711 print(
4712 '\nWARNING Mismatch between local config and server. Did a previous '
4713 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4714 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004715 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004716 try:
4717 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4718 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004719 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004720 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004721 except Exception as e:
4722 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004723 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004724 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004725 return 1
4726 else:
4727 try:
4728 cl.RpcServer().trigger_distributed_try_jobs(
4729 cl.GetIssue(), patchset, options.name, options.clobber,
4730 options.revision, masters)
4731 except urllib2.HTTPError as e:
4732 if e.code == 404:
4733 print('404 from rietveld; '
4734 'did you mean to use "git try" instead of "git cl try"?')
4735 return 1
4736 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004737
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004738 for (master, builders) in sorted(masters.iteritems()):
4739 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004740 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004741 length = max(len(builder) for builder in builders)
4742 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004743 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004744 return 0
4745
4746
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004747def CMDtry_results(parser, args):
4748 group = optparse.OptionGroup(parser, "Try job results options")
4749 group.add_option(
4750 "-p", "--patchset", type=int, help="patchset number if not current.")
4751 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004752 "--print-master", action='store_true', help="print master name as well.")
4753 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004754 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004755 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004756 group.add_option(
4757 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4758 help="Host of buildbucket. The default host is %default.")
4759 parser.add_option_group(group)
4760 auth.add_auth_options(parser)
4761 options, args = parser.parse_args(args)
4762 if args:
4763 parser.error('Unrecognized args: %s' % ' '.join(args))
4764
4765 auth_config = auth.extract_auth_config_from_options(options)
4766 cl = Changelist(auth_config=auth_config)
4767 if not cl.GetIssue():
4768 parser.error('Need to upload first')
4769
4770 if not options.patchset:
4771 options.patchset = cl.GetMostRecentPatchset()
4772 if options.patchset and options.patchset != cl.GetPatchset():
4773 print(
4774 '\nWARNING Mismatch between local config and server. Did a previous '
4775 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4776 'Continuing using\npatchset %s.\n' % options.patchset)
4777 try:
4778 jobs = fetch_try_jobs(auth_config, cl, options)
4779 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004780 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004781 return 1
4782 except Exception as e:
4783 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004784 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004785 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004786 return 1
qyearsleyeab3c042016-08-24 09:18:28 -07004787 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004788 return 0
4789
4790
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004791@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004792def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004793 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004794 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004795 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004796 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004798 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004799 if args:
4800 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004801 branch = cl.GetBranch()
4802 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004803 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004804 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004805
4806 # Clear configured merge-base, if there is one.
4807 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004808 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004809 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810 return 0
4811
4812
thestig@chromium.org00858c82013-12-02 23:08:03 +00004813def CMDweb(parser, args):
4814 """Opens the current CL in the web browser."""
4815 _, args = parser.parse_args(args)
4816 if args:
4817 parser.error('Unrecognized args: %s' % ' '.join(args))
4818
4819 issue_url = Changelist().GetIssueURL()
4820 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004821 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004822 return 1
4823
4824 webbrowser.open(issue_url)
4825 return 0
4826
4827
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004828def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004829 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004830 parser.add_option('-d', '--dry-run', action='store_true',
4831 help='trigger in dry run mode')
4832 parser.add_option('-c', '--clear', action='store_true',
4833 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004834 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004835 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004836 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004837 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004838 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004839 if args:
4840 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004841 if options.dry_run and options.clear:
4842 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4843
iannuccie53c9352016-08-17 14:40:40 -07004844 cl = Changelist(auth_config=auth_config, issue=options.issue,
4845 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004846 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004847 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004848 elif options.dry_run:
4849 state = _CQState.DRY_RUN
4850 else:
4851 state = _CQState.COMMIT
4852 if not cl.GetIssue():
4853 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004854 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004855 return 0
4856
4857
groby@chromium.org411034a2013-02-26 15:12:01 +00004858def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004859 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004860 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 auth.add_auth_options(parser)
4862 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004863 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004864 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004865 if args:
4866 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004867 cl = Changelist(auth_config=auth_config, issue=options.issue,
4868 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004869 # Ensure there actually is an issue to close.
4870 cl.GetDescription()
4871 cl.CloseIssue()
4872 return 0
4873
4874
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004875def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004876 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004877 auth.add_auth_options(parser)
4878 options, args = parser.parse_args(args)
4879 auth_config = auth.extract_auth_config_from_options(options)
4880 if args:
4881 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004882
4883 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004884 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004885 # Staged changes would be committed along with the patch from last
4886 # upload, hence counted toward the "last upload" side in the final
4887 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004888 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004889 return 1
4890
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004891 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004892 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004893 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004894 if not issue:
4895 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004896 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004897 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004898
4899 # Create a new branch based on the merge-base
4900 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004901 # Clear cached branch in cl object, to avoid overwriting original CL branch
4902 # properties.
4903 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004904 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004905 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004906 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004907 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004908 return rtn
4909
wychen@chromium.org06928532015-02-03 02:11:29 +00004910 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004911 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004912 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004913 finally:
4914 RunGit(['checkout', '-q', branch])
4915 RunGit(['branch', '-D', TMP_BRANCH])
4916
4917 return 0
4918
4919
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004920def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004921 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004922 parser.add_option(
4923 '--no-color',
4924 action='store_true',
4925 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004926 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004927 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004928 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004929
4930 author = RunGit(['config', 'user.email']).strip() or None
4931
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004932 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004933
4934 if args:
4935 if len(args) > 1:
4936 parser.error('Unknown args')
4937 base_branch = args[0]
4938 else:
4939 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004940 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004941
4942 change = cl.GetChange(base_branch, None)
4943 return owners_finder.OwnersFinder(
4944 [f.LocalPath() for f in
4945 cl.GetChange(base_branch, None).AffectedFiles()],
4946 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004947 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004948 disable_color=options.no_color).run()
4949
4950
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004951def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004952 """Generates a diff command."""
4953 # Generate diff for the current branch's changes.
4954 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4955 upstream_commit, '--' ]
4956
4957 if args:
4958 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004959 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004960 diff_cmd.append(arg)
4961 else:
4962 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004963
4964 return diff_cmd
4965
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004966def MatchingFileType(file_name, extensions):
4967 """Returns true if the file name ends with one of the given extensions."""
4968 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004969
enne@chromium.org555cfe42014-01-29 18:21:39 +00004970@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004971def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004972 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004973 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004974 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004975 parser.add_option('--full', action='store_true',
4976 help='Reformat the full content of all touched files')
4977 parser.add_option('--dry-run', action='store_true',
4978 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004979 parser.add_option('--python', action='store_true',
4980 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004981 parser.add_option('--diff', action='store_true',
4982 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004983 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004984
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004985 # git diff generates paths against the root of the repository. Change
4986 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004987 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004988 if rel_base_path:
4989 os.chdir(rel_base_path)
4990
digit@chromium.org29e47272013-05-17 17:01:46 +00004991 # Grab the merge-base commit, i.e. the upstream commit of the current
4992 # branch when it was created or the last time it was rebased. This is
4993 # to cover the case where the user may have called "git fetch origin",
4994 # moving the origin branch to a newer commit, but hasn't rebased yet.
4995 upstream_commit = None
4996 cl = Changelist()
4997 upstream_branch = cl.GetUpstreamBranch()
4998 if upstream_branch:
4999 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5000 upstream_commit = upstream_commit.strip()
5001
5002 if not upstream_commit:
5003 DieWithError('Could not find base commit for this branch. '
5004 'Are you in detached state?')
5005
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005006 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5007 diff_output = RunGit(changed_files_cmd)
5008 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005009 # Filter out files deleted by this CL
5010 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005011
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005012 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5013 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5014 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005015 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005016
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005017 top_dir = os.path.normpath(
5018 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5019
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005020 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5021 # formatted. This is used to block during the presubmit.
5022 return_value = 0
5023
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005024 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005025 # Locate the clang-format binary in the checkout
5026 try:
5027 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005028 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005029 DieWithError(e)
5030
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005031 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005032 cmd = [clang_format_tool]
5033 if not opts.dry_run and not opts.diff:
5034 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005035 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005036 if opts.diff:
5037 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005038 else:
5039 env = os.environ.copy()
5040 env['PATH'] = str(os.path.dirname(clang_format_tool))
5041 try:
5042 script = clang_format.FindClangFormatScriptInChromiumTree(
5043 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005044 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005045 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005046
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005047 cmd = [sys.executable, script, '-p0']
5048 if not opts.dry_run and not opts.diff:
5049 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005050
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005051 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5052 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005053
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005054 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5055 if opts.diff:
5056 sys.stdout.write(stdout)
5057 if opts.dry_run and len(stdout) > 0:
5058 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005059
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005060 # Similar code to above, but using yapf on .py files rather than clang-format
5061 # on C/C++ files
5062 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005063 yapf_tool = gclient_utils.FindExecutable('yapf')
5064 if yapf_tool is None:
5065 DieWithError('yapf not found in PATH')
5066
5067 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005068 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005069 cmd = [yapf_tool]
5070 if not opts.dry_run and not opts.diff:
5071 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005072 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005073 if opts.diff:
5074 sys.stdout.write(stdout)
5075 else:
5076 # TODO(sbc): yapf --lines mode still has some issues.
5077 # https://github.com/google/yapf/issues/154
5078 DieWithError('--python currently only works with --full')
5079
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005080 # Dart's formatter does not have the nice property of only operating on
5081 # modified chunks, so hard code full.
5082 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005083 try:
5084 command = [dart_format.FindDartFmtToolInChromiumTree()]
5085 if not opts.dry_run and not opts.diff:
5086 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005087 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005088
ppi@chromium.org6593d932016-03-03 15:41:15 +00005089 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005090 if opts.dry_run and stdout:
5091 return_value = 2
5092 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005093 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5094 'found in this checkout. Files in other languages are still '
5095 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005096
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005097 # Format GN build files. Always run on full build files for canonical form.
5098 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005099 cmd = ['gn', 'format' ]
5100 if opts.dry_run or opts.diff:
5101 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005102 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005103 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5104 shell=sys.platform == 'win32',
5105 cwd=top_dir)
5106 if opts.dry_run and gn_ret == 2:
5107 return_value = 2 # Not formatted.
5108 elif opts.diff and gn_ret == 2:
5109 # TODO this should compute and print the actual diff.
5110 print("This change has GN build file diff for " + gn_diff_file)
5111 elif gn_ret != 0:
5112 # For non-dry run cases (and non-2 return values for dry-run), a
5113 # nonzero error code indicates a failure, probably because the file
5114 # doesn't parse.
5115 DieWithError("gn format failed on " + gn_diff_file +
5116 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005117
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005118 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005119
5120
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005121@subcommand.usage('<codereview url or issue id>')
5122def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005123 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005124 _, args = parser.parse_args(args)
5125
5126 if len(args) != 1:
5127 parser.print_help()
5128 return 1
5129
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005130 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005131 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005132 parser.print_help()
5133 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005134 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005135
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005136 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005137 output = RunGit(['config', '--local', '--get-regexp',
5138 r'branch\..*\.%s' % issueprefix],
5139 error_ok=True)
5140 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005141 if issue == target_issue:
5142 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005143
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005144 branches = []
5145 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005146 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005147 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005148 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005149 return 1
5150 if len(branches) == 1:
5151 RunGit(['checkout', branches[0]])
5152 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005153 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005154 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005155 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005156 which = raw_input('Choose by index: ')
5157 try:
5158 RunGit(['checkout', branches[int(which)]])
5159 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005160 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005161 return 1
5162
5163 return 0
5164
5165
maruel@chromium.org29404b52014-09-08 22:58:00 +00005166def CMDlol(parser, args):
5167 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005168 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005169 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5170 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5171 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005172 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005173 return 0
5174
5175
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005176class OptionParser(optparse.OptionParser):
5177 """Creates the option parse and add --verbose support."""
5178 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005179 optparse.OptionParser.__init__(
5180 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005181 self.add_option(
5182 '-v', '--verbose', action='count', default=0,
5183 help='Use 2 times for more debugging info')
5184
5185 def parse_args(self, args=None, values=None):
5186 options, args = optparse.OptionParser.parse_args(self, args, values)
5187 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5188 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5189 return options, args
5190
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005192def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005193 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005194 print('\nYour python version %s is unsupported, please upgrade.\n' %
5195 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005196 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005197
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005198 # Reload settings.
5199 global settings
5200 settings = Settings()
5201
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005202 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005203 dispatcher = subcommand.CommandDispatcher(__name__)
5204 try:
5205 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005206 except auth.AuthenticationError as e:
5207 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005208 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005209 if e.code != 500:
5210 raise
5211 DieWithError(
5212 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5213 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005214 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005215
5216
5217if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005218 # These affect sys.stdout so do it outside of main() to simplify mocks in
5219 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005220 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005221 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005222 try:
5223 sys.exit(main(sys.argv[1:]))
5224 except KeyboardInterrupt:
5225 sys.stderr.write('interrupted\n')
5226 sys.exit(1)