blob: 5831b121ee638d0fe779203991b1692bd60b8db3 [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
1028 Return is a string suitable for passing to gcl with the --cc flag.
1029 """
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:
2016 upload_args.extend(['--base_url', remote_url])
2017 remote, remote_branch = self.GetRemoteBranch()
2018 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2019 settings.GetPendingRefPrefix())
2020 if target_ref:
2021 upload_args.extend(['--target_ref', target_ref])
2022
2023 # Look for dependent patchsets. See crbug.com/480453 for more details.
2024 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2025 upstream_branch = ShortBranchName(upstream_branch)
2026 if remote is '.':
2027 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002028 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002029 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002030 print()
2031 print('Skipping dependency patchset upload because git config '
2032 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2033 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002034 else:
2035 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002036 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002037 auth_config=auth_config)
2038 branch_cl_issue_url = branch_cl.GetIssueURL()
2039 branch_cl_issue = branch_cl.GetIssue()
2040 branch_cl_patchset = branch_cl.GetPatchset()
2041 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2042 upload_args.extend(
2043 ['--depends_on_patchset', '%s:%s' % (
2044 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002045 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002046 '\n'
2047 'The current branch (%s) is tracking a local branch (%s) with '
2048 'an associated CL.\n'
2049 'Adding %s/#ps%s as a dependency patchset.\n'
2050 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2051 branch_cl_patchset))
2052
2053 project = settings.GetProject()
2054 if project:
2055 upload_args.extend(['--project', project])
2056
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002057 try:
2058 upload_args = ['upload'] + upload_args + args
2059 logging.info('upload.RealMain(%s)', upload_args)
2060 issue, patchset = upload.RealMain(upload_args)
2061 issue = int(issue)
2062 patchset = int(patchset)
2063 except KeyboardInterrupt:
2064 sys.exit(1)
2065 except:
2066 # If we got an exception after the user typed a description for their
2067 # change, back up the description before re-raising.
2068 if change_desc:
2069 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2070 print('\nGot exception while uploading -- saving description to %s\n' %
2071 backup_path)
2072 backup_file = open(backup_path, 'w')
2073 backup_file.write(change_desc.description)
2074 backup_file.close()
2075 raise
2076
2077 if not self.GetIssue():
2078 self.SetIssue(issue)
2079 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002080 return 0
2081
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002082
2083class _GerritChangelistImpl(_ChangelistCodereviewBase):
2084 def __init__(self, changelist, auth_config=None):
2085 # auth_config is Rietveld thing, kept here to preserve interface only.
2086 super(_GerritChangelistImpl, self).__init__(changelist)
2087 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002088 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002090 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002091
2092 def _GetGerritHost(self):
2093 # Lazy load of configs.
2094 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002095 if self._gerrit_host and '.' not in self._gerrit_host:
2096 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2097 # This happens for internal stuff http://crbug.com/614312.
2098 parsed = urlparse.urlparse(self.GetRemoteUrl())
2099 if parsed.scheme == 'sso':
2100 print('WARNING: using non https URLs for remote is likely broken\n'
2101 ' Your current remote is: %s' % self.GetRemoteUrl())
2102 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2103 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002104 return self._gerrit_host
2105
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002106 def _GetGitHost(self):
2107 """Returns git host to be used when uploading change to Gerrit."""
2108 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2109
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002110 def GetCodereviewServer(self):
2111 if not self._gerrit_server:
2112 # If we're on a branch then get the server potentially associated
2113 # with that branch.
2114 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002115 self._gerrit_server = self._GitGetBranchConfigValue(
2116 self.CodereviewServerConfigKey())
2117 if self._gerrit_server:
2118 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002119 if not self._gerrit_server:
2120 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2121 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002122 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002123 parts[0] = parts[0] + '-review'
2124 self._gerrit_host = '.'.join(parts)
2125 self._gerrit_server = 'https://%s' % self._gerrit_host
2126 return self._gerrit_server
2127
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002128 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002129 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002130 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002131
tandrii5d48c322016-08-18 16:19:37 -07002132 @classmethod
2133 def PatchsetConfigKey(cls):
2134 return 'gerritpatchset'
2135
2136 @classmethod
2137 def CodereviewServerConfigKey(cls):
2138 return 'gerritserver'
2139
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002140 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002141 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002142 if settings.GetGerritSkipEnsureAuthenticated():
2143 # For projects with unusual authentication schemes.
2144 # See http://crbug.com/603378.
2145 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002146 # Lazy-loader to identify Gerrit and Git hosts.
2147 if gerrit_util.GceAuthenticator.is_gce():
2148 return
2149 self.GetCodereviewServer()
2150 git_host = self._GetGitHost()
2151 assert self._gerrit_server and self._gerrit_host
2152 cookie_auth = gerrit_util.CookiesAuthenticator()
2153
2154 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2155 git_auth = cookie_auth.get_auth_header(git_host)
2156 if gerrit_auth and git_auth:
2157 if gerrit_auth == git_auth:
2158 return
2159 print((
2160 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2161 ' Check your %s or %s file for credentials of hosts:\n'
2162 ' %s\n'
2163 ' %s\n'
2164 ' %s') %
2165 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2166 git_host, self._gerrit_host,
2167 cookie_auth.get_new_password_message(git_host)))
2168 if not force:
2169 ask_for_data('If you know what you are doing, press Enter to continue, '
2170 'Ctrl+C to abort.')
2171 return
2172 else:
2173 missing = (
2174 [] if gerrit_auth else [self._gerrit_host] +
2175 [] if git_auth else [git_host])
2176 DieWithError('Credentials for the following hosts are required:\n'
2177 ' %s\n'
2178 'These are read from %s (or legacy %s)\n'
2179 '%s' % (
2180 '\n '.join(missing),
2181 cookie_auth.get_gitcookies_path(),
2182 cookie_auth.get_netrc_path(),
2183 cookie_auth.get_new_password_message(git_host)))
2184
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002185 def _PostUnsetIssueProperties(self):
2186 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002187 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002188
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002189 def GetRieveldObjForPresubmit(self):
2190 class ThisIsNotRietveldIssue(object):
2191 def __nonzero__(self):
2192 # This is a hack to make presubmit_support think that rietveld is not
2193 # defined, yet still ensure that calls directly result in a decent
2194 # exception message below.
2195 return False
2196
2197 def __getattr__(self, attr):
2198 print(
2199 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2200 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2201 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2202 'or use Rietveld for codereview.\n'
2203 'See also http://crbug.com/579160.' % attr)
2204 raise NotImplementedError()
2205 return ThisIsNotRietveldIssue()
2206
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002207 def GetGerritObjForPresubmit(self):
2208 return presubmit_support.GerritAccessor(self._GetGerritHost())
2209
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002210 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002211 """Apply a rough heuristic to give a simple summary of an issue's review
2212 or CQ status, assuming adherence to a common workflow.
2213
2214 Returns None if no issue for this branch, or one of the following keywords:
2215 * 'error' - error from review tool (including deleted issues)
2216 * 'unsent' - no reviewers added
2217 * 'waiting' - waiting for review
2218 * 'reply' - waiting for owner to reply to review
2219 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2220 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2221 * 'commit' - in the commit queue
2222 * 'closed' - abandoned
2223 """
2224 if not self.GetIssue():
2225 return None
2226
2227 try:
2228 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2229 except httplib.HTTPException:
2230 return 'error'
2231
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002232 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002233 return 'closed'
2234
2235 cq_label = data['labels'].get('Commit-Queue', {})
2236 if cq_label:
2237 # Vote value is a stringified integer, which we expect from 0 to 2.
2238 vote_value = cq_label.get('value', '0')
2239 vote_text = cq_label.get('values', {}).get(vote_value, '')
2240 if vote_text.lower() == 'commit':
2241 return 'commit'
2242
2243 lgtm_label = data['labels'].get('Code-Review', {})
2244 if lgtm_label:
2245 if 'rejected' in lgtm_label:
2246 return 'not lgtm'
2247 if 'approved' in lgtm_label:
2248 return 'lgtm'
2249
2250 if not data.get('reviewers', {}).get('REVIEWER', []):
2251 return 'unsent'
2252
2253 messages = data.get('messages', [])
2254 if messages:
2255 owner = data['owner'].get('_account_id')
2256 last_message_author = messages[-1].get('author', {}).get('_account_id')
2257 if owner != last_message_author:
2258 # Some reply from non-owner.
2259 return 'reply'
2260
2261 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262
2263 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002264 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002265 return data['revisions'][data['current_revision']]['_number']
2266
2267 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002268 data = self._GetChangeDetail(['CURRENT_REVISION'])
2269 current_rev = data['current_revision']
2270 url = data['revisions'][current_rev]['fetch']['http']['url']
2271 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002272
2273 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002274 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2275 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002276
2277 def CloseIssue(self):
2278 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2279
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002280 def GetApprovingReviewers(self):
2281 """Returns a list of reviewers approving the change.
2282
2283 Note: not necessarily committers.
2284 """
2285 raise NotImplementedError()
2286
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002287 def SubmitIssue(self, wait_for_merge=True):
2288 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2289 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002290
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 def _GetChangeDetail(self, options=None, issue=None):
2292 options = options or []
2293 issue = issue or self.GetIssue()
2294 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002295 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2296 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002297
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002298 def CMDLand(self, force, bypass_hooks, verbose):
2299 if git_common.is_dirty_git_tree('land'):
2300 return 1
tandriid60367b2016-06-22 05:25:12 -07002301 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2302 if u'Commit-Queue' in detail.get('labels', {}):
2303 if not force:
2304 ask_for_data('\nIt seems this repository has a Commit Queue, '
2305 'which can test and land changes for you. '
2306 'Are you sure you wish to bypass it?\n'
2307 'Press Enter to continue, Ctrl+C to abort.')
2308
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002309 differs = True
tandrii5d48c322016-08-18 16:19:37 -07002310 last_upload = RunGit(['config', self._GitBranchSetting('gerritsquashhash')],
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002311 error_ok=True).strip()
2312 # Note: git diff outputs nothing if there is no diff.
2313 if not last_upload or RunGit(['diff', last_upload]).strip():
2314 print('WARNING: some changes from local branch haven\'t been uploaded')
2315 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002316 if detail['current_revision'] == last_upload:
2317 differs = False
2318 else:
2319 print('WARNING: local branch contents differ from latest uploaded '
2320 'patchset')
2321 if differs:
2322 if not force:
2323 ask_for_data(
2324 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2325 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2326 elif not bypass_hooks:
2327 hook_results = self.RunHook(
2328 committing=True,
2329 may_prompt=not force,
2330 verbose=verbose,
2331 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2332 if not hook_results.should_continue():
2333 return 1
2334
2335 self.SubmitIssue(wait_for_merge=True)
2336 print('Issue %s has been submitted.' % self.GetIssueURL())
2337 return 0
2338
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002339 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2340 directory):
2341 assert not reject
2342 assert not nocommit
2343 assert not directory
2344 assert parsed_issue_arg.valid
2345
2346 self._changelist.issue = parsed_issue_arg.issue
2347
2348 if parsed_issue_arg.hostname:
2349 self._gerrit_host = parsed_issue_arg.hostname
2350 self._gerrit_server = 'https://%s' % self._gerrit_host
2351
2352 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2353
2354 if not parsed_issue_arg.patchset:
2355 # Use current revision by default.
2356 revision_info = detail['revisions'][detail['current_revision']]
2357 patchset = int(revision_info['_number'])
2358 else:
2359 patchset = parsed_issue_arg.patchset
2360 for revision_info in detail['revisions'].itervalues():
2361 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2362 break
2363 else:
2364 DieWithError('Couldn\'t find patchset %i in issue %i' %
2365 (parsed_issue_arg.patchset, self.GetIssue()))
2366
2367 fetch_info = revision_info['fetch']['http']
2368 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2369 RunGit(['cherry-pick', 'FETCH_HEAD'])
2370 self.SetIssue(self.GetIssue())
2371 self.SetPatchset(patchset)
2372 print('Committed patch for issue %i pathset %i locally' %
2373 (self.GetIssue(), self.GetPatchset()))
2374 return 0
2375
2376 @staticmethod
2377 def ParseIssueURL(parsed_url):
2378 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2379 return None
2380 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2381 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2382 # Short urls like https://domain/<issue_number> can be used, but don't allow
2383 # specifying the patchset (you'd 404), but we allow that here.
2384 if parsed_url.path == '/':
2385 part = parsed_url.fragment
2386 else:
2387 part = parsed_url.path
2388 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2389 if match:
2390 return _ParsedIssueNumberArgument(
2391 issue=int(match.group(2)),
2392 patchset=int(match.group(4)) if match.group(4) else None,
2393 hostname=parsed_url.netloc)
2394 return None
2395
tandrii16e0b4e2016-06-07 10:34:28 -07002396 def _GerritCommitMsgHookCheck(self, offer_removal):
2397 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2398 if not os.path.exists(hook):
2399 return
2400 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2401 # custom developer made one.
2402 data = gclient_utils.FileRead(hook)
2403 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2404 return
2405 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002406 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002407 'and may interfere with it in subtle ways.\n'
2408 'We recommend you remove the commit-msg hook.')
2409 if offer_removal:
2410 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2411 if reply.lower().startswith('y'):
2412 gclient_utils.rm_file_or_tree(hook)
2413 print('Gerrit commit-msg hook removed.')
2414 else:
2415 print('OK, will keep Gerrit commit-msg hook in place.')
2416
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002417 def CMDUploadChange(self, options, args, change):
2418 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002419 if options.squash and options.no_squash:
2420 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002421
2422 if not options.squash and not options.no_squash:
2423 # Load default for user, repo, squash=true, in this order.
2424 options.squash = settings.GetSquashGerritUploads()
2425 elif options.no_squash:
2426 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002427
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002428 # We assume the remote called "origin" is the one we want.
2429 # It is probably not worthwhile to support different workflows.
2430 gerrit_remote = 'origin'
2431
2432 remote, remote_branch = self.GetRemoteBranch()
2433 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2434 pending_prefix='')
2435
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002436 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002437 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002438 if self.GetIssue():
2439 # Try to get the message from a previous upload.
2440 message = self.GetDescription()
2441 if not message:
2442 DieWithError(
2443 'failed to fetch description from current Gerrit issue %d\n'
2444 '%s' % (self.GetIssue(), self.GetIssueURL()))
2445 change_id = self._GetChangeDetail()['change_id']
2446 while True:
2447 footer_change_ids = git_footers.get_footer_change_id(message)
2448 if footer_change_ids == [change_id]:
2449 break
2450 if not footer_change_ids:
2451 message = git_footers.add_footer_change_id(message, change_id)
2452 print('WARNING: appended missing Change-Id to issue description')
2453 continue
2454 # There is already a valid footer but with different or several ids.
2455 # Doing this automatically is non-trivial as we don't want to lose
2456 # existing other footers, yet we want to append just 1 desired
2457 # Change-Id. Thus, just create a new footer, but let user verify the
2458 # new description.
2459 message = '%s\n\nChange-Id: %s' % (message, change_id)
2460 print(
2461 'WARNING: issue %s has Change-Id footer(s):\n'
2462 ' %s\n'
2463 'but issue has Change-Id %s, according to Gerrit.\n'
2464 'Please, check the proposed correction to the description, '
2465 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2466 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2467 change_id))
2468 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2469 if not options.force:
2470 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002471 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 message = change_desc.description
2473 if not message:
2474 DieWithError("Description is empty. Aborting...")
2475 # Continue the while loop.
2476 # Sanity check of this code - we should end up with proper message
2477 # footer.
2478 assert [change_id] == git_footers.get_footer_change_id(message)
2479 change_desc = ChangeDescription(message)
2480 else:
2481 change_desc = ChangeDescription(
2482 options.message or CreateDescriptionFromLog(args))
2483 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002484 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 if not change_desc.description:
2486 DieWithError("Description is empty. Aborting...")
2487 message = change_desc.description
2488 change_ids = git_footers.get_footer_change_id(message)
2489 if len(change_ids) > 1:
2490 DieWithError('too many Change-Id footers, at most 1 allowed.')
2491 if not change_ids:
2492 # Generate the Change-Id automatically.
2493 message = git_footers.add_footer_change_id(
2494 message, GenerateGerritChangeId(message))
2495 change_desc.set_description(message)
2496 change_ids = git_footers.get_footer_change_id(message)
2497 assert len(change_ids) == 1
2498 change_id = change_ids[0]
2499
2500 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2501 if remote is '.':
2502 # If our upstream branch is local, we base our squashed commit on its
2503 # squashed version.
2504 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2505 # Check the squashed hash of the parent.
2506 parent = RunGit(['config',
2507 'branch.%s.gerritsquashhash' % upstream_branch_name],
2508 error_ok=True).strip()
2509 # Verify that the upstream branch has been uploaded too, otherwise
2510 # Gerrit will create additional CLs when uploading.
2511 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2512 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002513 DieWithError(
2514 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002515 'Note: maybe you\'ve uploaded it with --no-squash. '
2516 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002517 ' git cl upload --squash\n' % upstream_branch_name)
2518 else:
2519 parent = self.GetCommonAncestorWithUpstream()
2520
2521 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2522 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2523 '-m', message]).strip()
2524 else:
2525 change_desc = ChangeDescription(
2526 options.message or CreateDescriptionFromLog(args))
2527 if not change_desc.description:
2528 DieWithError("Description is empty. Aborting...")
2529
2530 if not git_footers.get_footer_change_id(change_desc.description):
2531 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002532 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2533 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002534 ref_to_push = 'HEAD'
2535 parent = '%s/%s' % (gerrit_remote, branch)
2536 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2537
2538 assert change_desc
2539 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2540 ref_to_push)]).splitlines()
2541 if len(commits) > 1:
2542 print('WARNING: This will upload %d commits. Run the following command '
2543 'to see which commits will be uploaded: ' % len(commits))
2544 print('git log %s..%s' % (parent, ref_to_push))
2545 print('You can also use `git squash-branch` to squash these into a '
2546 'single commit.')
2547 ask_for_data('About to upload; enter to confirm.')
2548
2549 if options.reviewers or options.tbr_owners:
2550 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2551 change)
2552
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002553 # Extra options that can be specified at push time. Doc:
2554 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2555 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002556 if change_desc.get_reviewers(tbr_only=True):
2557 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2558 refspec_opts.append('l=Code-Review+1')
2559
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002560 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002561 if not re.match(r'^[\w ]+$', options.title):
2562 options.title = re.sub(r'[^\w ]', '', options.title)
2563 print('WARNING: Patchset title may only contain alphanumeric chars '
2564 'and spaces. Cleaned up title:\n%s' % options.title)
2565 if not options.force:
2566 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002567 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2568 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002569 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2570
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002571 if options.send_mail:
2572 if not change_desc.get_reviewers():
2573 DieWithError('Must specify reviewers to send email.')
2574 refspec_opts.append('notify=ALL')
2575 else:
2576 refspec_opts.append('notify=NONE')
2577
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002578 cc = self.GetCCList().split(',')
2579 if options.cc:
2580 cc.extend(options.cc)
2581 cc = filter(None, cc)
2582 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002583 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002584
tandrii99a72f22016-08-17 14:33:24 -07002585 reviewers = change_desc.get_reviewers()
2586 if reviewers:
2587 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002588
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002589 refspec_suffix = ''
2590 if refspec_opts:
2591 refspec_suffix = '%' + ','.join(refspec_opts)
2592 assert ' ' not in refspec_suffix, (
2593 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002594 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002595
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002596 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002597 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002598 print_stdout=True,
2599 # Flush after every line: useful for seeing progress when running as
2600 # recipe.
2601 filter_fn=lambda _: sys.stdout.flush())
2602
2603 if options.squash:
2604 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2605 change_numbers = [m.group(1)
2606 for m in map(regex.match, push_stdout.splitlines())
2607 if m]
2608 if len(change_numbers) != 1:
2609 DieWithError(
2610 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2611 'Change-Id: %s') % (len(change_numbers), change_id))
2612 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002613 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002614 return 0
2615
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002616 def _AddChangeIdToCommitMessage(self, options, args):
2617 """Re-commits using the current message, assumes the commit hook is in
2618 place.
2619 """
2620 log_desc = options.message or CreateDescriptionFromLog(args)
2621 git_command = ['commit', '--amend', '-m', log_desc]
2622 RunGit(git_command)
2623 new_log_desc = CreateDescriptionFromLog(args)
2624 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002625 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002626 return new_log_desc
2627 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002628 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002629
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002630 def SetCQState(self, new_state):
2631 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002632 vote_map = {
2633 _CQState.NONE: 0,
2634 _CQState.DRY_RUN: 1,
2635 _CQState.COMMIT : 2,
2636 }
2637 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2638 labels={'Commit-Queue': vote_map[new_state]})
2639
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002640
2641_CODEREVIEW_IMPLEMENTATIONS = {
2642 'rietveld': _RietveldChangelistImpl,
2643 'gerrit': _GerritChangelistImpl,
2644}
2645
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002646
iannuccie53c9352016-08-17 14:40:40 -07002647def _add_codereview_issue_select_options(parser, extra=""):
2648 _add_codereview_select_options(parser)
2649
2650 text = ('Operate on this issue number instead of the current branch\'s '
2651 'implicit issue.')
2652 if extra:
2653 text += ' '+extra
2654 parser.add_option('-i', '--issue', type=int, help=text)
2655
2656
2657def _process_codereview_issue_select_options(parser, options):
2658 _process_codereview_select_options(parser, options)
2659 if options.issue is not None and not options.forced_codereview:
2660 parser.error('--issue must be specified with either --rietveld or --gerrit')
2661
2662
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002663def _add_codereview_select_options(parser):
2664 """Appends --gerrit and --rietveld options to force specific codereview."""
2665 parser.codereview_group = optparse.OptionGroup(
2666 parser, 'EXPERIMENTAL! Codereview override options')
2667 parser.add_option_group(parser.codereview_group)
2668 parser.codereview_group.add_option(
2669 '--gerrit', action='store_true',
2670 help='Force the use of Gerrit for codereview')
2671 parser.codereview_group.add_option(
2672 '--rietveld', action='store_true',
2673 help='Force the use of Rietveld for codereview')
2674
2675
2676def _process_codereview_select_options(parser, options):
2677 if options.gerrit and options.rietveld:
2678 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2679 options.forced_codereview = None
2680 if options.gerrit:
2681 options.forced_codereview = 'gerrit'
2682 elif options.rietveld:
2683 options.forced_codereview = 'rietveld'
2684
2685
tandriif9aefb72016-07-01 09:06:51 -07002686def _get_bug_line_values(default_project, bugs):
2687 """Given default_project and comma separated list of bugs, yields bug line
2688 values.
2689
2690 Each bug can be either:
2691 * a number, which is combined with default_project
2692 * string, which is left as is.
2693
2694 This function may produce more than one line, because bugdroid expects one
2695 project per line.
2696
2697 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2698 ['v8:123', 'chromium:789']
2699 """
2700 default_bugs = []
2701 others = []
2702 for bug in bugs.split(','):
2703 bug = bug.strip()
2704 if bug:
2705 try:
2706 default_bugs.append(int(bug))
2707 except ValueError:
2708 others.append(bug)
2709
2710 if default_bugs:
2711 default_bugs = ','.join(map(str, default_bugs))
2712 if default_project:
2713 yield '%s:%s' % (default_project, default_bugs)
2714 else:
2715 yield default_bugs
2716 for other in sorted(others):
2717 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2718 yield other
2719
2720
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002721class ChangeDescription(object):
2722 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002723 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002724 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002725
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002726 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002727 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002728
agable@chromium.org42c20792013-09-12 17:34:49 +00002729 @property # www.logilab.org/ticket/89786
2730 def description(self): # pylint: disable=E0202
2731 return '\n'.join(self._description_lines)
2732
2733 def set_description(self, desc):
2734 if isinstance(desc, basestring):
2735 lines = desc.splitlines()
2736 else:
2737 lines = [line.rstrip() for line in desc]
2738 while lines and not lines[0]:
2739 lines.pop(0)
2740 while lines and not lines[-1]:
2741 lines.pop(-1)
2742 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002743
piman@chromium.org336f9122014-09-04 02:16:55 +00002744 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002745 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002746 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002747 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002748 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002749 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002750
agable@chromium.org42c20792013-09-12 17:34:49 +00002751 # Get the set of R= and TBR= lines and remove them from the desciption.
2752 regexp = re.compile(self.R_LINE)
2753 matches = [regexp.match(line) for line in self._description_lines]
2754 new_desc = [l for i, l in enumerate(self._description_lines)
2755 if not matches[i]]
2756 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002757
agable@chromium.org42c20792013-09-12 17:34:49 +00002758 # Construct new unified R= and TBR= lines.
2759 r_names = []
2760 tbr_names = []
2761 for match in matches:
2762 if not match:
2763 continue
2764 people = cleanup_list([match.group(2).strip()])
2765 if match.group(1) == 'TBR':
2766 tbr_names.extend(people)
2767 else:
2768 r_names.extend(people)
2769 for name in r_names:
2770 if name not in reviewers:
2771 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002772 if add_owners_tbr:
2773 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002774 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002775 all_reviewers = set(tbr_names + reviewers)
2776 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2777 all_reviewers)
2778 tbr_names.extend(owners_db.reviewers_for(missing_files,
2779 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002780 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2781 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2782
2783 # Put the new lines in the description where the old first R= line was.
2784 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2785 if 0 <= line_loc < len(self._description_lines):
2786 if new_tbr_line:
2787 self._description_lines.insert(line_loc, new_tbr_line)
2788 if new_r_line:
2789 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002790 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002791 if new_r_line:
2792 self.append_footer(new_r_line)
2793 if new_tbr_line:
2794 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002795
tandriif9aefb72016-07-01 09:06:51 -07002796 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002797 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002798 self.set_description([
2799 '# Enter a description of the change.',
2800 '# This will be displayed on the codereview site.',
2801 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002802 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002803 '--------------------',
2804 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002805
agable@chromium.org42c20792013-09-12 17:34:49 +00002806 regexp = re.compile(self.BUG_LINE)
2807 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002808 prefix = settings.GetBugPrefix()
2809 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2810 for value in values:
2811 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2812 self.append_footer('BUG=%s' % value)
2813
agable@chromium.org42c20792013-09-12 17:34:49 +00002814 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002815 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002816 if not content:
2817 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002818 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002819
2820 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002821 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2822 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002823 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002824 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002825
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002826 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002827 """Adds a footer line to the description.
2828
2829 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2830 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2831 that Gerrit footers are always at the end.
2832 """
2833 parsed_footer_line = git_footers.parse_footer(line)
2834 if parsed_footer_line:
2835 # Line is a gerrit footer in the form: Footer-Key: any value.
2836 # Thus, must be appended observing Gerrit footer rules.
2837 self.set_description(
2838 git_footers.add_footer(self.description,
2839 key=parsed_footer_line[0],
2840 value=parsed_footer_line[1]))
2841 return
2842
2843 if not self._description_lines:
2844 self._description_lines.append(line)
2845 return
2846
2847 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2848 if gerrit_footers:
2849 # git_footers.split_footers ensures that there is an empty line before
2850 # actual (gerrit) footers, if any. We have to keep it that way.
2851 assert top_lines and top_lines[-1] == ''
2852 top_lines, separator = top_lines[:-1], top_lines[-1:]
2853 else:
2854 separator = [] # No need for separator if there are no gerrit_footers.
2855
2856 prev_line = top_lines[-1] if top_lines else ''
2857 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2858 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2859 top_lines.append('')
2860 top_lines.append(line)
2861 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002862
tandrii99a72f22016-08-17 14:33:24 -07002863 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002864 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002865 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002866 reviewers = [match.group(2).strip()
2867 for match in matches
2868 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002869 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002870
2871
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002872def get_approving_reviewers(props):
2873 """Retrieves the reviewers that approved a CL from the issue properties with
2874 messages.
2875
2876 Note that the list may contain reviewers that are not committer, thus are not
2877 considered by the CQ.
2878 """
2879 return sorted(
2880 set(
2881 message['sender']
2882 for message in props['messages']
2883 if message['approval'] and message['sender'] in props['reviewers']
2884 )
2885 )
2886
2887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888def FindCodereviewSettingsFile(filename='codereview.settings'):
2889 """Finds the given file starting in the cwd and going up.
2890
2891 Only looks up to the top of the repository unless an
2892 'inherit-review-settings-ok' file exists in the root of the repository.
2893 """
2894 inherit_ok_file = 'inherit-review-settings-ok'
2895 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002896 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002897 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2898 root = '/'
2899 while True:
2900 if filename in os.listdir(cwd):
2901 if os.path.isfile(os.path.join(cwd, filename)):
2902 return open(os.path.join(cwd, filename))
2903 if cwd == root:
2904 break
2905 cwd = os.path.dirname(cwd)
2906
2907
2908def LoadCodereviewSettingsFromFile(fileobj):
2909 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002910 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002912 def SetProperty(name, setting, unset_error_ok=False):
2913 fullname = 'rietveld.' + name
2914 if setting in keyvals:
2915 RunGit(['config', fullname, keyvals[setting]])
2916 else:
2917 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2918
2919 SetProperty('server', 'CODE_REVIEW_SERVER')
2920 # Only server setting is required. Other settings can be absent.
2921 # In that case, we ignore errors raised during option deletion attempt.
2922 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002923 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2925 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002926 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002927 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002928 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2929 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002930 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002931 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002932 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002933 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2934 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002936 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002937 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002938
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002939 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002940 RunGit(['config', 'gerrit.squash-uploads',
2941 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002942
tandrii@chromium.org28253532016-04-14 13:46:56 +00002943 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002944 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002945 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2948 #should be of the form
2949 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2950 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2951 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2952 keyvals['ORIGIN_URL_CONFIG']])
2953
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002955def urlretrieve(source, destination):
2956 """urllib is broken for SSL connections via a proxy therefore we
2957 can't use urllib.urlretrieve()."""
2958 with open(destination, 'w') as f:
2959 f.write(urllib2.urlopen(source).read())
2960
2961
ukai@chromium.org712d6102013-11-27 00:52:58 +00002962def hasSheBang(fname):
2963 """Checks fname is a #! script."""
2964 with open(fname) as f:
2965 return f.read(2).startswith('#!')
2966
2967
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002968# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2969def DownloadHooks(*args, **kwargs):
2970 pass
2971
2972
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002973def DownloadGerritHook(force):
2974 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002975
2976 Args:
2977 force: True to update hooks. False to install hooks if not present.
2978 """
2979 if not settings.GetIsGerrit():
2980 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002981 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002982 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2983 if not os.access(dst, os.X_OK):
2984 if os.path.exists(dst):
2985 if not force:
2986 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002987 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002988 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002989 if not hasSheBang(dst):
2990 DieWithError('Not a script: %s\n'
2991 'You need to download from\n%s\n'
2992 'into .git/hooks/commit-msg and '
2993 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002994 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2995 except Exception:
2996 if os.path.exists(dst):
2997 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002998 DieWithError('\nFailed to download hooks.\n'
2999 'You need to download from\n%s\n'
3000 'into .git/hooks/commit-msg and '
3001 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003002
3003
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003004
3005def GetRietveldCodereviewSettingsInteractively():
3006 """Prompt the user for settings."""
3007 server = settings.GetDefaultServerUrl(error_ok=True)
3008 prompt = 'Rietveld server (host[:port])'
3009 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3010 newserver = ask_for_data(prompt + ':')
3011 if not server and not newserver:
3012 newserver = DEFAULT_SERVER
3013 if newserver:
3014 newserver = gclient_utils.UpgradeToHttps(newserver)
3015 if newserver != server:
3016 RunGit(['config', 'rietveld.server', newserver])
3017
3018 def SetProperty(initial, caption, name, is_url):
3019 prompt = caption
3020 if initial:
3021 prompt += ' ("x" to clear) [%s]' % initial
3022 new_val = ask_for_data(prompt + ':')
3023 if new_val == 'x':
3024 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3025 elif new_val:
3026 if is_url:
3027 new_val = gclient_utils.UpgradeToHttps(new_val)
3028 if new_val != initial:
3029 RunGit(['config', 'rietveld.' + name, new_val])
3030
3031 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3032 SetProperty(settings.GetDefaultPrivateFlag(),
3033 'Private flag (rietveld only)', 'private', False)
3034 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3035 'tree-status-url', False)
3036 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3037 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3038 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3039 'run-post-upload-hook', False)
3040
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003041@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003043 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003045 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003046 'For Gerrit, see http://crbug.com/603116.')
3047 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003048 parser.add_option('--activate-update', action='store_true',
3049 help='activate auto-updating [rietveld] section in '
3050 '.git/config')
3051 parser.add_option('--deactivate-update', action='store_true',
3052 help='deactivate auto-updating [rietveld] section in '
3053 '.git/config')
3054 options, args = parser.parse_args(args)
3055
3056 if options.deactivate_update:
3057 RunGit(['config', 'rietveld.autoupdate', 'false'])
3058 return
3059
3060 if options.activate_update:
3061 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3062 return
3063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003064 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003065 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003066 return 0
3067
3068 url = args[0]
3069 if not url.endswith('codereview.settings'):
3070 url = os.path.join(url, 'codereview.settings')
3071
3072 # Load code review settings and download hooks (if available).
3073 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3074 return 0
3075
3076
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003077def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003078 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003079 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3080 branch = ShortBranchName(branchref)
3081 _, args = parser.parse_args(args)
3082 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003083 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003084 return RunGit(['config', 'branch.%s.base-url' % branch],
3085 error_ok=False).strip()
3086 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003087 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003088 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3089 error_ok=False).strip()
3090
3091
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003092def color_for_status(status):
3093 """Maps a Changelist status to color, for CMDstatus and other tools."""
3094 return {
3095 'unsent': Fore.RED,
3096 'waiting': Fore.BLUE,
3097 'reply': Fore.YELLOW,
3098 'lgtm': Fore.GREEN,
3099 'commit': Fore.MAGENTA,
3100 'closed': Fore.CYAN,
3101 'error': Fore.WHITE,
3102 }.get(status, Fore.WHITE)
3103
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003104
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003105def get_cl_statuses(changes, fine_grained, max_processes=None):
3106 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003107
3108 If fine_grained is true, this will fetch CL statuses from the server.
3109 Otherwise, simply indicate if there's a matching url for the given branches.
3110
3111 If max_processes is specified, it is used as the maximum number of processes
3112 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3113 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003114
3115 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003116 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003117 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003118 upload.verbosity = 0
3119
3120 if fine_grained:
3121 # Process one branch synchronously to work through authentication, then
3122 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003123 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003124 def fetch(cl):
3125 try:
3126 return (cl, cl.GetStatus())
3127 except:
3128 # See http://crbug.com/629863.
3129 logging.exception('failed to fetch status for %s:', cl)
3130 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003131 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003132
tandriiea9514a2016-08-17 12:32:37 -07003133 changes_to_fetch = changes[1:]
3134 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003135 # Exit early if there was only one branch to fetch.
3136 return
3137
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003138 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003139 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003140 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003141 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003142
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003143 fetched_cls = set()
3144 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003145 while True:
3146 try:
3147 row = it.next(timeout=5)
3148 except multiprocessing.TimeoutError:
3149 break
3150
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003151 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003152 yield row
3153
3154 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003155 for cl in set(changes_to_fetch) - fetched_cls:
3156 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003157
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003158 else:
3159 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003160 for cl in changes:
3161 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003162
rmistry@google.com2dd99862015-06-22 12:22:18 +00003163
3164def upload_branch_deps(cl, args):
3165 """Uploads CLs of local branches that are dependents of the current branch.
3166
3167 If the local branch dependency tree looks like:
3168 test1 -> test2.1 -> test3.1
3169 -> test3.2
3170 -> test2.2 -> test3.3
3171
3172 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3173 run on the dependent branches in this order:
3174 test2.1, test3.1, test3.2, test2.2, test3.3
3175
3176 Note: This function does not rebase your local dependent branches. Use it when
3177 you make a change to the parent branch that will not conflict with its
3178 dependent branches, and you would like their dependencies updated in
3179 Rietveld.
3180 """
3181 if git_common.is_dirty_git_tree('upload-branch-deps'):
3182 return 1
3183
3184 root_branch = cl.GetBranch()
3185 if root_branch is None:
3186 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3187 'Get on a branch!')
3188 if not cl.GetIssue() or not cl.GetPatchset():
3189 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3190 'patchset dependencies without an uploaded CL.')
3191
3192 branches = RunGit(['for-each-ref',
3193 '--format=%(refname:short) %(upstream:short)',
3194 'refs/heads'])
3195 if not branches:
3196 print('No local branches found.')
3197 return 0
3198
3199 # Create a dictionary of all local branches to the branches that are dependent
3200 # on it.
3201 tracked_to_dependents = collections.defaultdict(list)
3202 for b in branches.splitlines():
3203 tokens = b.split()
3204 if len(tokens) == 2:
3205 branch_name, tracked = tokens
3206 tracked_to_dependents[tracked].append(branch_name)
3207
vapiera7fbd5a2016-06-16 09:17:49 -07003208 print()
3209 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003210 dependents = []
3211 def traverse_dependents_preorder(branch, padding=''):
3212 dependents_to_process = tracked_to_dependents.get(branch, [])
3213 padding += ' '
3214 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003215 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003216 dependents.append(dependent)
3217 traverse_dependents_preorder(dependent, padding)
3218 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003219 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003220
3221 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003222 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003223 return 0
3224
vapiera7fbd5a2016-06-16 09:17:49 -07003225 print('This command will checkout all dependent branches and run '
3226 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003227 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3228
andybons@chromium.org962f9462016-02-03 20:00:42 +00003229 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003230 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003231 args.extend(['-t', 'Updated patchset dependency'])
3232
rmistry@google.com2dd99862015-06-22 12:22:18 +00003233 # Record all dependents that failed to upload.
3234 failures = {}
3235 # Go through all dependents, checkout the branch and upload.
3236 try:
3237 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003238 print()
3239 print('--------------------------------------')
3240 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003241 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003242 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003243 try:
3244 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003245 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003246 failures[dependent_branch] = 1
3247 except: # pylint: disable=W0702
3248 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003249 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003250 finally:
3251 # Swap back to the original root branch.
3252 RunGit(['checkout', '-q', root_branch])
3253
vapiera7fbd5a2016-06-16 09:17:49 -07003254 print()
3255 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003256 for dependent_branch in dependents:
3257 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003258 print(' %s : %s' % (dependent_branch, upload_status))
3259 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003260
3261 return 0
3262
3263
kmarshall3bff56b2016-06-06 18:31:47 -07003264def CMDarchive(parser, args):
3265 """Archives and deletes branches associated with closed changelists."""
3266 parser.add_option(
3267 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003268 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003269 parser.add_option(
3270 '-f', '--force', action='store_true',
3271 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003272 parser.add_option(
3273 '-d', '--dry-run', action='store_true',
3274 help='Skip the branch tagging and removal steps.')
3275 parser.add_option(
3276 '-t', '--notags', action='store_true',
3277 help='Do not tag archived branches. '
3278 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003279
3280 auth.add_auth_options(parser)
3281 options, args = parser.parse_args(args)
3282 if args:
3283 parser.error('Unsupported args: %s' % ' '.join(args))
3284 auth_config = auth.extract_auth_config_from_options(options)
3285
3286 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3287 if not branches:
3288 return 0
3289
vapiera7fbd5a2016-06-16 09:17:49 -07003290 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003291 changes = [Changelist(branchref=b, auth_config=auth_config)
3292 for b in branches.splitlines()]
3293 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3294 statuses = get_cl_statuses(changes,
3295 fine_grained=True,
3296 max_processes=options.maxjobs)
3297 proposal = [(cl.GetBranch(),
3298 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3299 for cl, status in statuses
3300 if status == 'closed']
3301 proposal.sort()
3302
3303 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003304 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003305 return 0
3306
3307 current_branch = GetCurrentBranch()
3308
vapiera7fbd5a2016-06-16 09:17:49 -07003309 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003310 if options.notags:
3311 for next_item in proposal:
3312 print(' ' + next_item[0])
3313 else:
3314 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3315 for next_item in proposal:
3316 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003317
kmarshall9249e012016-08-23 12:02:16 -07003318 # Quit now on precondition failure or if instructed by the user, either
3319 # via an interactive prompt or by command line flags.
3320 if options.dry_run:
3321 print('\nNo changes were made (dry run).\n')
3322 return 0
3323 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003324 print('You are currently on a branch \'%s\' which is associated with a '
3325 'closed codereview issue, so archive cannot proceed. Please '
3326 'checkout another branch and run this command again.' %
3327 current_branch)
3328 return 1
kmarshall9249e012016-08-23 12:02:16 -07003329 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003330 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3331 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003332 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003333 return 1
3334
3335 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003336 if not options.notags:
3337 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003338 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003339
vapiera7fbd5a2016-06-16 09:17:49 -07003340 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003341
3342 return 0
3343
3344
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003346 """Show status of changelists.
3347
3348 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003349 - Red not sent for review or broken
3350 - Blue waiting for review
3351 - Yellow waiting for you to reply to review
3352 - Green LGTM'ed
3353 - Magenta in the commit queue
3354 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003355
3356 Also see 'git cl comments'.
3357 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003358 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003359 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003360 parser.add_option('-f', '--fast', action='store_true',
3361 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003362 parser.add_option(
3363 '-j', '--maxjobs', action='store', type=int,
3364 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003365
3366 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003367 _add_codereview_issue_select_options(
3368 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003369 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003370 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003371 if args:
3372 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003373 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003374
iannuccie53c9352016-08-17 14:40:40 -07003375 if options.issue is not None and not options.field:
3376 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003379 cl = Changelist(auth_config=auth_config, issue=options.issue,
3380 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003382 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003383 elif options.field == 'id':
3384 issueid = cl.GetIssue()
3385 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003386 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003387 elif options.field == 'patch':
3388 patchset = cl.GetPatchset()
3389 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003390 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003391 elif options.field == 'status':
3392 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393 elif options.field == 'url':
3394 url = cl.GetIssueURL()
3395 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003396 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003397 return 0
3398
3399 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3400 if not branches:
3401 print('No local branch found.')
3402 return 0
3403
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003404 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003405 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003406 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003407 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003408 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003409 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003410 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003411
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003412 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003413 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3414 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3415 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003416 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003417 c, status = output.next()
3418 branch_statuses[c.GetBranch()] = status
3419 status = branch_statuses.pop(branch)
3420 url = cl.GetIssueURL()
3421 if url and (not status or status == 'error'):
3422 # The issue probably doesn't exist anymore.
3423 url += ' (broken)'
3424
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003425 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003426 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003427 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003428 color = ''
3429 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003430 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003431 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003432 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003433 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003434
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003435 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003436 print()
3437 print('Current branch:',)
3438 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003439 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003440 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003441 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003442 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003443 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003444 print('Issue description:')
3445 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003446 return 0
3447
3448
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003449def colorize_CMDstatus_doc():
3450 """To be called once in main() to add colors to git cl status help."""
3451 colors = [i for i in dir(Fore) if i[0].isupper()]
3452
3453 def colorize_line(line):
3454 for color in colors:
3455 if color in line.upper():
3456 # Extract whitespaces first and the leading '-'.
3457 indent = len(line) - len(line.lstrip(' ')) + 1
3458 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3459 return line
3460
3461 lines = CMDstatus.__doc__.splitlines()
3462 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3463
3464
phajdan.jre328cf92016-08-22 04:12:17 -07003465def write_json(path, contents):
3466 with open(path, 'w') as f:
3467 json.dump(contents, f)
3468
3469
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003470@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003471def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003472 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473
3474 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003475 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003476 parser.add_option('-r', '--reverse', action='store_true',
3477 help='Lookup the branch(es) for the specified issues. If '
3478 'no issues are specified, all branches with mapped '
3479 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003480 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003481 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003482 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003483 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003484
dnj@chromium.org406c4402015-03-03 17:22:28 +00003485 if options.reverse:
3486 branches = RunGit(['for-each-ref', 'refs/heads',
3487 '--format=%(refname:short)']).splitlines()
3488
3489 # Reverse issue lookup.
3490 issue_branch_map = {}
3491 for branch in branches:
3492 cl = Changelist(branchref=branch)
3493 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3494 if not args:
3495 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003496 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003497 for issue in args:
3498 if not issue:
3499 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003500 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003501 print('Branch for issue number %s: %s' % (
3502 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003503 if options.json:
3504 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003505 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003506 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003507 if len(args) > 0:
3508 try:
3509 issue = int(args[0])
3510 except ValueError:
3511 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003512 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003513 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003514 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003515 if options.json:
3516 write_json(options.json, {
3517 'issue': cl.GetIssue(),
3518 'issue_url': cl.GetIssueURL(),
3519 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003520 return 0
3521
3522
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003523def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003524 """Shows or posts review comments for any changelist."""
3525 parser.add_option('-a', '--add-comment', dest='comment',
3526 help='comment to add to an issue')
3527 parser.add_option('-i', dest='issue',
3528 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003529 parser.add_option('-j', '--json-file',
3530 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003531 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003532 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003533 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003534
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003535 issue = None
3536 if options.issue:
3537 try:
3538 issue = int(options.issue)
3539 except ValueError:
3540 DieWithError('A review issue id is expected to be a number')
3541
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003542 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003543
3544 if options.comment:
3545 cl.AddComment(options.comment)
3546 return 0
3547
3548 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003549 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003550 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003551 summary.append({
3552 'date': message['date'],
3553 'lgtm': False,
3554 'message': message['text'],
3555 'not_lgtm': False,
3556 'sender': message['sender'],
3557 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003558 if message['disapproval']:
3559 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003560 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003561 elif message['approval']:
3562 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003563 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003564 elif message['sender'] == data['owner_email']:
3565 color = Fore.MAGENTA
3566 else:
3567 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003569 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003570 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003571 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003573 if options.json_file:
3574 with open(options.json_file, 'wb') as f:
3575 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003576 return 0
3577
3578
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003579@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003580def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003581 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003582 parser.add_option('-d', '--display', action='store_true',
3583 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003584 parser.add_option('-n', '--new-description',
3585 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003586
3587 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003588 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003589 options, args = parser.parse_args(args)
3590 _process_codereview_select_options(parser, options)
3591
3592 target_issue = None
3593 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003594 target_issue = ParseIssueNumberArgument(args[0])
3595 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003596 parser.print_help()
3597 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003598
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003599 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003600
martiniss6eda05f2016-06-30 10:18:35 -07003601 kwargs = {
3602 'auth_config': auth_config,
3603 'codereview': options.forced_codereview,
3604 }
3605 if target_issue:
3606 kwargs['issue'] = target_issue.issue
3607 if options.forced_codereview == 'rietveld':
3608 kwargs['rietveld_server'] = target_issue.hostname
3609
3610 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003611
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003612 if not cl.GetIssue():
3613 DieWithError('This branch has no associated changelist.')
3614 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003615
smut@google.com34fb6b12015-07-13 20:03:26 +00003616 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003617 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003618 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003619
3620 if options.new_description:
3621 text = options.new_description
3622 if text == '-':
3623 text = '\n'.join(l.rstrip() for l in sys.stdin)
3624
3625 description.set_description(text)
3626 else:
3627 description.prompt()
3628
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003629 if cl.GetDescription() != description.description:
3630 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003631 return 0
3632
3633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634def CreateDescriptionFromLog(args):
3635 """Pulls out the commit log to use as a base for the CL description."""
3636 log_args = []
3637 if len(args) == 1 and not args[0].endswith('.'):
3638 log_args = [args[0] + '..']
3639 elif len(args) == 1 and args[0].endswith('...'):
3640 log_args = [args[0][:-1]]
3641 elif len(args) == 2:
3642 log_args = [args[0] + '..' + args[1]]
3643 else:
3644 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003645 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003646
3647
thestig@chromium.org44202a22014-03-11 19:22:18 +00003648def CMDlint(parser, args):
3649 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003650 parser.add_option('--filter', action='append', metavar='-x,+y',
3651 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003652 auth.add_auth_options(parser)
3653 options, args = parser.parse_args(args)
3654 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003655
3656 # Access to a protected member _XX of a client class
3657 # pylint: disable=W0212
3658 try:
3659 import cpplint
3660 import cpplint_chromium
3661 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003662 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003663 return 1
3664
3665 # Change the current working directory before calling lint so that it
3666 # shows the correct base.
3667 previous_cwd = os.getcwd()
3668 os.chdir(settings.GetRoot())
3669 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003670 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003671 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3672 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003673 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003675 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003676
3677 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003678 command = args + files
3679 if options.filter:
3680 command = ['--filter=' + ','.join(options.filter)] + command
3681 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003682
3683 white_regex = re.compile(settings.GetLintRegex())
3684 black_regex = re.compile(settings.GetLintIgnoreRegex())
3685 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3686 for filename in filenames:
3687 if white_regex.match(filename):
3688 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003689 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003690 else:
3691 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3692 extra_check_functions)
3693 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003695 finally:
3696 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003697 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003698 if cpplint._cpplint_state.error_count != 0:
3699 return 1
3700 return 0
3701
3702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003704 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003705 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003707 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003708 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 auth.add_auth_options(parser)
3710 options, args = parser.parse_args(args)
3711 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003712
sbc@chromium.org71437c02015-04-09 19:29:40 +00003713 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003714 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003715 return 1
3716
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003717 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003718 if args:
3719 base_branch = args[0]
3720 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003721 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003722 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003724 cl.RunHook(
3725 committing=not options.upload,
3726 may_prompt=False,
3727 verbose=options.verbose,
3728 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003729 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730
3731
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003732def GenerateGerritChangeId(message):
3733 """Returns Ixxxxxx...xxx change id.
3734
3735 Works the same way as
3736 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3737 but can be called on demand on all platforms.
3738
3739 The basic idea is to generate git hash of a state of the tree, original commit
3740 message, author/committer info and timestamps.
3741 """
3742 lines = []
3743 tree_hash = RunGitSilent(['write-tree'])
3744 lines.append('tree %s' % tree_hash.strip())
3745 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3746 if code == 0:
3747 lines.append('parent %s' % parent.strip())
3748 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3749 lines.append('author %s' % author.strip())
3750 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3751 lines.append('committer %s' % committer.strip())
3752 lines.append('')
3753 # Note: Gerrit's commit-hook actually cleans message of some lines and
3754 # whitespace. This code is not doing this, but it clearly won't decrease
3755 # entropy.
3756 lines.append(message)
3757 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3758 stdin='\n'.join(lines))
3759 return 'I%s' % change_hash.strip()
3760
3761
wittman@chromium.org455dc922015-01-26 20:15:50 +00003762def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3763 """Computes the remote branch ref to use for the CL.
3764
3765 Args:
3766 remote (str): The git remote for the CL.
3767 remote_branch (str): The git remote branch for the CL.
3768 target_branch (str): The target branch specified by the user.
3769 pending_prefix (str): The pending prefix from the settings.
3770 """
3771 if not (remote and remote_branch):
3772 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003773
wittman@chromium.org455dc922015-01-26 20:15:50 +00003774 if target_branch:
3775 # Cannonicalize branch references to the equivalent local full symbolic
3776 # refs, which are then translated into the remote full symbolic refs
3777 # below.
3778 if '/' not in target_branch:
3779 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3780 else:
3781 prefix_replacements = (
3782 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3783 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3784 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3785 )
3786 match = None
3787 for regex, replacement in prefix_replacements:
3788 match = re.search(regex, target_branch)
3789 if match:
3790 remote_branch = target_branch.replace(match.group(0), replacement)
3791 break
3792 if not match:
3793 # This is a branch path but not one we recognize; use as-is.
3794 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003795 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3796 # Handle the refs that need to land in different refs.
3797 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003798
wittman@chromium.org455dc922015-01-26 20:15:50 +00003799 # Create the true path to the remote branch.
3800 # Does the following translation:
3801 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3802 # * refs/remotes/origin/master -> refs/heads/master
3803 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3804 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3805 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3806 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3807 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3808 'refs/heads/')
3809 elif remote_branch.startswith('refs/remotes/branch-heads'):
3810 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3811 # If a pending prefix exists then replace refs/ with it.
3812 if pending_prefix:
3813 remote_branch = remote_branch.replace('refs/', pending_prefix)
3814 return remote_branch
3815
3816
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003817def cleanup_list(l):
3818 """Fixes a list so that comma separated items are put as individual items.
3819
3820 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3821 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3822 """
3823 items = sum((i.split(',') for i in l), [])
3824 stripped_items = (i.strip() for i in items)
3825 return sorted(filter(None, stripped_items))
3826
3827
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003828@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003829def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003830 """Uploads the current changelist to codereview.
3831
3832 Can skip dependency patchset uploads for a branch by running:
3833 git config branch.branch_name.skip-deps-uploads True
3834 To unset run:
3835 git config --unset branch.branch_name.skip-deps-uploads
3836 Can also set the above globally by using the --global flag.
3837 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003838 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3839 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003840 parser.add_option('--bypass-watchlists', action='store_true',
3841 dest='bypass_watchlists',
3842 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003843 parser.add_option('-f', action='store_true', dest='force',
3844 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003845 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003846 parser.add_option('-b', '--bug',
3847 help='pre-populate the bug number(s) for this issue. '
3848 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003849 parser.add_option('--message-file', dest='message_file',
3850 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003851 parser.add_option('-t', dest='title',
3852 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003853 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003854 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003855 help='reviewer email addresses')
3856 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003857 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003858 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003859 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003860 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003861 parser.add_option('--emulate_svn_auto_props',
3862 '--emulate-svn-auto-props',
3863 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003864 dest="emulate_svn_auto_props",
3865 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003866 parser.add_option('-c', '--use-commit-queue', action='store_true',
3867 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003868 parser.add_option('--private', action='store_true',
3869 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003870 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003871 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003872 metavar='TARGET',
3873 help='Apply CL to remote ref TARGET. ' +
3874 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003875 parser.add_option('--squash', action='store_true',
3876 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003877 parser.add_option('--no-squash', action='store_true',
3878 help='Don\'t squash multiple commits into one ' +
3879 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003880 parser.add_option('--email', default=None,
3881 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003882 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3883 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003884 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3885 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003886 help='Send the patchset to do a CQ dry run right after '
3887 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003888 parser.add_option('--dependencies', action='store_true',
3889 help='Uploads CLs of all the local branches that depend on '
3890 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003891
rmistry@google.com2dd99862015-06-22 12:22:18 +00003892 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003893 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003894 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003895 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003896 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003897 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003898 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003899
sbc@chromium.org71437c02015-04-09 19:29:40 +00003900 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003901 return 1
3902
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003903 options.reviewers = cleanup_list(options.reviewers)
3904 options.cc = cleanup_list(options.cc)
3905
tandriib80458a2016-06-23 12:20:07 -07003906 if options.message_file:
3907 if options.message:
3908 parser.error('only one of --message and --message-file allowed.')
3909 options.message = gclient_utils.FileRead(options.message_file)
3910 options.message_file = None
3911
tandrii4d0545a2016-07-06 03:56:49 -07003912 if options.cq_dry_run and options.use_commit_queue:
3913 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3914
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003915 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3916 settings.GetIsGerrit()
3917
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003918 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003919 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003920
3921
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003922def IsSubmoduleMergeCommit(ref):
3923 # When submodules are added to the repo, we expect there to be a single
3924 # non-git-svn merge commit at remote HEAD with a signature comment.
3925 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003926 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003927 return RunGit(cmd) != ''
3928
3929
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003930def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003931 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003933 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3934 upstream and closes the issue automatically and atomically.
3935
3936 Otherwise (in case of Rietveld):
3937 Squashes branch into a single commit.
3938 Updates changelog with metadata (e.g. pointer to review).
3939 Pushes/dcommits the code upstream.
3940 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941 """
3942 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3943 help='bypass upload presubmit hook')
3944 parser.add_option('-m', dest='message',
3945 help="override review description")
3946 parser.add_option('-f', action='store_true', dest='force',
3947 help="force yes to questions (don't prompt)")
3948 parser.add_option('-c', dest='contributor',
3949 help="external contributor for patch (appended to " +
3950 "description and used as author for git). Should be " +
3951 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003952 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003953 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003955 auth_config = auth.extract_auth_config_from_options(options)
3956
3957 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003959 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3960 if cl.IsGerrit():
3961 if options.message:
3962 # This could be implemented, but it requires sending a new patch to
3963 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3964 # Besides, Gerrit has the ability to change the commit message on submit
3965 # automatically, thus there is no need to support this option (so far?).
3966 parser.error('-m MESSAGE option is not supported for Gerrit.')
3967 if options.contributor:
3968 parser.error(
3969 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3970 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3971 'the contributor\'s "name <email>". If you can\'t upload such a '
3972 'commit for review, contact your repository admin and request'
3973 '"Forge-Author" permission.')
3974 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3975 options.verbose)
3976
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003977 current = cl.GetBranch()
3978 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3979 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003980 print()
3981 print('Attempting to push branch %r into another local branch!' % current)
3982 print()
3983 print('Either reparent this branch on top of origin/master:')
3984 print(' git reparent-branch --root')
3985 print()
3986 print('OR run `git rebase-update` if you think the parent branch is ')
3987 print('already committed.')
3988 print()
3989 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003990 return 1
3991
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003992 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993 # Default to merging against our best guess of the upstream branch.
3994 args = [cl.GetUpstreamBranch()]
3995
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003996 if options.contributor:
3997 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003998 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003999 return 1
4000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004002 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003
sbc@chromium.org71437c02015-04-09 19:29:40 +00004004 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 return 1
4006
4007 # This rev-list syntax means "show all commits not in my branch that
4008 # are in base_branch".
4009 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4010 base_branch]).splitlines()
4011 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004012 print('Base branch "%s" has %d commits '
4013 'not in this branch.' % (base_branch, len(upstream_commits)))
4014 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004015 return 1
4016
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004017 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004018 svn_head = None
4019 if cmd == 'dcommit' or base_has_submodules:
4020 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4021 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004022
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004023 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004024 # If the base_head is a submodule merge commit, the first parent of the
4025 # base_head should be a git-svn commit, which is what we're interested in.
4026 base_svn_head = base_branch
4027 if base_has_submodules:
4028 base_svn_head += '^1'
4029
4030 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004032 print('This branch has %d additional commits not upstreamed yet.'
4033 % len(extra_commits.splitlines()))
4034 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4035 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004036 return 1
4037
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004038 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004039 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004040 author = None
4041 if options.contributor:
4042 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004043 hook_results = cl.RunHook(
4044 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004045 may_prompt=not options.force,
4046 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004047 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004048 if not hook_results.should_continue():
4049 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004051 # Check the tree status if the tree status URL is set.
4052 status = GetTreeStatus()
4053 if 'closed' == status:
4054 print('The tree is closed. Please wait for it to reopen. Use '
4055 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4056 return 1
4057 elif 'unknown' == status:
4058 print('Unable to determine tree status. Please verify manually and '
4059 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4060 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004062 change_desc = ChangeDescription(options.message)
4063 if not change_desc.description and cl.GetIssue():
4064 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004066 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004067 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004068 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004069 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004070 print('No description set.')
4071 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004072 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004074 # Keep a separate copy for the commit message, because the commit message
4075 # contains the link to the Rietveld issue, while the Rietveld message contains
4076 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004077 # Keep a separate copy for the commit message.
4078 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004079 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004080
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004081 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004082 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004083 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004084 # after it. Add a period on a new line to circumvent this. Also add a space
4085 # before the period to make sure that Gitiles continues to correctly resolve
4086 # the URL.
4087 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004089 commit_desc.append_footer('Patch from %s.' % options.contributor)
4090
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004091 print('Description:')
4092 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004094 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004096 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004097
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004098 # We want to squash all this branch's commits into one commit with the proper
4099 # description. We do this by doing a "reset --soft" to the base branch (which
4100 # keeps the working copy the same), then dcommitting that. If origin/master
4101 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4102 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004103 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004104 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4105 # Delete the branches if they exist.
4106 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4107 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4108 result = RunGitWithCode(showref_cmd)
4109 if result[0] == 0:
4110 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004111
4112 # We might be in a directory that's present in this branch but not in the
4113 # trunk. Move up to the top of the tree so that git commands that expect a
4114 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004115 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116 if rel_base_path:
4117 os.chdir(rel_base_path)
4118
4119 # Stuff our change into the merge branch.
4120 # We wrap in a try...finally block so if anything goes wrong,
4121 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004122 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004123 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004124 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004125 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004126 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004127 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004128 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004129 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004130 RunGit(
4131 [
4132 'commit', '--author', options.contributor,
4133 '-m', commit_desc.description,
4134 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004135 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004136 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004137 if base_has_submodules:
4138 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4139 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4140 RunGit(['checkout', CHERRY_PICK_BRANCH])
4141 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004142 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004143 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004144 mirror = settings.GetGitMirror(remote)
4145 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004146 pending_prefix = settings.GetPendingRefPrefix()
4147 if not pending_prefix or branch.startswith(pending_prefix):
4148 # If not using refs/pending/heads/* at all, or target ref is already set
4149 # to pending, then push to the target ref directly.
4150 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004151 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004152 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004153 else:
4154 # Cherry-pick the change on top of pending ref and then push it.
4155 assert branch.startswith('refs/'), branch
4156 assert pending_prefix[-1] == '/', pending_prefix
4157 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004158 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004159 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004160 if retcode == 0:
4161 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004162 else:
4163 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004164 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004165 'svn', 'dcommit',
4166 '-C%s' % options.similarity,
4167 '--no-rebase', '--rmdir',
4168 ]
4169 if settings.GetForceHttpsCommitUrl():
4170 # Allow forcing https commit URLs for some projects that don't allow
4171 # committing to http URLs (like Google Code).
4172 remote_url = cl.GetGitSvnRemoteUrl()
4173 if urlparse.urlparse(remote_url).scheme == 'http':
4174 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004175 cmd_args.append('--commit-url=%s' % remote_url)
4176 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004177 if 'Committed r' in output:
4178 revision = re.match(
4179 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4180 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181 finally:
4182 # And then swap back to the original branch and clean up.
4183 RunGit(['checkout', '-q', cl.GetBranch()])
4184 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004185 if base_has_submodules:
4186 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004187
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004188 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004189 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004190 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004191
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004192 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004193 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004194 try:
4195 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4196 # We set pushed_to_pending to False, since it made it all the way to the
4197 # real ref.
4198 pushed_to_pending = False
4199 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004200 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004201
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004203 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004205 if not to_pending:
4206 if viewvc_url and revision:
4207 change_desc.append_footer(
4208 'Committed: %s%s' % (viewvc_url, revision))
4209 elif revision:
4210 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004211 print('Closing issue '
4212 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004213 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004214 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004215 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004216 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004217 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004218 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004219 if options.bypass_hooks:
4220 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4221 else:
4222 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004223 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004224
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004225 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004226 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004227 print('The commit is in the pending queue (%s).' % pending_ref)
4228 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4229 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004230
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004231 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4232 if os.path.isfile(hook):
4233 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004234
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004235 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236
4237
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004238def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004239 print()
4240 print('Waiting for commit to be landed on %s...' % real_ref)
4241 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004242 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4243 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004244 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004245
4246 loop = 0
4247 while True:
4248 sys.stdout.write('fetching (%d)... \r' % loop)
4249 sys.stdout.flush()
4250 loop += 1
4251
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004252 if mirror:
4253 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004254 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4255 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4256 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4257 for commit in commits.splitlines():
4258 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004259 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004260 return commit
4261
4262 current_rev = to_rev
4263
4264
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004265def PushToGitPending(remote, pending_ref, upstream_ref):
4266 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4267
4268 Returns:
4269 (retcode of last operation, output log of last operation).
4270 """
4271 assert pending_ref.startswith('refs/'), pending_ref
4272 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4273 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4274 code = 0
4275 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004276 max_attempts = 3
4277 attempts_left = max_attempts
4278 while attempts_left:
4279 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004281 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004282
4283 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004284 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004285 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004286 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004287 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004288 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004289 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004290 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004291 continue
4292
4293 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004294 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004295 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004296 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004297 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004298 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4299 'the following files have merge conflicts:' % pending_ref)
4300 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4301 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004302 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004303 return code, out
4304
4305 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004306 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004307 code, out = RunGitWithCode(
4308 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4309 if code == 0:
4310 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004311 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004312 return code, out
4313
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004315 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004316 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004317 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004318 print('Fatal push error. Make sure your .netrc credentials and git '
4319 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004320 return code, out
4321
vapiera7fbd5a2016-06-16 09:17:49 -07004322 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004323 return code, out
4324
4325
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004326def IsFatalPushFailure(push_stdout):
4327 """True if retrying push won't help."""
4328 return '(prohibited by Gerrit)' in push_stdout
4329
4330
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004331@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004333 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004334 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004335 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004336 # If it looks like previous commits were mirrored with git-svn.
4337 message = """This repository appears to be a git-svn mirror, but no
4338upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4339 else:
4340 message = """This doesn't appear to be an SVN repository.
4341If your project has a true, writeable git repository, you probably want to run
4342'git cl land' instead.
4343If your project has a git mirror of an upstream SVN master, you probably need
4344to run 'git svn init'.
4345
4346Using the wrong command might cause your commit to appear to succeed, and the
4347review to be closed, without actually landing upstream. If you choose to
4348proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004349 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004350 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004351 # TODO(tandrii): kill this post SVN migration with
4352 # https://codereview.chromium.org/2076683002
4353 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4354 'Please let us know of this project you are committing to:'
4355 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356 return SendUpstream(parser, args, 'dcommit')
4357
4358
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004359@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004360def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004361 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004362 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004363 print('This appears to be an SVN repository.')
4364 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004365 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004366 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004367 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004368
4369
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004370@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004371def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004372 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373 parser.add_option('-b', dest='newbranch',
4374 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004375 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004377 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4378 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004379 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004380 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004381 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004382 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004383 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004384 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004385
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004386
4387 group = optparse.OptionGroup(
4388 parser,
4389 'Options for continuing work on the current issue uploaded from a '
4390 'different clone (e.g. different machine). Must be used independently '
4391 'from the other options. No issue number should be specified, and the '
4392 'branch must have an issue number associated with it')
4393 group.add_option('--reapply', action='store_true', dest='reapply',
4394 help='Reset the branch and reapply the issue.\n'
4395 'CAUTION: This will undo any local changes in this '
4396 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004397
4398 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004399 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004400 parser.add_option_group(group)
4401
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004402 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004403 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004405 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004406 auth_config = auth.extract_auth_config_from_options(options)
4407
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004408
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004409 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004410 if options.newbranch:
4411 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004412 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004413 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004414
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004415 cl = Changelist(auth_config=auth_config,
4416 codereview=options.forced_codereview)
4417 if not cl.GetIssue():
4418 parser.error('current branch must have an associated issue')
4419
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004420 upstream = cl.GetUpstreamBranch()
4421 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004422 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004423
4424 RunGit(['reset', '--hard', upstream])
4425 if options.pull:
4426 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004427
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004428 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4429 options.directory)
4430
4431 if len(args) != 1 or not args[0]:
4432 parser.error('Must specify issue number or url')
4433
4434 # We don't want uncommitted changes mixed up with the patch.
4435 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004436 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004438 if options.newbranch:
4439 if options.force:
4440 RunGit(['branch', '-D', options.newbranch],
4441 stderr=subprocess2.PIPE, error_ok=True)
4442 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004443 elif not GetCurrentBranch():
4444 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004445
4446 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4447
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004448 if cl.IsGerrit():
4449 if options.reject:
4450 parser.error('--reject is not supported with Gerrit codereview.')
4451 if options.nocommit:
4452 parser.error('--nocommit is not supported with Gerrit codereview.')
4453 if options.directory:
4454 parser.error('--directory is not supported with Gerrit codereview.')
4455
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004456 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004457 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004458
4459
4460def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004461 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462 # Provide a wrapper for git svn rebase to help avoid accidental
4463 # git svn dcommit.
4464 # It's the only command that doesn't use parser at all since we just defer
4465 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004466
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004467 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004468
4469
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004470def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004471 """Fetches the tree status and returns either 'open', 'closed',
4472 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004473 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004474 if url:
4475 status = urllib2.urlopen(url).read().lower()
4476 if status.find('closed') != -1 or status == '0':
4477 return 'closed'
4478 elif status.find('open') != -1 or status == '1':
4479 return 'open'
4480 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004481 return 'unset'
4482
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004483
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004484def GetTreeStatusReason():
4485 """Fetches the tree status from a json url and returns the message
4486 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004487 url = settings.GetTreeStatusUrl()
4488 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004489 connection = urllib2.urlopen(json_url)
4490 status = json.loads(connection.read())
4491 connection.close()
4492 return status['message']
4493
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004494
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004495def GetBuilderMaster(bot_list):
4496 """For a given builder, fetch the master from AE if available."""
4497 map_url = 'https://builders-map.appspot.com/'
4498 try:
4499 master_map = json.load(urllib2.urlopen(map_url))
4500 except urllib2.URLError as e:
4501 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4502 (map_url, e))
4503 except ValueError as e:
4504 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4505 if not master_map:
4506 return None, 'Failed to build master map.'
4507
4508 result_master = ''
4509 for bot in bot_list:
4510 builder = bot.split(':', 1)[0]
4511 master_list = master_map.get(builder, [])
4512 if not master_list:
4513 return None, ('No matching master for builder %s.' % builder)
4514 elif len(master_list) > 1:
4515 return None, ('The builder name %s exists in multiple masters %s.' %
4516 (builder, master_list))
4517 else:
4518 cur_master = master_list[0]
4519 if not result_master:
4520 result_master = cur_master
4521 elif result_master != cur_master:
4522 return None, 'The builders do not belong to the same master.'
4523 return result_master, None
4524
4525
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004527 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004528 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529 status = GetTreeStatus()
4530 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004531 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532 return 2
4533
vapiera7fbd5a2016-06-16 09:17:49 -07004534 print('The tree is %s' % status)
4535 print()
4536 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 if status != 'open':
4538 return 1
4539 return 0
4540
4541
maruel@chromium.org15192402012-09-06 12:38:29 +00004542def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004543 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004544 group = optparse.OptionGroup(parser, "Try job options")
4545 group.add_option(
4546 "-b", "--bot", action="append",
4547 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4548 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004549 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004550 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004551 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004552 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004553 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004554 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004555 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004556 "-r", "--revision",
4557 help="Revision to use for the try job; default: the "
4558 "revision will be determined by the try server; see "
4559 "its waterfall for more info")
4560 group.add_option(
4561 "-c", "--clobber", action="store_true", default=False,
4562 help="Force a clobber before building; e.g. don't do an "
4563 "incremental build")
4564 group.add_option(
4565 "--project",
4566 help="Override which project to use. Projects are defined "
4567 "server-side to define what default bot set to use")
4568 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004569 "-p", "--property", dest="properties", action="append", default=[],
4570 help="Specify generic properties in the form -p key1=value1 -p "
4571 "key2=value2 etc (buildbucket only). The value will be treated as "
4572 "json if decodable, or as string otherwise.")
4573 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004574 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004575 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004576 "--use-rietveld", action="store_true", default=False,
4577 help="Use Rietveld to trigger try jobs.")
4578 group.add_option(
4579 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4580 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004581 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004582 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004583 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004584 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004585
machenbach@chromium.org45453142015-09-15 08:45:22 +00004586 if options.use_rietveld and options.properties:
4587 parser.error('Properties can only be specified with buildbucket')
4588
4589 # Make sure that all properties are prop=value pairs.
4590 bad_params = [x for x in options.properties if '=' not in x]
4591 if bad_params:
4592 parser.error('Got properties with missing "=": %s' % bad_params)
4593
maruel@chromium.org15192402012-09-06 12:38:29 +00004594 if args:
4595 parser.error('Unknown arguments: %s' % args)
4596
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004597 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004598 if not cl.GetIssue():
4599 parser.error('Need to upload first')
4600
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004601 if cl.IsGerrit():
4602 parser.error(
4603 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4604 'If your project has Commit Queue, dry run is a workaround:\n'
4605 ' git cl set-commit --dry-run')
4606 # Code below assumes Rietveld issue.
4607 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4608
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004609 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004610 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004611 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004612
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004613 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004614 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004615
maruel@chromium.org15192402012-09-06 12:38:29 +00004616 if not options.name:
4617 options.name = cl.GetBranch()
4618
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004619 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004620 options.master, err_msg = GetBuilderMaster(options.bot)
4621 if err_msg:
4622 parser.error('Tryserver master cannot be found because: %s\n'
4623 'Please manually specify the tryserver master'
4624 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004625
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004626 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004627 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004628 if not options.bot:
4629 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004630
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004631 # Get try masters from PRESUBMIT.py files.
4632 masters = presubmit_support.DoGetTryMasters(
4633 change,
4634 change.LocalPaths(),
4635 settings.GetRoot(),
4636 None,
4637 None,
4638 options.verbose,
4639 sys.stdout)
4640 if masters:
4641 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004642
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004643 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4644 options.bot = presubmit_support.DoGetTrySlaves(
4645 change,
4646 change.LocalPaths(),
4647 settings.GetRoot(),
4648 None,
4649 None,
4650 options.verbose,
4651 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004652
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004653 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004654 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004655
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004656 builders_and_tests = {}
4657 # TODO(machenbach): The old style command-line options don't support
4658 # multiple try masters yet.
4659 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4660 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4661
4662 for bot in old_style:
4663 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004664 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004665 elif ',' in bot:
4666 parser.error('Specify one bot per --bot flag')
4667 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004668 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004669
4670 for bot, tests in new_style:
4671 builders_and_tests.setdefault(bot, []).extend(tests)
4672
4673 # Return a master map with one master to be backwards compatible. The
4674 # master name defaults to an empty string, which will cause the master
4675 # not to be set on rietveld (deprecated).
4676 return {options.master: builders_and_tests}
4677
4678 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004679 if not masters:
4680 # Default to triggering Dry Run (see http://crbug.com/625697).
4681 if options.verbose:
4682 print('git cl try with no bots now defaults to CQ Dry Run.')
4683 try:
4684 cl.SetCQState(_CQState.DRY_RUN)
4685 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4686 return 0
4687 except KeyboardInterrupt:
4688 raise
4689 except:
4690 print('WARNING: failed to trigger CQ Dry Run.\n'
4691 'Either:\n'
4692 ' * your project has no CQ\n'
4693 ' * you don\'t have permission to trigger Dry Run\n'
4694 ' * bug in this code (see stack trace below).\n'
4695 'Consider specifying which bots to trigger manually '
4696 'or asking your project owners for permissions '
4697 'or contacting Chrome Infrastructure team at '
4698 'https://www.chromium.org/infra\n\n')
4699 # Still raise exception so that stack trace is printed.
4700 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004701
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004702 for builders in masters.itervalues():
4703 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004704 print('ERROR You are trying to send a job to a triggered bot. This type '
4705 'of bot requires an\ninitial job from a parent (usually a builder).'
4706 ' Instead send your job to the parent.\n'
4707 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004708 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004709
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004710 patchset = cl.GetMostRecentPatchset()
4711 if patchset and patchset != cl.GetPatchset():
4712 print(
4713 '\nWARNING Mismatch between local config and server. Did a previous '
4714 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4715 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004716 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004717 try:
4718 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4719 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004720 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004721 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004722 except Exception as e:
4723 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004724 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004725 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004726 return 1
4727 else:
4728 try:
4729 cl.RpcServer().trigger_distributed_try_jobs(
4730 cl.GetIssue(), patchset, options.name, options.clobber,
4731 options.revision, masters)
4732 except urllib2.HTTPError as e:
4733 if e.code == 404:
4734 print('404 from rietveld; '
4735 'did you mean to use "git try" instead of "git cl try"?')
4736 return 1
4737 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004738
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004739 for (master, builders) in sorted(masters.iteritems()):
4740 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004741 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004742 length = max(len(builder) for builder in builders)
4743 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004744 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004745 return 0
4746
4747
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004748def CMDtry_results(parser, args):
4749 group = optparse.OptionGroup(parser, "Try job results options")
4750 group.add_option(
4751 "-p", "--patchset", type=int, help="patchset number if not current.")
4752 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004753 "--print-master", action='store_true', help="print master name as well.")
4754 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004755 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004756 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004757 group.add_option(
4758 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4759 help="Host of buildbucket. The default host is %default.")
4760 parser.add_option_group(group)
4761 auth.add_auth_options(parser)
4762 options, args = parser.parse_args(args)
4763 if args:
4764 parser.error('Unrecognized args: %s' % ' '.join(args))
4765
4766 auth_config = auth.extract_auth_config_from_options(options)
4767 cl = Changelist(auth_config=auth_config)
4768 if not cl.GetIssue():
4769 parser.error('Need to upload first')
4770
4771 if not options.patchset:
4772 options.patchset = cl.GetMostRecentPatchset()
4773 if options.patchset and options.patchset != cl.GetPatchset():
4774 print(
4775 '\nWARNING Mismatch between local config and server. Did a previous '
4776 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4777 'Continuing using\npatchset %s.\n' % options.patchset)
4778 try:
4779 jobs = fetch_try_jobs(auth_config, cl, options)
4780 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004781 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004782 return 1
4783 except Exception as e:
4784 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004785 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004786 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004787 return 1
qyearsleyeab3c042016-08-24 09:18:28 -07004788 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004789 return 0
4790
4791
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004792@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004793def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004794 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004795 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004796 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004797 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004798
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004799 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004800 if args:
4801 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004802 branch = cl.GetBranch()
4803 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004804 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004805 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004806
4807 # Clear configured merge-base, if there is one.
4808 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004809 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004810 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004811 return 0
4812
4813
thestig@chromium.org00858c82013-12-02 23:08:03 +00004814def CMDweb(parser, args):
4815 """Opens the current CL in the web browser."""
4816 _, args = parser.parse_args(args)
4817 if args:
4818 parser.error('Unrecognized args: %s' % ' '.join(args))
4819
4820 issue_url = Changelist().GetIssueURL()
4821 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004822 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004823 return 1
4824
4825 webbrowser.open(issue_url)
4826 return 0
4827
4828
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004829def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004830 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004831 parser.add_option('-d', '--dry-run', action='store_true',
4832 help='trigger in dry run mode')
4833 parser.add_option('-c', '--clear', action='store_true',
4834 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004835 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004836 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004837 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004838 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004839 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004840 if args:
4841 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004842 if options.dry_run and options.clear:
4843 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4844
iannuccie53c9352016-08-17 14:40:40 -07004845 cl = Changelist(auth_config=auth_config, issue=options.issue,
4846 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004847 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004848 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004849 elif options.dry_run:
4850 state = _CQState.DRY_RUN
4851 else:
4852 state = _CQState.COMMIT
4853 if not cl.GetIssue():
4854 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004855 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004856 return 0
4857
4858
groby@chromium.org411034a2013-02-26 15:12:01 +00004859def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004860 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004861 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004862 auth.add_auth_options(parser)
4863 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004864 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004865 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004866 if args:
4867 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004868 cl = Changelist(auth_config=auth_config, issue=options.issue,
4869 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004870 # Ensure there actually is an issue to close.
4871 cl.GetDescription()
4872 cl.CloseIssue()
4873 return 0
4874
4875
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004876def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004877 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004878 auth.add_auth_options(parser)
4879 options, args = parser.parse_args(args)
4880 auth_config = auth.extract_auth_config_from_options(options)
4881 if args:
4882 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004883
4884 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004885 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004886 # Staged changes would be committed along with the patch from last
4887 # upload, hence counted toward the "last upload" side in the final
4888 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004889 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004890 return 1
4891
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004892 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004893 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004894 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004895 if not issue:
4896 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004897 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004898 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004899
4900 # Create a new branch based on the merge-base
4901 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004902 # Clear cached branch in cl object, to avoid overwriting original CL branch
4903 # properties.
4904 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004905 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004906 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004907 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004908 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004909 return rtn
4910
wychen@chromium.org06928532015-02-03 02:11:29 +00004911 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004912 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004913 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004914 finally:
4915 RunGit(['checkout', '-q', branch])
4916 RunGit(['branch', '-D', TMP_BRANCH])
4917
4918 return 0
4919
4920
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004921def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004922 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004923 parser.add_option(
4924 '--no-color',
4925 action='store_true',
4926 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004927 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004928 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004929 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004930
4931 author = RunGit(['config', 'user.email']).strip() or None
4932
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004933 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004934
4935 if args:
4936 if len(args) > 1:
4937 parser.error('Unknown args')
4938 base_branch = args[0]
4939 else:
4940 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004941 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004942
4943 change = cl.GetChange(base_branch, None)
4944 return owners_finder.OwnersFinder(
4945 [f.LocalPath() for f in
4946 cl.GetChange(base_branch, None).AffectedFiles()],
4947 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004948 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004949 disable_color=options.no_color).run()
4950
4951
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004952def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004953 """Generates a diff command."""
4954 # Generate diff for the current branch's changes.
4955 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4956 upstream_commit, '--' ]
4957
4958 if args:
4959 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004960 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004961 diff_cmd.append(arg)
4962 else:
4963 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004964
4965 return diff_cmd
4966
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004967def MatchingFileType(file_name, extensions):
4968 """Returns true if the file name ends with one of the given extensions."""
4969 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004970
enne@chromium.org555cfe42014-01-29 18:21:39 +00004971@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004972def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004973 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004974 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004975 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004976 parser.add_option('--full', action='store_true',
4977 help='Reformat the full content of all touched files')
4978 parser.add_option('--dry-run', action='store_true',
4979 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004980 parser.add_option('--python', action='store_true',
4981 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004982 parser.add_option('--diff', action='store_true',
4983 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004984 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004985
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004986 # git diff generates paths against the root of the repository. Change
4987 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004988 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004989 if rel_base_path:
4990 os.chdir(rel_base_path)
4991
digit@chromium.org29e47272013-05-17 17:01:46 +00004992 # Grab the merge-base commit, i.e. the upstream commit of the current
4993 # branch when it was created or the last time it was rebased. This is
4994 # to cover the case where the user may have called "git fetch origin",
4995 # moving the origin branch to a newer commit, but hasn't rebased yet.
4996 upstream_commit = None
4997 cl = Changelist()
4998 upstream_branch = cl.GetUpstreamBranch()
4999 if upstream_branch:
5000 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5001 upstream_commit = upstream_commit.strip()
5002
5003 if not upstream_commit:
5004 DieWithError('Could not find base commit for this branch. '
5005 'Are you in detached state?')
5006
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005007 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5008 diff_output = RunGit(changed_files_cmd)
5009 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005010 # Filter out files deleted by this CL
5011 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005012
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005013 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5014 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5015 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005016 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005017
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005018 top_dir = os.path.normpath(
5019 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5020
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005021 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5022 # formatted. This is used to block during the presubmit.
5023 return_value = 0
5024
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005025 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005026 # Locate the clang-format binary in the checkout
5027 try:
5028 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005029 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005030 DieWithError(e)
5031
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005032 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005033 cmd = [clang_format_tool]
5034 if not opts.dry_run and not opts.diff:
5035 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005036 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005037 if opts.diff:
5038 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005039 else:
5040 env = os.environ.copy()
5041 env['PATH'] = str(os.path.dirname(clang_format_tool))
5042 try:
5043 script = clang_format.FindClangFormatScriptInChromiumTree(
5044 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005045 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005046 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005047
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005048 cmd = [sys.executable, script, '-p0']
5049 if not opts.dry_run and not opts.diff:
5050 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005051
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005052 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5053 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005054
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005055 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5056 if opts.diff:
5057 sys.stdout.write(stdout)
5058 if opts.dry_run and len(stdout) > 0:
5059 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005060
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005061 # Similar code to above, but using yapf on .py files rather than clang-format
5062 # on C/C++ files
5063 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005064 yapf_tool = gclient_utils.FindExecutable('yapf')
5065 if yapf_tool is None:
5066 DieWithError('yapf not found in PATH')
5067
5068 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005069 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005070 cmd = [yapf_tool]
5071 if not opts.dry_run and not opts.diff:
5072 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005073 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005074 if opts.diff:
5075 sys.stdout.write(stdout)
5076 else:
5077 # TODO(sbc): yapf --lines mode still has some issues.
5078 # https://github.com/google/yapf/issues/154
5079 DieWithError('--python currently only works with --full')
5080
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005081 # Dart's formatter does not have the nice property of only operating on
5082 # modified chunks, so hard code full.
5083 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005084 try:
5085 command = [dart_format.FindDartFmtToolInChromiumTree()]
5086 if not opts.dry_run and not opts.diff:
5087 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005088 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005089
ppi@chromium.org6593d932016-03-03 15:41:15 +00005090 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005091 if opts.dry_run and stdout:
5092 return_value = 2
5093 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005094 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5095 'found in this checkout. Files in other languages are still '
5096 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005097
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005098 # Format GN build files. Always run on full build files for canonical form.
5099 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005100 cmd = ['gn', 'format' ]
5101 if opts.dry_run or opts.diff:
5102 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005103 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005104 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5105 shell=sys.platform == 'win32',
5106 cwd=top_dir)
5107 if opts.dry_run and gn_ret == 2:
5108 return_value = 2 # Not formatted.
5109 elif opts.diff and gn_ret == 2:
5110 # TODO this should compute and print the actual diff.
5111 print("This change has GN build file diff for " + gn_diff_file)
5112 elif gn_ret != 0:
5113 # For non-dry run cases (and non-2 return values for dry-run), a
5114 # nonzero error code indicates a failure, probably because the file
5115 # doesn't parse.
5116 DieWithError("gn format failed on " + gn_diff_file +
5117 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005118
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005119 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005120
5121
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005122@subcommand.usage('<codereview url or issue id>')
5123def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005124 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005125 _, args = parser.parse_args(args)
5126
5127 if len(args) != 1:
5128 parser.print_help()
5129 return 1
5130
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005131 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005132 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005133 parser.print_help()
5134 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005135 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005136
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005137 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005138 output = RunGit(['config', '--local', '--get-regexp',
5139 r'branch\..*\.%s' % issueprefix],
5140 error_ok=True)
5141 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005142 if issue == target_issue:
5143 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005144
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005145 branches = []
5146 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005147 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005148 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005149 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005150 return 1
5151 if len(branches) == 1:
5152 RunGit(['checkout', branches[0]])
5153 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005154 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005155 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005156 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005157 which = raw_input('Choose by index: ')
5158 try:
5159 RunGit(['checkout', branches[int(which)]])
5160 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005161 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005162 return 1
5163
5164 return 0
5165
5166
maruel@chromium.org29404b52014-09-08 22:58:00 +00005167def CMDlol(parser, args):
5168 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005169 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005170 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5171 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5172 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005173 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005174 return 0
5175
5176
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005177class OptionParser(optparse.OptionParser):
5178 """Creates the option parse and add --verbose support."""
5179 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005180 optparse.OptionParser.__init__(
5181 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005182 self.add_option(
5183 '-v', '--verbose', action='count', default=0,
5184 help='Use 2 times for more debugging info')
5185
5186 def parse_args(self, args=None, values=None):
5187 options, args = optparse.OptionParser.parse_args(self, args, values)
5188 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5189 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5190 return options, args
5191
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005193def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005194 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005195 print('\nYour python version %s is unsupported, please upgrade.\n' %
5196 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005197 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005198
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005199 # Reload settings.
5200 global settings
5201 settings = Settings()
5202
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005203 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005204 dispatcher = subcommand.CommandDispatcher(__name__)
5205 try:
5206 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005207 except auth.AuthenticationError as e:
5208 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005209 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005210 if e.code != 500:
5211 raise
5212 DieWithError(
5213 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5214 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005215 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005216
5217
5218if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005219 # These affect sys.stdout so do it outside of main() to simplify mocks in
5220 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005221 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005222 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005223 try:
5224 sys.exit(main(sys.argv[1:]))
5225 except KeyboardInterrupt:
5226 sys.stderr.write('interrupted\n')
5227 sys.exit(1)