blob: 2d9ced9689664dafe547ddd61bc5835e26b5d528 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 try:
tandrii5d48c322016-08-18 16:19:37 -0700122 (out, _), code = subprocess2.communicate(['git'] + args,
123 env=GetNoGitPagerEnv(),
124 stdout=subprocess2.PIPE,
125 stderr=stderr)
126 return code, out
127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
129 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
tandrii5d48c322016-08-18 16:19:37 -0700159def _git_branch_config_key(branch, key):
160 """Helper method to return Git config key for a branch."""
161 assert branch, 'branch name is required to set git config for it'
162 return 'branch.%s.%s' % (branch, key)
163
164
165def _git_get_branch_config_value(key, default=None, value_type=str,
166 branch=False):
167 """Returns git config value of given or current branch if any.
168
169 Returns default in all other cases.
170 """
171 assert value_type in (int, str, bool)
172 if branch is False: # Distinguishing default arg value from None.
173 branch = GetCurrentBranch()
174
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000175 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700176 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000177
tandrii5d48c322016-08-18 16:19:37 -0700178 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700179 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700180 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700181 # git config also has --int, but apparently git config suffers from integer
182 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700183 args.append(_git_branch_config_key(branch, key))
184 code, out = RunGitWithCode(args)
185 if code == 0:
186 value = out.strip()
187 if value_type == int:
188 return int(value)
189 if value_type == bool:
190 return bool(value.lower() == 'true')
191 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 return default
193
194
tandrii5d48c322016-08-18 16:19:37 -0700195def _git_set_branch_config_value(key, value, branch=None, **kwargs):
196 """Sets the value or unsets if it's None of a git branch config.
197
198 Valid, though not necessarily existing, branch must be provided,
199 otherwise currently checked out branch is used.
200 """
201 if not branch:
202 branch = GetCurrentBranch()
203 assert branch, 'a branch name OR currently checked out branch is required'
204 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700205 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700206 if value is None:
207 args.append('--unset')
208 elif isinstance(value, bool):
209 args.append('--bool')
210 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700211 else:
tandrii33a46ff2016-08-23 05:53:40 -0700212 # git config also has --int, but apparently git config suffers from integer
213 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700214 value = str(value)
215 args.append(_git_branch_config_key(branch, key))
216 if value is not None:
217 args.append(value)
218 RunGit(args, **kwargs)
219
220
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000221def add_git_similarity(parser):
222 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700223 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000224 help='Sets the percentage that a pair of files need to match in order to'
225 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000226 parser.add_option(
227 '--find-copies', action='store_true',
228 help='Allows git to look for copies.')
229 parser.add_option(
230 '--no-find-copies', action='store_false', dest='find_copies',
231 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000232
233 old_parser_args = parser.parse_args
234 def Parse(args):
235 options, args = old_parser_args(args)
236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000237 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700238 options.similarity = _git_get_branch_config_value(
239 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000240 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241 print('Note: Saving similarity of %d%% in git config.'
242 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700243 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 options.similarity = max(0, min(options.similarity, 100))
246
247 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.find_copies = _git_get_branch_config_value(
249 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 else:
tandrii5d48c322016-08-18 16:19:37 -0700251 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000252
253 print('Using %d%% similarity for rename/copy detection. '
254 'Override with --similarity.' % options.similarity)
255
256 return options, args
257 parser.parse_args = Parse
258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
261 properties = dict(x.split('=', 1) for x in options.properties)
262 for key, val in properties.iteritems():
263 try:
264 properties[key] = json.loads(val)
265 except ValueError:
266 pass # If a value couldn't be evaluated, treat it as a string.
267 return properties
268
269
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270def _prefix_master(master):
271 """Convert user-specified master name to full master name.
272
273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
274 name, while the developers always use shortened master name
275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
276 function does the conversion for buildbucket migration.
277 """
278 prefix = 'master.'
279 if master.startswith(prefix):
280 return master
281 return '%s%s' % (prefix, master)
282
283
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000284def _buildbucket_retry(operation_name, http, *args, **kwargs):
285 """Retries requests to buildbucket service and returns parsed json content."""
286 try_count = 0
287 while True:
288 response, content = http.request(*args, **kwargs)
289 try:
290 content_json = json.loads(content)
291 except ValueError:
292 content_json = None
293
294 # Buildbucket could return an error even if status==200.
295 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000296 error = content_json.get('error')
297 if error.get('code') == 403:
298 raise BuildbucketResponseException(
299 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000300 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000301 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000302 raise BuildbucketResponseException(msg)
303
304 if response.status == 200:
305 if not content_json:
306 raise BuildbucketResponseException(
307 'Buildbucket returns invalid json content: %s.\n'
308 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
309 content)
310 return content_json
311 if response.status < 500 or try_count >= 2:
312 raise httplib2.HttpLib2Error(content)
313
314 # status >= 500 means transient failures.
315 logging.debug('Transient errors when %s. Will retry.', operation_name)
316 time.sleep(0.5 + 1.5*try_count)
317 try_count += 1
318 assert False, 'unreachable'
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322 rietveld_url = settings.GetDefaultServerUrl()
323 rietveld_host = urlparse.urlparse(rietveld_url).hostname
324 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
325 http = authenticator.authorize(httplib2.Http())
326 http.force_exception_to_status_code = True
327 issue_props = changelist.GetIssueProperties()
328 issue = changelist.GetIssue()
329 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000330 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332 buildbucket_put_url = (
333 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000334 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
336 hostname=rietveld_host,
337 issue=issue,
338 patch=patchset)
339
340 batch_req_body = {'builds': []}
341 print_text = []
342 print_text.append('Tried jobs on:')
343 for master, builders_and_tests in sorted(masters.iteritems()):
344 print_text.append('Master: %s' % master)
345 bucket = _prefix_master(master)
346 for builder, tests in sorted(builders_and_tests.iteritems()):
347 print_text.append(' %s: %s' % (builder, tests))
348 parameters = {
349 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000350 'changes': [{
351 'author': {'email': issue_props['owner_email']},
352 'revision': options.revision,
353 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'properties': {
355 'category': category,
356 'issue': issue,
357 'master': master,
358 'patch_project': issue_props['project'],
359 'patch_storage': 'rietveld',
360 'patchset': patchset,
361 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 },
364 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000365 if 'presubmit' in builder.lower():
366 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000367 if tests:
368 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000369 if properties:
370 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 if options.clobber:
372 parameters['properties']['clobber'] = True
373 batch_req_body['builds'].append(
374 {
375 'bucket': bucket,
376 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000378 'tags': ['builder:%s' % builder,
379 'buildset:%s' % buildset,
380 'master:%s' % master,
381 'user_agent:git_cl_try']
382 }
383 )
384
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700386 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 http,
388 buildbucket_put_url,
389 'PUT',
390 body=json.dumps(batch_req_body),
391 headers={'Content-Type': 'application/json'}
392 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000393 print_text.append('To see results here, run: git cl try-results')
394 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700395 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000398def fetch_try_jobs(auth_config, changelist, options):
qyearsleyeab3c042016-08-24 09:18:28 -0700399 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400
qyearsley53f48a12016-09-01 10:45:13 -0700401 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000402 """
403 rietveld_url = settings.GetDefaultServerUrl()
404 rietveld_host = urlparse.urlparse(rietveld_url).hostname
405 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
406 if authenticator.has_cached_credentials():
407 http = authenticator.authorize(httplib2.Http())
408 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700409 print('Warning: Some results might be missing because %s' %
410 # Get the message on how to login.
411 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000412 http = httplib2.Http()
413
414 http.force_exception_to_status_code = True
415
416 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
417 hostname=rietveld_host,
418 issue=changelist.GetIssue(),
419 patch=options.patchset)
420 params = {'tag': 'buildset:%s' % buildset}
421
422 builds = {}
423 while True:
424 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
425 hostname=options.buildbucket_host,
426 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700427 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000428 for build in content.get('builds', []):
429 builds[build['id']] = build
430 if 'next_cursor' in content:
431 params['start_cursor'] = content['next_cursor']
432 else:
433 break
434 return builds
435
436
qyearsleyeab3c042016-08-24 09:18:28 -0700437def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000438 """Prints nicely result of fetch_try_jobs."""
439 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700440 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000441 return
442
443 # Make a copy, because we'll be modifying builds dictionary.
444 builds = builds.copy()
445 builder_names_cache = {}
446
447 def get_builder(b):
448 try:
449 return builder_names_cache[b['id']]
450 except KeyError:
451 try:
452 parameters = json.loads(b['parameters_json'])
453 name = parameters['builder_name']
454 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700455 print('WARNING: failed to get builder name for build %s: %s' % (
456 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 name = None
458 builder_names_cache[b['id']] = name
459 return name
460
461 def get_bucket(b):
462 bucket = b['bucket']
463 if bucket.startswith('master.'):
464 return bucket[len('master.'):]
465 return bucket
466
467 if options.print_master:
468 name_fmt = '%%-%ds %%-%ds' % (
469 max(len(str(get_bucket(b))) for b in builds.itervalues()),
470 max(len(str(get_builder(b))) for b in builds.itervalues()))
471 def get_name(b):
472 return name_fmt % (get_bucket(b), get_builder(b))
473 else:
474 name_fmt = '%%-%ds' % (
475 max(len(str(get_builder(b))) for b in builds.itervalues()))
476 def get_name(b):
477 return name_fmt % get_builder(b)
478
479 def sort_key(b):
480 return b['status'], b.get('result'), get_name(b), b.get('url')
481
482 def pop(title, f, color=None, **kwargs):
483 """Pop matching builds from `builds` dict and print them."""
484
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000485 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486 colorize = str
487 else:
488 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
489
490 result = []
491 for b in builds.values():
492 if all(b.get(k) == v for k, v in kwargs.iteritems()):
493 builds.pop(b['id'])
494 result.append(b)
495 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700496 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000497 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700498 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000499
500 total = len(builds)
501 pop(status='COMPLETED', result='SUCCESS',
502 title='Successes:', color=Fore.GREEN,
503 f=lambda b: (get_name(b), b.get('url')))
504 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
505 title='Infra Failures:', color=Fore.MAGENTA,
506 f=lambda b: (get_name(b), b.get('url')))
507 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
508 title='Failures:', color=Fore.RED,
509 f=lambda b: (get_name(b), b.get('url')))
510 pop(status='COMPLETED', result='CANCELED',
511 title='Canceled:', color=Fore.MAGENTA,
512 f=lambda b: (get_name(b),))
513 pop(status='COMPLETED', result='FAILURE',
514 failure_reason='INVALID_BUILD_DEFINITION',
515 title='Wrong master/builder name:', color=Fore.MAGENTA,
516 f=lambda b: (get_name(b),))
517 pop(status='COMPLETED', result='FAILURE',
518 title='Other failures:',
519 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
520 pop(status='COMPLETED',
521 title='Other finished:',
522 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
523 pop(status='STARTED',
524 title='Started:', color=Fore.YELLOW,
525 f=lambda b: (get_name(b), b.get('url')))
526 pop(status='SCHEDULED',
527 title='Scheduled:',
528 f=lambda b: (get_name(b), 'id=%s' % b['id']))
529 # The last section is just in case buildbucket API changes OR there is a bug.
530 pop(title='Other:',
531 f=lambda b: (get_name(b), 'id=%s' % b['id']))
532 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700533 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
535
qyearsley53f48a12016-09-01 10:45:13 -0700536def write_try_results_json(output_file, builds):
537 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
538
539 The input |builds| dict is assumed to be generated by Buildbucket.
540 Buildbucket documentation: http://goo.gl/G0s101
541 """
542
543 def convert_build_dict(build):
544 return {
545 'buildbucket_id': build.get('id'),
546 'status': build.get('status'),
547 'result': build.get('result'),
548 'bucket': build.get('bucket'),
549 'builder_name': json.loads(
550 build.get('parameters_json', '{}')).get('builder_name'),
551 'failure_reason': build.get('failure_reason'),
552 'url': build.get('url'),
553 }
554
555 converted = []
556 for _, build in sorted(builds.items()):
557 converted.append(convert_build_dict(build))
558 write_json(output_file, converted)
559
560
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000561def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
562 """Return the corresponding git ref if |base_url| together with |glob_spec|
563 matches the full |url|.
564
565 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
566 """
567 fetch_suburl, as_ref = glob_spec.split(':')
568 if allow_wildcards:
569 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
570 if glob_match:
571 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
572 # "branches/{472,597,648}/src:refs/remotes/svn/*".
573 branch_re = re.escape(base_url)
574 if glob_match.group(1):
575 branch_re += '/' + re.escape(glob_match.group(1))
576 wildcard = glob_match.group(2)
577 if wildcard == '*':
578 branch_re += '([^/]*)'
579 else:
580 # Escape and replace surrounding braces with parentheses and commas
581 # with pipe symbols.
582 wildcard = re.escape(wildcard)
583 wildcard = re.sub('^\\\\{', '(', wildcard)
584 wildcard = re.sub('\\\\,', '|', wildcard)
585 wildcard = re.sub('\\\\}$', ')', wildcard)
586 branch_re += wildcard
587 if glob_match.group(3):
588 branch_re += re.escape(glob_match.group(3))
589 match = re.match(branch_re, url)
590 if match:
591 return re.sub('\*$', match.group(1), as_ref)
592
593 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
594 if fetch_suburl:
595 full_url = base_url + '/' + fetch_suburl
596 else:
597 full_url = base_url
598 if full_url == url:
599 return as_ref
600 return None
601
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000602
iannucci@chromium.org79540052012-10-19 23:15:26 +0000603def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000604 """Prints statistics about the change to the user."""
605 # --no-ext-diff is broken in some versions of Git, so try to work around
606 # this by overriding the environment (but there is still a problem if the
607 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000608 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000609 if 'GIT_EXTERNAL_DIFF' in env:
610 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000611
612 if find_copies:
613 similarity_options = ['--find-copies-harder', '-l100000',
614 '-C%s' % similarity]
615 else:
616 similarity_options = ['-M%s' % similarity]
617
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000618 try:
619 stdout = sys.stdout.fileno()
620 except AttributeError:
621 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000622 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000623 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000624 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000625 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000626
627
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000628class BuildbucketResponseException(Exception):
629 pass
630
631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632class Settings(object):
633 def __init__(self):
634 self.default_server = None
635 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000636 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 self.is_git_svn = None
638 self.svn_branch = None
639 self.tree_status_url = None
640 self.viewvc_url = None
641 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000642 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000643 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000644 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000645 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000646 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000647 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000648 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649
650 def LazyUpdateIfNeeded(self):
651 """Updates the settings from a codereview.settings file, if available."""
652 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000653 # The only value that actually changes the behavior is
654 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000655 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000656 error_ok=True
657 ).strip().lower()
658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000660 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661 LoadCodereviewSettingsFromFile(cr_settings_file)
662 self.updated = True
663
664 def GetDefaultServerUrl(self, error_ok=False):
665 if not self.default_server:
666 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000667 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000668 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 if error_ok:
670 return self.default_server
671 if not self.default_server:
672 error_message = ('Could not find settings file. You must configure '
673 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000674 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 return self.default_server
677
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000678 @staticmethod
679 def GetRelativeRoot():
680 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000683 if self.root is None:
684 self.root = os.path.abspath(self.GetRelativeRoot())
685 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000687 def GetGitMirror(self, remote='origin'):
688 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000689 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000690 if not os.path.isdir(local_url):
691 return None
692 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
693 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
694 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
695 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
696 if mirror.exists():
697 return mirror
698 return None
699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700 def GetIsGitSvn(self):
701 """Return true if this repo looks like it's using git-svn."""
702 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000703 if self.GetPendingRefPrefix():
704 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
705 self.is_git_svn = False
706 else:
707 # If you have any "svn-remote.*" config keys, we think you're using svn.
708 self.is_git_svn = RunGitWithCode(
709 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 return self.is_git_svn
711
712 def GetSVNBranch(self):
713 if self.svn_branch is None:
714 if not self.GetIsGitSvn():
715 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
716
717 # Try to figure out which remote branch we're based on.
718 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000719 # 1) iterate through our branch history and find the svn URL.
720 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 # regexp matching the git-svn line that contains the URL.
723 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
724
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000725 # We don't want to go through all of history, so read a line from the
726 # pipe at a time.
727 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000728 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
730 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000731 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000732 for line in proc.stdout:
733 match = git_svn_re.match(line)
734 if match:
735 url = match.group(1)
736 proc.stdout.close() # Cut pipe.
737 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000739 if url:
740 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
741 remotes = RunGit(['config', '--get-regexp',
742 r'^svn-remote\..*\.url']).splitlines()
743 for remote in remotes:
744 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000746 remote = match.group(1)
747 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000748 rewrite_root = RunGit(
749 ['config', 'svn-remote.%s.rewriteRoot' % remote],
750 error_ok=True).strip()
751 if rewrite_root:
752 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000753 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000754 ['config', 'svn-remote.%s.fetch' % remote],
755 error_ok=True).strip()
756 if fetch_spec:
757 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
758 if self.svn_branch:
759 break
760 branch_spec = RunGit(
761 ['config', 'svn-remote.%s.branches' % remote],
762 error_ok=True).strip()
763 if branch_spec:
764 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
765 if self.svn_branch:
766 break
767 tag_spec = RunGit(
768 ['config', 'svn-remote.%s.tags' % remote],
769 error_ok=True).strip()
770 if tag_spec:
771 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
772 if self.svn_branch:
773 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 if not self.svn_branch:
776 DieWithError('Can\'t guess svn branch -- try specifying it on the '
777 'command line')
778
779 return self.svn_branch
780
781 def GetTreeStatusUrl(self, error_ok=False):
782 if not self.tree_status_url:
783 error_message = ('You must configure your tree status URL by running '
784 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000785 self.tree_status_url = self._GetRietveldConfig(
786 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 return self.tree_status_url
788
789 def GetViewVCUrl(self):
790 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 return self.viewvc_url
793
rmistry@google.com90752582014-01-14 21:04:50 +0000794 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000796
rmistry@google.com78948ed2015-07-08 23:09:57 +0000797 def GetIsSkipDependencyUpload(self, branch_name):
798 """Returns true if specified branch should skip dep uploads."""
799 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
800 error_ok=True)
801
rmistry@google.com5626a922015-02-26 14:03:30 +0000802 def GetRunPostUploadHook(self):
803 run_post_upload_hook = self._GetRietveldConfig(
804 'run-post-upload-hook', error_ok=True)
805 return run_post_upload_hook == "True"
806
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000807 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000809
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000810 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000812
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 def GetIsGerrit(self):
814 """Return true if this repo is assosiated with gerrit code review system."""
815 if self.is_gerrit is None:
816 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
817 return self.is_gerrit
818
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000819 def GetSquashGerritUploads(self):
820 """Return true if uploads to Gerrit should be squashed by default."""
821 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700822 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
823 if self.squash_gerrit_uploads is None:
824 # Default is squash now (http://crbug.com/611892#c23).
825 self.squash_gerrit_uploads = not (
826 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
827 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000828 return self.squash_gerrit_uploads
829
tandriia60502f2016-06-20 02:01:53 -0700830 def GetSquashGerritUploadsOverride(self):
831 """Return True or False if codereview.settings should be overridden.
832
833 Returns None if no override has been defined.
834 """
835 # See also http://crbug.com/611892#c23
836 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
837 error_ok=True).strip()
838 if result == 'true':
839 return True
840 if result == 'false':
841 return False
842 return None
843
tandrii@chromium.org28253532016-04-14 13:46:56 +0000844 def GetGerritSkipEnsureAuthenticated(self):
845 """Return True if EnsureAuthenticated should not be done for Gerrit
846 uploads."""
847 if self.gerrit_skip_ensure_authenticated is None:
848 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000849 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000850 error_ok=True).strip() == 'true')
851 return self.gerrit_skip_ensure_authenticated
852
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000853 def GetGitEditor(self):
854 """Return the editor specified in the git config, or None if none is."""
855 if self.git_editor is None:
856 self.git_editor = self._GetConfig('core.editor', error_ok=True)
857 return self.git_editor or None
858
thestig@chromium.org44202a22014-03-11 19:22:18 +0000859 def GetLintRegex(self):
860 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
861 DEFAULT_LINT_REGEX)
862
863 def GetLintIgnoreRegex(self):
864 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
865 DEFAULT_LINT_IGNORE_REGEX)
866
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000867 def GetProject(self):
868 if not self.project:
869 self.project = self._GetRietveldConfig('project', error_ok=True)
870 return self.project
871
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000872 def GetForceHttpsCommitUrl(self):
873 if not self.force_https_commit_url:
874 self.force_https_commit_url = self._GetRietveldConfig(
875 'force-https-commit-url', error_ok=True)
876 return self.force_https_commit_url
877
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000878 def GetPendingRefPrefix(self):
879 if not self.pending_ref_prefix:
880 self.pending_ref_prefix = self._GetRietveldConfig(
881 'pending-ref-prefix', error_ok=True)
882 return self.pending_ref_prefix
883
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000884 def _GetRietveldConfig(self, param, **kwargs):
885 return self._GetConfig('rietveld.' + param, **kwargs)
886
rmistry@google.com78948ed2015-07-08 23:09:57 +0000887 def _GetBranchConfig(self, branch_name, param, **kwargs):
888 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 def _GetConfig(self, param, **kwargs):
891 self.LazyUpdateIfNeeded()
892 return RunGit(['config', param], **kwargs).strip()
893
894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895def ShortBranchName(branch):
896 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000897 return branch.replace('refs/heads/', '', 1)
898
899
900def GetCurrentBranchRef():
901 """Returns branch ref (e.g., refs/heads/master) or None."""
902 return RunGit(['symbolic-ref', 'HEAD'],
903 stderr=subprocess2.VOID, error_ok=True).strip() or None
904
905
906def GetCurrentBranch():
907 """Returns current branch or None.
908
909 For refs/heads/* branches, returns just last part. For others, full ref.
910 """
911 branchref = GetCurrentBranchRef()
912 if branchref:
913 return ShortBranchName(branchref)
914 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915
916
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000917class _CQState(object):
918 """Enum for states of CL with respect to Commit Queue."""
919 NONE = 'none'
920 DRY_RUN = 'dry_run'
921 COMMIT = 'commit'
922
923 ALL_STATES = [NONE, DRY_RUN, COMMIT]
924
925
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000926class _ParsedIssueNumberArgument(object):
927 def __init__(self, issue=None, patchset=None, hostname=None):
928 self.issue = issue
929 self.patchset = patchset
930 self.hostname = hostname
931
932 @property
933 def valid(self):
934 return self.issue is not None
935
936
937class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
938 def __init__(self, *args, **kwargs):
939 self.patch_url = kwargs.pop('patch_url', None)
940 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
941
942
943def ParseIssueNumberArgument(arg):
944 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
945 fail_result = _ParsedIssueNumberArgument()
946
947 if arg.isdigit():
948 return _ParsedIssueNumberArgument(issue=int(arg))
949 if not arg.startswith('http'):
950 return fail_result
951 url = gclient_utils.UpgradeToHttps(arg)
952 try:
953 parsed_url = urlparse.urlparse(url)
954 except ValueError:
955 return fail_result
956 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
957 tmp = cls.ParseIssueURL(parsed_url)
958 if tmp is not None:
959 return tmp
960 return fail_result
961
962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 """Changelist works with one changelist in local branch.
965
966 Supports two codereview backends: Rietveld or Gerrit, selected at object
967 creation.
968
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000969 Notes:
970 * Not safe for concurrent multi-{thread,process} use.
971 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700972 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000973 """
974
975 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
976 """Create a new ChangeList instance.
977
978 If issue is given, the codereview must be given too.
979
980 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
981 Otherwise, it's decided based on current configuration of the local branch,
982 with default being 'rietveld' for backwards compatibility.
983 See _load_codereview_impl for more details.
984
985 **kwargs will be passed directly to codereview implementation.
986 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000988 global settings
989 if not settings:
990 # Happens when git_cl.py is used as a utility library.
991 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000992
993 if issue:
994 assert codereview, 'codereview must be known, if issue is known'
995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 self.branchref = branchref
997 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000998 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 self.branch = ShortBranchName(self.branchref)
1000 else:
1001 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001003 self.lookedup_issue = False
1004 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 self.has_description = False
1006 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001007 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001009 self.cc = None
1010 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001011 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001012
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001013 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001014 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001015 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001016 assert self._codereview_impl
1017 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001018
1019 def _load_codereview_impl(self, codereview=None, **kwargs):
1020 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001021 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1022 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1023 self._codereview = codereview
1024 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001025 return
1026
1027 # Automatic selection based on issue number set for a current branch.
1028 # Rietveld takes precedence over Gerrit.
1029 assert not self.issue
1030 # Whether we find issue or not, we are doing the lookup.
1031 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001032 if self.GetBranch():
1033 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1034 issue = _git_get_branch_config_value(
1035 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1036 if issue:
1037 self._codereview = codereview
1038 self._codereview_impl = cls(self, **kwargs)
1039 self.issue = int(issue)
1040 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001041
1042 # No issue is set for this branch, so decide based on repo-wide settings.
1043 return self._load_codereview_impl(
1044 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1045 **kwargs)
1046
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001047 def IsGerrit(self):
1048 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001049
1050 def GetCCList(self):
1051 """Return the users cc'd on this CL.
1052
agable92bec4f2016-08-24 09:27:27 -07001053 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001054 """
1055 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001056 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001057 more_cc = ','.join(self.watchers)
1058 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1059 return self.cc
1060
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001061 def GetCCListWithoutDefault(self):
1062 """Return the users cc'd on this CL excluding default ones."""
1063 if self.cc is None:
1064 self.cc = ','.join(self.watchers)
1065 return self.cc
1066
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001067 def SetWatchers(self, watchers):
1068 """Set the list of email addresses that should be cc'd based on the changed
1069 files in this CL.
1070 """
1071 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
1073 def GetBranch(self):
1074 """Returns the short branch name, e.g. 'master'."""
1075 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001077 if not branchref:
1078 return None
1079 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 self.branch = ShortBranchName(self.branchref)
1081 return self.branch
1082
1083 def GetBranchRef(self):
1084 """Returns the full branch name, e.g. 'refs/heads/master'."""
1085 self.GetBranch() # Poke the lazy loader.
1086 return self.branchref
1087
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001088 def ClearBranch(self):
1089 """Clears cached branch data of this object."""
1090 self.branch = self.branchref = None
1091
tandrii5d48c322016-08-18 16:19:37 -07001092 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1093 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1094 kwargs['branch'] = self.GetBranch()
1095 return _git_get_branch_config_value(key, default, **kwargs)
1096
1097 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1098 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1099 assert self.GetBranch(), (
1100 'this CL must have an associated branch to %sset %s%s' %
1101 ('un' if value is None else '',
1102 key,
1103 '' if value is None else ' to %r' % value))
1104 kwargs['branch'] = self.GetBranch()
1105 return _git_set_branch_config_value(key, value, **kwargs)
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 @staticmethod
1108 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001109 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 e.g. 'origin', 'refs/heads/master'
1111 """
1112 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001113 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001116 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001118 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1119 error_ok=True).strip()
1120 if upstream_branch:
1121 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001123 # Fall back on trying a git-svn upstream branch.
1124 if settings.GetIsGitSvn():
1125 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001127 # Else, try to guess the origin remote.
1128 remote_branches = RunGit(['branch', '-r']).split()
1129 if 'origin/master' in remote_branches:
1130 # Fall back on origin/master if it exits.
1131 remote = 'origin'
1132 upstream_branch = 'refs/heads/master'
1133 elif 'origin/trunk' in remote_branches:
1134 # Fall back on origin/trunk if it exists. Generally a shared
1135 # git-svn clone
1136 remote = 'origin'
1137 upstream_branch = 'refs/heads/trunk'
1138 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139 DieWithError(
1140 'Unable to determine default branch to diff against.\n'
1141 'Either pass complete "git diff"-style arguments, like\n'
1142 ' git cl upload origin/master\n'
1143 'or verify this branch is set up to track another \n'
1144 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 return remote, upstream_branch
1147
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001148 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001149 upstream_branch = self.GetUpstreamBranch()
1150 if not BranchExists(upstream_branch):
1151 DieWithError('The upstream for the current branch (%s) does not exist '
1152 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001153 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001154 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetUpstreamBranch(self):
1157 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001160 upstream_branch = upstream_branch.replace('refs/heads/',
1161 'refs/remotes/%s/' % remote)
1162 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1163 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 self.upstream_branch = upstream_branch
1165 return self.upstream_branch
1166
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001168 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 remote, branch = None, self.GetBranch()
1170 seen_branches = set()
1171 while branch not in seen_branches:
1172 seen_branches.add(branch)
1173 remote, branch = self.FetchUpstreamTuple(branch)
1174 branch = ShortBranchName(branch)
1175 if remote != '.' or branch.startswith('refs/remotes'):
1176 break
1177 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001178 remotes = RunGit(['remote'], error_ok=True).split()
1179 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001180 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001181 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001182 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001183 logging.warning('Could not determine which remote this change is '
1184 'associated with, so defaulting to "%s". This may '
1185 'not be what you want. You may prevent this message '
1186 'by running "git svn info" as documented here: %s',
1187 self._remote,
1188 GIT_INSTRUCTIONS_URL)
1189 else:
1190 logging.warn('Could not determine which remote this change is '
1191 'associated with. You may prevent this message by '
1192 'running "git svn info" as documented here: %s',
1193 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001194 branch = 'HEAD'
1195 if branch.startswith('refs/remotes'):
1196 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001197 elif branch.startswith('refs/branch-heads/'):
1198 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001199 else:
1200 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001201 return self._remote
1202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 def GitSanityChecks(self, upstream_git_obj):
1204 """Checks git repo status and ensures diff is from local commits."""
1205
sbc@chromium.org79706062015-01-14 21:18:12 +00001206 if upstream_git_obj is None:
1207 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001208 print('ERROR: unable to determine current branch (detached HEAD?)',
1209 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001210 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001211 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001212 return False
1213
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001214 # Verify the commit we're diffing against is in our current branch.
1215 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1216 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1217 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001218 print('ERROR: %s is not in the current branch. You may need to rebase '
1219 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 return False
1221
1222 # List the commits inside the diff, and verify they are all local.
1223 commits_in_diff = RunGit(
1224 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1225 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1226 remote_branch = remote_branch.strip()
1227 if code != 0:
1228 _, remote_branch = self.GetRemoteBranch()
1229
1230 commits_in_remote = RunGit(
1231 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1232
1233 common_commits = set(commits_in_diff) & set(commits_in_remote)
1234 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001235 print('ERROR: Your diff contains %d commits already in %s.\n'
1236 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1237 'the diff. If you are using a custom git flow, you can override'
1238 ' the reference used for this check with "git config '
1239 'gitcl.remotebranch <git-ref>".' % (
1240 len(common_commits), remote_branch, upstream_git_obj),
1241 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 return False
1243 return True
1244
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001245 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001246 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001247
1248 Returns None if it is not set.
1249 """
tandrii5d48c322016-08-18 16:19:37 -07001250 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001251
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001252 def GetGitSvnRemoteUrl(self):
1253 """Return the configured git-svn remote URL parsed from git svn info.
1254
1255 Returns None if it is not set.
1256 """
1257 # URL is dependent on the current directory.
1258 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1259 if data:
1260 keys = dict(line.split(': ', 1) for line in data.splitlines()
1261 if ': ' in line)
1262 return keys.get('URL', None)
1263 return None
1264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 def GetRemoteUrl(self):
1266 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1267
1268 Returns None if there is no remote.
1269 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001271 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1272
1273 # If URL is pointing to a local directory, it is probably a git cache.
1274 if os.path.isdir(url):
1275 url = RunGit(['config', 'remote.%s.url' % remote],
1276 error_ok=True,
1277 cwd=url).strip()
1278 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001280 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001281 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001282 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001283 self.issue = self._GitGetBranchConfigValue(
1284 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001285 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 return self.issue
1287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetIssueURL(self):
1289 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001290 issue = self.GetIssue()
1291 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001292 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001293 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
1295 def GetDescription(self, pretty=False):
1296 if not self.has_description:
1297 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 self.has_description = True
1300 if pretty:
1301 wrapper = textwrap.TextWrapper()
1302 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1303 return wrapper.fill(self.description)
1304 return self.description
1305
1306 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001307 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001308 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001309 self.patchset = self._GitGetBranchConfigValue(
1310 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001311 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 return self.patchset
1313
1314 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001315 """Set this branch's patchset. If patchset=0, clears the patchset."""
1316 assert self.GetBranch()
1317 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001318 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001319 else:
1320 self.patchset = int(patchset)
1321 self._GitSetBranchConfigValue(
1322 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001324 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001325 """Set this branch's issue. If issue isn't given, clears the issue."""
1326 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001328 issue = int(issue)
1329 self._GitSetBranchConfigValue(
1330 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001331 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001332 codereview_server = self._codereview_impl.GetCodereviewServer()
1333 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001334 self._GitSetBranchConfigValue(
1335 self._codereview_impl.CodereviewServerConfigKey(),
1336 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 else:
tandrii5d48c322016-08-18 16:19:37 -07001338 # Reset all of these just to be clean.
1339 reset_suffixes = [
1340 'last-upload-hash',
1341 self._codereview_impl.IssueConfigKey(),
1342 self._codereview_impl.PatchsetConfigKey(),
1343 self._codereview_impl.CodereviewServerConfigKey(),
1344 ] + self._PostUnsetIssueProperties()
1345 for prop in reset_suffixes:
1346 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001348 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
dnjba1b0f32016-09-02 12:37:42 -07001350 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 if not self.GitSanityChecks(upstream_branch):
1352 DieWithError('\nGit sanity check failure')
1353
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001354 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001355 if not root:
1356 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001357 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001358
1359 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001360 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001361 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001362 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001363 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001364 except subprocess2.CalledProcessError:
1365 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001366 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001367 'This branch probably doesn\'t exist anymore. To reset the\n'
1368 'tracking branch, please run\n'
1369 ' git branch --set-upstream %s trunk\n'
1370 'replacing trunk with origin/master or the relevant branch') %
1371 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001372
maruel@chromium.org52424302012-08-29 15:14:30 +00001373 issue = self.GetIssue()
1374 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001375 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001376 description = self.GetDescription()
1377 else:
1378 # If the change was never uploaded, use the log messages of all commits
1379 # up to the branch point, as git cl upload will prefill the description
1380 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001381 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1382 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001383
1384 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001385 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001386 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001387 name,
1388 description,
1389 absroot,
1390 files,
1391 issue,
1392 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001393 author,
1394 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001395
dsansomee2d6fd92016-09-08 00:10:47 -07001396 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001398 return self._codereview_impl.UpdateDescriptionRemote(
1399 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001400
1401 def RunHook(self, committing, may_prompt, verbose, change):
1402 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1403 try:
1404 return presubmit_support.DoPresubmitChecks(change, committing,
1405 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1406 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001407 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1408 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001409 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 DieWithError(
1411 ('%s\nMaybe your depot_tools is out of date?\n'
1412 'If all fails, contact maruel@') % e)
1413
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001414 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1415 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001416 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1417 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001418 else:
1419 # Assume url.
1420 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1421 urlparse.urlparse(issue_arg))
1422 if not parsed_issue_arg or not parsed_issue_arg.valid:
1423 DieWithError('Failed to parse issue argument "%s". '
1424 'Must be an issue number or a valid URL.' % issue_arg)
1425 return self._codereview_impl.CMDPatchWithParsedIssue(
1426 parsed_issue_arg, reject, nocommit, directory)
1427
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001428 def CMDUpload(self, options, git_diff_args, orig_args):
1429 """Uploads a change to codereview."""
1430 if git_diff_args:
1431 # TODO(ukai): is it ok for gerrit case?
1432 base_branch = git_diff_args[0]
1433 else:
1434 if self.GetBranch() is None:
1435 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1436
1437 # Default to diffing against common ancestor of upstream branch
1438 base_branch = self.GetCommonAncestorWithUpstream()
1439 git_diff_args = [base_branch, 'HEAD']
1440
1441 # Make sure authenticated to codereview before running potentially expensive
1442 # hooks. It is a fast, best efforts check. Codereview still can reject the
1443 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001444 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001445
1446 # Apply watchlists on upload.
1447 change = self.GetChange(base_branch, None)
1448 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1449 files = [f.LocalPath() for f in change.AffectedFiles()]
1450 if not options.bypass_watchlists:
1451 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1452
1453 if not options.bypass_hooks:
1454 if options.reviewers or options.tbr_owners:
1455 # Set the reviewer list now so that presubmit checks can access it.
1456 change_description = ChangeDescription(change.FullDescriptionText())
1457 change_description.update_reviewers(options.reviewers,
1458 options.tbr_owners,
1459 change)
1460 change.SetDescriptionText(change_description.description)
1461 hook_results = self.RunHook(committing=False,
1462 may_prompt=not options.force,
1463 verbose=options.verbose,
1464 change=change)
1465 if not hook_results.should_continue():
1466 return 1
1467 if not options.reviewers and hook_results.reviewers:
1468 options.reviewers = hook_results.reviewers.split(',')
1469
1470 if self.GetIssue():
1471 latest_patchset = self.GetMostRecentPatchset()
1472 local_patchset = self.GetPatchset()
1473 if (latest_patchset and local_patchset and
1474 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001475 print('The last upload made from this repository was patchset #%d but '
1476 'the most recent patchset on the server is #%d.'
1477 % (local_patchset, latest_patchset))
1478 print('Uploading will still work, but if you\'ve uploaded to this '
1479 'issue from another machine or branch the patch you\'re '
1480 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001481 ask_for_data('About to upload; enter to confirm.')
1482
1483 print_stats(options.similarity, options.find_copies, git_diff_args)
1484 ret = self.CMDUploadChange(options, git_diff_args, change)
1485 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001486 if options.use_commit_queue:
1487 self.SetCQState(_CQState.COMMIT)
1488 elif options.cq_dry_run:
1489 self.SetCQState(_CQState.DRY_RUN)
1490
tandrii5d48c322016-08-18 16:19:37 -07001491 _git_set_branch_config_value('last-upload-hash',
1492 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001493 # Run post upload hooks, if specified.
1494 if settings.GetRunPostUploadHook():
1495 presubmit_support.DoPostUploadExecuter(
1496 change,
1497 self,
1498 settings.GetRoot(),
1499 options.verbose,
1500 sys.stdout)
1501
1502 # Upload all dependencies if specified.
1503 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001504 print()
1505 print('--dependencies has been specified.')
1506 print('All dependent local branches will be re-uploaded.')
1507 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001508 # Remove the dependencies flag from args so that we do not end up in a
1509 # loop.
1510 orig_args.remove('--dependencies')
1511 ret = upload_branch_deps(self, orig_args)
1512 return ret
1513
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001514 def SetCQState(self, new_state):
1515 """Update the CQ state for latest patchset.
1516
1517 Issue must have been already uploaded and known.
1518 """
1519 assert new_state in _CQState.ALL_STATES
1520 assert self.GetIssue()
1521 return self._codereview_impl.SetCQState(new_state)
1522
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523 # Forward methods to codereview specific implementation.
1524
1525 def CloseIssue(self):
1526 return self._codereview_impl.CloseIssue()
1527
1528 def GetStatus(self):
1529 return self._codereview_impl.GetStatus()
1530
1531 def GetCodereviewServer(self):
1532 return self._codereview_impl.GetCodereviewServer()
1533
1534 def GetApprovingReviewers(self):
1535 return self._codereview_impl.GetApprovingReviewers()
1536
1537 def GetMostRecentPatchset(self):
1538 return self._codereview_impl.GetMostRecentPatchset()
1539
1540 def __getattr__(self, attr):
1541 # This is because lots of untested code accesses Rietveld-specific stuff
1542 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001543 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001544 # Note that child method defines __getattr__ as well, and forwards it here,
1545 # because _RietveldChangelistImpl is not cleaned up yet, and given
1546 # deprecation of Rietveld, it should probably be just removed.
1547 # Until that time, avoid infinite recursion by bypassing __getattr__
1548 # of implementation class.
1549 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001550
1551
1552class _ChangelistCodereviewBase(object):
1553 """Abstract base class encapsulating codereview specifics of a changelist."""
1554 def __init__(self, changelist):
1555 self._changelist = changelist # instance of Changelist
1556
1557 def __getattr__(self, attr):
1558 # Forward methods to changelist.
1559 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1560 # _RietveldChangelistImpl to avoid this hack?
1561 return getattr(self._changelist, attr)
1562
1563 def GetStatus(self):
1564 """Apply a rough heuristic to give a simple summary of an issue's review
1565 or CQ status, assuming adherence to a common workflow.
1566
1567 Returns None if no issue for this branch, or specific string keywords.
1568 """
1569 raise NotImplementedError()
1570
1571 def GetCodereviewServer(self):
1572 """Returns server URL without end slash, like "https://codereview.com"."""
1573 raise NotImplementedError()
1574
1575 def FetchDescription(self):
1576 """Fetches and returns description from the codereview server."""
1577 raise NotImplementedError()
1578
tandrii5d48c322016-08-18 16:19:37 -07001579 @classmethod
1580 def IssueConfigKey(cls):
1581 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001582 raise NotImplementedError()
1583
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001584 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001585 def PatchsetConfigKey(cls):
1586 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 raise NotImplementedError()
1588
tandrii5d48c322016-08-18 16:19:37 -07001589 @classmethod
1590 def CodereviewServerConfigKey(cls):
1591 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592 raise NotImplementedError()
1593
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001594 def _PostUnsetIssueProperties(self):
1595 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001596 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001597
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001598 def GetRieveldObjForPresubmit(self):
1599 # This is an unfortunate Rietveld-embeddedness in presubmit.
1600 # For non-Rietveld codereviews, this probably should return a dummy object.
1601 raise NotImplementedError()
1602
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001603 def GetGerritObjForPresubmit(self):
1604 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1605 return None
1606
dsansomee2d6fd92016-09-08 00:10:47 -07001607 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608 """Update the description on codereview site."""
1609 raise NotImplementedError()
1610
1611 def CloseIssue(self):
1612 """Closes the issue."""
1613 raise NotImplementedError()
1614
1615 def GetApprovingReviewers(self):
1616 """Returns a list of reviewers approving the change.
1617
1618 Note: not necessarily committers.
1619 """
1620 raise NotImplementedError()
1621
1622 def GetMostRecentPatchset(self):
1623 """Returns the most recent patchset number from the codereview site."""
1624 raise NotImplementedError()
1625
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001626 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1627 directory):
1628 """Fetches and applies the issue.
1629
1630 Arguments:
1631 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1632 reject: if True, reject the failed patch instead of switching to 3-way
1633 merge. Rietveld only.
1634 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1635 only.
1636 directory: switch to directory before applying the patch. Rietveld only.
1637 """
1638 raise NotImplementedError()
1639
1640 @staticmethod
1641 def ParseIssueURL(parsed_url):
1642 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1643 failed."""
1644 raise NotImplementedError()
1645
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001646 def EnsureAuthenticated(self, force):
1647 """Best effort check that user is authenticated with codereview server.
1648
1649 Arguments:
1650 force: whether to skip confirmation questions.
1651 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 raise NotImplementedError()
1653
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001654 def CMDUploadChange(self, options, args, change):
1655 """Uploads a change to codereview."""
1656 raise NotImplementedError()
1657
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001658 def SetCQState(self, new_state):
1659 """Update the CQ state for latest patchset.
1660
1661 Issue must have been already uploaded and known.
1662 """
1663 raise NotImplementedError()
1664
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001665
1666class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1667 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1668 super(_RietveldChangelistImpl, self).__init__(changelist)
1669 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001670 if not rietveld_server:
1671 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672
1673 self._rietveld_server = rietveld_server
1674 self._auth_config = auth_config
1675 self._props = None
1676 self._rpc_server = None
1677
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001678 def GetCodereviewServer(self):
1679 if not self._rietveld_server:
1680 # If we're on a branch then get the server potentially associated
1681 # with that branch.
1682 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001683 self._rietveld_server = gclient_utils.UpgradeToHttps(
1684 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 if not self._rietveld_server:
1686 self._rietveld_server = settings.GetDefaultServerUrl()
1687 return self._rietveld_server
1688
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001689 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001690 """Best effort check that user is authenticated with Rietveld server."""
1691 if self._auth_config.use_oauth2:
1692 authenticator = auth.get_authenticator_for_host(
1693 self.GetCodereviewServer(), self._auth_config)
1694 if not authenticator.has_cached_credentials():
1695 raise auth.LoginRequiredError(self.GetCodereviewServer())
1696
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697 def FetchDescription(self):
1698 issue = self.GetIssue()
1699 assert issue
1700 try:
1701 return self.RpcServer().get_description(issue).strip()
1702 except urllib2.HTTPError as e:
1703 if e.code == 404:
1704 DieWithError(
1705 ('\nWhile fetching the description for issue %d, received a '
1706 '404 (not found)\n'
1707 'error. It is likely that you deleted this '
1708 'issue on the server. If this is the\n'
1709 'case, please run\n\n'
1710 ' git cl issue 0\n\n'
1711 'to clear the association with the deleted issue. Then run '
1712 'this command again.') % issue)
1713 else:
1714 DieWithError(
1715 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1716 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001717 print('Warning: Failed to retrieve CL description due to network '
1718 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 return ''
1720
1721 def GetMostRecentPatchset(self):
1722 return self.GetIssueProperties()['patchsets'][-1]
1723
1724 def GetPatchSetDiff(self, issue, patchset):
1725 return self.RpcServer().get(
1726 '/download/issue%s_%s.diff' % (issue, patchset))
1727
1728 def GetIssueProperties(self):
1729 if self._props is None:
1730 issue = self.GetIssue()
1731 if not issue:
1732 self._props = {}
1733 else:
1734 self._props = self.RpcServer().get_issue_properties(issue, True)
1735 return self._props
1736
1737 def GetApprovingReviewers(self):
1738 return get_approving_reviewers(self.GetIssueProperties())
1739
1740 def AddComment(self, message):
1741 return self.RpcServer().add_comment(self.GetIssue(), message)
1742
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001743 def GetStatus(self):
1744 """Apply a rough heuristic to give a simple summary of an issue's review
1745 or CQ status, assuming adherence to a common workflow.
1746
1747 Returns None if no issue for this branch, or one of the following keywords:
1748 * 'error' - error from review tool (including deleted issues)
1749 * 'unsent' - not sent for review
1750 * 'waiting' - waiting for review
1751 * 'reply' - waiting for owner to reply to review
1752 * 'lgtm' - LGTM from at least one approved reviewer
1753 * 'commit' - in the commit queue
1754 * 'closed' - closed
1755 """
1756 if not self.GetIssue():
1757 return None
1758
1759 try:
1760 props = self.GetIssueProperties()
1761 except urllib2.HTTPError:
1762 return 'error'
1763
1764 if props.get('closed'):
1765 # Issue is closed.
1766 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001767 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001768 # Issue is in the commit queue.
1769 return 'commit'
1770
1771 try:
1772 reviewers = self.GetApprovingReviewers()
1773 except urllib2.HTTPError:
1774 return 'error'
1775
1776 if reviewers:
1777 # Was LGTM'ed.
1778 return 'lgtm'
1779
1780 messages = props.get('messages') or []
1781
tandrii9d2c7a32016-06-22 03:42:45 -07001782 # Skip CQ messages that don't require owner's action.
1783 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1784 if 'Dry run:' in messages[-1]['text']:
1785 messages.pop()
1786 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1787 # This message always follows prior messages from CQ,
1788 # so skip this too.
1789 messages.pop()
1790 else:
1791 # This is probably a CQ messages warranting user attention.
1792 break
1793
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001794 if not messages:
1795 # No message was sent.
1796 return 'unsent'
1797 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001798 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001799 return 'reply'
1800 return 'waiting'
1801
dsansomee2d6fd92016-09-08 00:10:47 -07001802 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001803 return self.RpcServer().update_description(
1804 self.GetIssue(), self.description)
1805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001807 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001809 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001810 return self.SetFlags({flag: value})
1811
1812 def SetFlags(self, flags):
1813 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001814 """
phajdan.jr68598232016-08-10 03:28:28 -07001815 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001816 try:
tandrii4b233bd2016-07-06 03:50:29 -07001817 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001818 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001819 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001820 if e.code == 404:
1821 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1822 if e.code == 403:
1823 DieWithError(
1824 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001825 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001826 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001828 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 """Returns an upload.RpcServer() to access this review's rietveld instance.
1830 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001831 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001832 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001834 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001835 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001837 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001838 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001839 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840
tandrii5d48c322016-08-18 16:19:37 -07001841 @classmethod
1842 def PatchsetConfigKey(cls):
1843 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844
tandrii5d48c322016-08-18 16:19:37 -07001845 @classmethod
1846 def CodereviewServerConfigKey(cls):
1847 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001848
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 def GetRieveldObjForPresubmit(self):
1850 return self.RpcServer()
1851
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001852 def SetCQState(self, new_state):
1853 props = self.GetIssueProperties()
1854 if props.get('private'):
1855 DieWithError('Cannot set-commit on private issue')
1856
1857 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001858 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001859 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001860 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001861 else:
tandrii4b233bd2016-07-06 03:50:29 -07001862 assert new_state == _CQState.DRY_RUN
1863 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001864
1865
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001866 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1867 directory):
1868 # TODO(maruel): Use apply_issue.py
1869
1870 # PatchIssue should never be called with a dirty tree. It is up to the
1871 # caller to check this, but just in case we assert here since the
1872 # consequences of the caller not checking this could be dire.
1873 assert(not git_common.is_dirty_git_tree('apply'))
1874 assert(parsed_issue_arg.valid)
1875 self._changelist.issue = parsed_issue_arg.issue
1876 if parsed_issue_arg.hostname:
1877 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1878
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001879 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1880 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001881 assert parsed_issue_arg.patchset
1882 patchset = parsed_issue_arg.patchset
1883 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1884 else:
1885 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1886 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1887
1888 # Switch up to the top-level directory, if necessary, in preparation for
1889 # applying the patch.
1890 top = settings.GetRelativeRoot()
1891 if top:
1892 os.chdir(top)
1893
1894 # Git patches have a/ at the beginning of source paths. We strip that out
1895 # with a sed script rather than the -p flag to patch so we can feed either
1896 # Git or svn-style patches into the same apply command.
1897 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1898 try:
1899 patch_data = subprocess2.check_output(
1900 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1901 except subprocess2.CalledProcessError:
1902 DieWithError('Git patch mungling failed.')
1903 logging.info(patch_data)
1904
1905 # We use "git apply" to apply the patch instead of "patch" so that we can
1906 # pick up file adds.
1907 # The --index flag means: also insert into the index (so we catch adds).
1908 cmd = ['git', 'apply', '--index', '-p0']
1909 if directory:
1910 cmd.extend(('--directory', directory))
1911 if reject:
1912 cmd.append('--reject')
1913 elif IsGitVersionAtLeast('1.7.12'):
1914 cmd.append('--3way')
1915 try:
1916 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1917 stdin=patch_data, stdout=subprocess2.VOID)
1918 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001919 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001920 return 1
1921
1922 # If we had an issue, commit the current state and register the issue.
1923 if not nocommit:
1924 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1925 'patch from issue %(i)s at patchset '
1926 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1927 % {'i': self.GetIssue(), 'p': patchset})])
1928 self.SetIssue(self.GetIssue())
1929 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001930 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001931 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001932 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001933 return 0
1934
1935 @staticmethod
1936 def ParseIssueURL(parsed_url):
1937 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1938 return None
wychen3c1c1722016-08-04 11:46:36 -07001939 # Rietveld patch: https://domain/<number>/#ps<patchset>
1940 match = re.match(r'/(\d+)/$', parsed_url.path)
1941 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1942 if match and match2:
1943 return _RietveldParsedIssueNumberArgument(
1944 issue=int(match.group(1)),
1945 patchset=int(match2.group(1)),
1946 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001947 # Typical url: https://domain/<issue_number>[/[other]]
1948 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1949 if match:
1950 return _RietveldParsedIssueNumberArgument(
1951 issue=int(match.group(1)),
1952 hostname=parsed_url.netloc)
1953 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1954 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1955 if match:
1956 return _RietveldParsedIssueNumberArgument(
1957 issue=int(match.group(1)),
1958 patchset=int(match.group(2)),
1959 hostname=parsed_url.netloc,
1960 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1961 return None
1962
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001963 def CMDUploadChange(self, options, args, change):
1964 """Upload the patch to Rietveld."""
1965 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1966 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001967 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1968 if options.emulate_svn_auto_props:
1969 upload_args.append('--emulate_svn_auto_props')
1970
1971 change_desc = None
1972
1973 if options.email is not None:
1974 upload_args.extend(['--email', options.email])
1975
1976 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001977 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001978 upload_args.extend(['--title', options.title])
1979 if options.message:
1980 upload_args.extend(['--message', options.message])
1981 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001982 print('This branch is associated with issue %s. '
1983 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001984 else:
nodirca166002016-06-27 10:59:51 -07001985 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001986 upload_args.extend(['--title', options.title])
1987 message = (options.title or options.message or
1988 CreateDescriptionFromLog(args))
1989 change_desc = ChangeDescription(message)
1990 if options.reviewers or options.tbr_owners:
1991 change_desc.update_reviewers(options.reviewers,
1992 options.tbr_owners,
1993 change)
1994 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001995 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001996
1997 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001998 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001999 return 1
2000
2001 upload_args.extend(['--message', change_desc.description])
2002 if change_desc.get_reviewers():
2003 upload_args.append('--reviewers=%s' % ','.join(
2004 change_desc.get_reviewers()))
2005 if options.send_mail:
2006 if not change_desc.get_reviewers():
2007 DieWithError("Must specify reviewers to send email.")
2008 upload_args.append('--send_mail')
2009
2010 # We check this before applying rietveld.private assuming that in
2011 # rietveld.cc only addresses which we can send private CLs to are listed
2012 # if rietveld.private is set, and so we should ignore rietveld.cc only
2013 # when --private is specified explicitly on the command line.
2014 if options.private:
2015 logging.warn('rietveld.cc is ignored since private flag is specified. '
2016 'You need to review and add them manually if necessary.')
2017 cc = self.GetCCListWithoutDefault()
2018 else:
2019 cc = self.GetCCList()
2020 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2021 if cc:
2022 upload_args.extend(['--cc', cc])
2023
2024 if options.private or settings.GetDefaultPrivateFlag() == "True":
2025 upload_args.append('--private')
2026
2027 upload_args.extend(['--git_similarity', str(options.similarity)])
2028 if not options.find_copies:
2029 upload_args.extend(['--git_no_find_copies'])
2030
2031 # Include the upstream repo's URL in the change -- this is useful for
2032 # projects that have their source spread across multiple repos.
2033 remote_url = self.GetGitBaseUrlFromConfig()
2034 if not remote_url:
2035 if settings.GetIsGitSvn():
2036 remote_url = self.GetGitSvnRemoteUrl()
2037 else:
2038 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2039 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2040 self.GetUpstreamBranch().split('/')[-1])
2041 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002042 remote, remote_branch = self.GetRemoteBranch()
2043 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2044 settings.GetPendingRefPrefix())
2045 if target_ref:
2046 upload_args.extend(['--target_ref', target_ref])
2047
2048 # Look for dependent patchsets. See crbug.com/480453 for more details.
2049 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2050 upstream_branch = ShortBranchName(upstream_branch)
2051 if remote is '.':
2052 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002053 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002054 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002055 print()
2056 print('Skipping dependency patchset upload because git config '
2057 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2058 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002059 else:
2060 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002061 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002062 auth_config=auth_config)
2063 branch_cl_issue_url = branch_cl.GetIssueURL()
2064 branch_cl_issue = branch_cl.GetIssue()
2065 branch_cl_patchset = branch_cl.GetPatchset()
2066 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2067 upload_args.extend(
2068 ['--depends_on_patchset', '%s:%s' % (
2069 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002070 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002071 '\n'
2072 'The current branch (%s) is tracking a local branch (%s) with '
2073 'an associated CL.\n'
2074 'Adding %s/#ps%s as a dependency patchset.\n'
2075 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2076 branch_cl_patchset))
2077
2078 project = settings.GetProject()
2079 if project:
2080 upload_args.extend(['--project', project])
2081
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002082 try:
2083 upload_args = ['upload'] + upload_args + args
2084 logging.info('upload.RealMain(%s)', upload_args)
2085 issue, patchset = upload.RealMain(upload_args)
2086 issue = int(issue)
2087 patchset = int(patchset)
2088 except KeyboardInterrupt:
2089 sys.exit(1)
2090 except:
2091 # If we got an exception after the user typed a description for their
2092 # change, back up the description before re-raising.
2093 if change_desc:
2094 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2095 print('\nGot exception while uploading -- saving description to %s\n' %
2096 backup_path)
2097 backup_file = open(backup_path, 'w')
2098 backup_file.write(change_desc.description)
2099 backup_file.close()
2100 raise
2101
2102 if not self.GetIssue():
2103 self.SetIssue(issue)
2104 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002105 return 0
2106
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002107
2108class _GerritChangelistImpl(_ChangelistCodereviewBase):
2109 def __init__(self, changelist, auth_config=None):
2110 # auth_config is Rietveld thing, kept here to preserve interface only.
2111 super(_GerritChangelistImpl, self).__init__(changelist)
2112 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002113 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002114 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002115 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002116
2117 def _GetGerritHost(self):
2118 # Lazy load of configs.
2119 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002120 if self._gerrit_host and '.' not in self._gerrit_host:
2121 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2122 # This happens for internal stuff http://crbug.com/614312.
2123 parsed = urlparse.urlparse(self.GetRemoteUrl())
2124 if parsed.scheme == 'sso':
2125 print('WARNING: using non https URLs for remote is likely broken\n'
2126 ' Your current remote is: %s' % self.GetRemoteUrl())
2127 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2128 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002129 return self._gerrit_host
2130
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002131 def _GetGitHost(self):
2132 """Returns git host to be used when uploading change to Gerrit."""
2133 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2134
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002135 def GetCodereviewServer(self):
2136 if not self._gerrit_server:
2137 # If we're on a branch then get the server potentially associated
2138 # with that branch.
2139 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002140 self._gerrit_server = self._GitGetBranchConfigValue(
2141 self.CodereviewServerConfigKey())
2142 if self._gerrit_server:
2143 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002144 if not self._gerrit_server:
2145 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2146 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002147 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002148 parts[0] = parts[0] + '-review'
2149 self._gerrit_host = '.'.join(parts)
2150 self._gerrit_server = 'https://%s' % self._gerrit_host
2151 return self._gerrit_server
2152
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002153 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002154 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002155 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002156
tandrii5d48c322016-08-18 16:19:37 -07002157 @classmethod
2158 def PatchsetConfigKey(cls):
2159 return 'gerritpatchset'
2160
2161 @classmethod
2162 def CodereviewServerConfigKey(cls):
2163 return 'gerritserver'
2164
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002165 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002166 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002167 if settings.GetGerritSkipEnsureAuthenticated():
2168 # For projects with unusual authentication schemes.
2169 # See http://crbug.com/603378.
2170 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002171 # Lazy-loader to identify Gerrit and Git hosts.
2172 if gerrit_util.GceAuthenticator.is_gce():
2173 return
2174 self.GetCodereviewServer()
2175 git_host = self._GetGitHost()
2176 assert self._gerrit_server and self._gerrit_host
2177 cookie_auth = gerrit_util.CookiesAuthenticator()
2178
2179 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2180 git_auth = cookie_auth.get_auth_header(git_host)
2181 if gerrit_auth and git_auth:
2182 if gerrit_auth == git_auth:
2183 return
2184 print((
2185 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2186 ' Check your %s or %s file for credentials of hosts:\n'
2187 ' %s\n'
2188 ' %s\n'
2189 ' %s') %
2190 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2191 git_host, self._gerrit_host,
2192 cookie_auth.get_new_password_message(git_host)))
2193 if not force:
2194 ask_for_data('If you know what you are doing, press Enter to continue, '
2195 'Ctrl+C to abort.')
2196 return
2197 else:
2198 missing = (
2199 [] if gerrit_auth else [self._gerrit_host] +
2200 [] if git_auth else [git_host])
2201 DieWithError('Credentials for the following hosts are required:\n'
2202 ' %s\n'
2203 'These are read from %s (or legacy %s)\n'
2204 '%s' % (
2205 '\n '.join(missing),
2206 cookie_auth.get_gitcookies_path(),
2207 cookie_auth.get_netrc_path(),
2208 cookie_auth.get_new_password_message(git_host)))
2209
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002210 def _PostUnsetIssueProperties(self):
2211 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002212 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002213
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214 def GetRieveldObjForPresubmit(self):
2215 class ThisIsNotRietveldIssue(object):
2216 def __nonzero__(self):
2217 # This is a hack to make presubmit_support think that rietveld is not
2218 # defined, yet still ensure that calls directly result in a decent
2219 # exception message below.
2220 return False
2221
2222 def __getattr__(self, attr):
2223 print(
2224 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2225 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2226 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2227 'or use Rietveld for codereview.\n'
2228 'See also http://crbug.com/579160.' % attr)
2229 raise NotImplementedError()
2230 return ThisIsNotRietveldIssue()
2231
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002232 def GetGerritObjForPresubmit(self):
2233 return presubmit_support.GerritAccessor(self._GetGerritHost())
2234
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002235 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002236 """Apply a rough heuristic to give a simple summary of an issue's review
2237 or CQ status, assuming adherence to a common workflow.
2238
2239 Returns None if no issue for this branch, or one of the following keywords:
2240 * 'error' - error from review tool (including deleted issues)
2241 * 'unsent' - no reviewers added
2242 * 'waiting' - waiting for review
2243 * 'reply' - waiting for owner to reply to review
2244 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2245 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2246 * 'commit' - in the commit queue
2247 * 'closed' - abandoned
2248 """
2249 if not self.GetIssue():
2250 return None
2251
2252 try:
2253 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2254 except httplib.HTTPException:
2255 return 'error'
2256
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002257 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002258 return 'closed'
2259
2260 cq_label = data['labels'].get('Commit-Queue', {})
2261 if cq_label:
2262 # Vote value is a stringified integer, which we expect from 0 to 2.
2263 vote_value = cq_label.get('value', '0')
2264 vote_text = cq_label.get('values', {}).get(vote_value, '')
2265 if vote_text.lower() == 'commit':
2266 return 'commit'
2267
2268 lgtm_label = data['labels'].get('Code-Review', {})
2269 if lgtm_label:
2270 if 'rejected' in lgtm_label:
2271 return 'not lgtm'
2272 if 'approved' in lgtm_label:
2273 return 'lgtm'
2274
2275 if not data.get('reviewers', {}).get('REVIEWER', []):
2276 return 'unsent'
2277
2278 messages = data.get('messages', [])
2279 if messages:
2280 owner = data['owner'].get('_account_id')
2281 last_message_author = messages[-1].get('author', {}).get('_account_id')
2282 if owner != last_message_author:
2283 # Some reply from non-owner.
2284 return 'reply'
2285
2286 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002287
2288 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002289 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 return data['revisions'][data['current_revision']]['_number']
2291
2292 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002293 data = self._GetChangeDetail(['CURRENT_REVISION'])
2294 current_rev = data['current_revision']
2295 url = data['revisions'][current_rev]['fetch']['http']['url']
2296 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002297
dsansomee2d6fd92016-09-08 00:10:47 -07002298 def UpdateDescriptionRemote(self, description, force=False):
2299 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2300 if not force:
2301 ask_for_data(
2302 'The description cannot be modified while the issue has a pending '
2303 'unpublished edit. Either publish the edit in the Gerrit web UI '
2304 'or delete it.\n\n'
2305 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2306
2307 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2308 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002309 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2310 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002311
2312 def CloseIssue(self):
2313 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2314
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002315 def GetApprovingReviewers(self):
2316 """Returns a list of reviewers approving the change.
2317
2318 Note: not necessarily committers.
2319 """
2320 raise NotImplementedError()
2321
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002322 def SubmitIssue(self, wait_for_merge=True):
2323 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2324 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002325
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002326 def _GetChangeDetail(self, options=None, issue=None):
2327 options = options or []
2328 issue = issue or self.GetIssue()
2329 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002330 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2331 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002332
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002333 def CMDLand(self, force, bypass_hooks, verbose):
2334 if git_common.is_dirty_git_tree('land'):
2335 return 1
tandriid60367b2016-06-22 05:25:12 -07002336 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2337 if u'Commit-Queue' in detail.get('labels', {}):
2338 if not force:
2339 ask_for_data('\nIt seems this repository has a Commit Queue, '
2340 'which can test and land changes for you. '
2341 'Are you sure you wish to bypass it?\n'
2342 'Press Enter to continue, Ctrl+C to abort.')
2343
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002344 differs = True
tandriic4344b52016-08-29 06:04:54 -07002345 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002346 # Note: git diff outputs nothing if there is no diff.
2347 if not last_upload or RunGit(['diff', last_upload]).strip():
2348 print('WARNING: some changes from local branch haven\'t been uploaded')
2349 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002350 if detail['current_revision'] == last_upload:
2351 differs = False
2352 else:
2353 print('WARNING: local branch contents differ from latest uploaded '
2354 'patchset')
2355 if differs:
2356 if not force:
2357 ask_for_data(
2358 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2359 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2360 elif not bypass_hooks:
2361 hook_results = self.RunHook(
2362 committing=True,
2363 may_prompt=not force,
2364 verbose=verbose,
2365 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2366 if not hook_results.should_continue():
2367 return 1
2368
2369 self.SubmitIssue(wait_for_merge=True)
2370 print('Issue %s has been submitted.' % self.GetIssueURL())
2371 return 0
2372
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002373 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2374 directory):
2375 assert not reject
2376 assert not nocommit
2377 assert not directory
2378 assert parsed_issue_arg.valid
2379
2380 self._changelist.issue = parsed_issue_arg.issue
2381
2382 if parsed_issue_arg.hostname:
2383 self._gerrit_host = parsed_issue_arg.hostname
2384 self._gerrit_server = 'https://%s' % self._gerrit_host
2385
2386 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2387
2388 if not parsed_issue_arg.patchset:
2389 # Use current revision by default.
2390 revision_info = detail['revisions'][detail['current_revision']]
2391 patchset = int(revision_info['_number'])
2392 else:
2393 patchset = parsed_issue_arg.patchset
2394 for revision_info in detail['revisions'].itervalues():
2395 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2396 break
2397 else:
2398 DieWithError('Couldn\'t find patchset %i in issue %i' %
2399 (parsed_issue_arg.patchset, self.GetIssue()))
2400
2401 fetch_info = revision_info['fetch']['http']
2402 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2403 RunGit(['cherry-pick', 'FETCH_HEAD'])
2404 self.SetIssue(self.GetIssue())
2405 self.SetPatchset(patchset)
2406 print('Committed patch for issue %i pathset %i locally' %
2407 (self.GetIssue(), self.GetPatchset()))
2408 return 0
2409
2410 @staticmethod
2411 def ParseIssueURL(parsed_url):
2412 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2413 return None
2414 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2415 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2416 # Short urls like https://domain/<issue_number> can be used, but don't allow
2417 # specifying the patchset (you'd 404), but we allow that here.
2418 if parsed_url.path == '/':
2419 part = parsed_url.fragment
2420 else:
2421 part = parsed_url.path
2422 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2423 if match:
2424 return _ParsedIssueNumberArgument(
2425 issue=int(match.group(2)),
2426 patchset=int(match.group(4)) if match.group(4) else None,
2427 hostname=parsed_url.netloc)
2428 return None
2429
tandrii16e0b4e2016-06-07 10:34:28 -07002430 def _GerritCommitMsgHookCheck(self, offer_removal):
2431 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2432 if not os.path.exists(hook):
2433 return
2434 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2435 # custom developer made one.
2436 data = gclient_utils.FileRead(hook)
2437 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2438 return
2439 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002440 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002441 'and may interfere with it in subtle ways.\n'
2442 'We recommend you remove the commit-msg hook.')
2443 if offer_removal:
2444 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2445 if reply.lower().startswith('y'):
2446 gclient_utils.rm_file_or_tree(hook)
2447 print('Gerrit commit-msg hook removed.')
2448 else:
2449 print('OK, will keep Gerrit commit-msg hook in place.')
2450
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002451 def CMDUploadChange(self, options, args, change):
2452 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002453 if options.squash and options.no_squash:
2454 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002455
2456 if not options.squash and not options.no_squash:
2457 # Load default for user, repo, squash=true, in this order.
2458 options.squash = settings.GetSquashGerritUploads()
2459 elif options.no_squash:
2460 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002461
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002462 # We assume the remote called "origin" is the one we want.
2463 # It is probably not worthwhile to support different workflows.
2464 gerrit_remote = 'origin'
2465
2466 remote, remote_branch = self.GetRemoteBranch()
2467 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2468 pending_prefix='')
2469
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002471 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 if self.GetIssue():
2473 # Try to get the message from a previous upload.
2474 message = self.GetDescription()
2475 if not message:
2476 DieWithError(
2477 'failed to fetch description from current Gerrit issue %d\n'
2478 '%s' % (self.GetIssue(), self.GetIssueURL()))
2479 change_id = self._GetChangeDetail()['change_id']
2480 while True:
2481 footer_change_ids = git_footers.get_footer_change_id(message)
2482 if footer_change_ids == [change_id]:
2483 break
2484 if not footer_change_ids:
2485 message = git_footers.add_footer_change_id(message, change_id)
2486 print('WARNING: appended missing Change-Id to issue description')
2487 continue
2488 # There is already a valid footer but with different or several ids.
2489 # Doing this automatically is non-trivial as we don't want to lose
2490 # existing other footers, yet we want to append just 1 desired
2491 # Change-Id. Thus, just create a new footer, but let user verify the
2492 # new description.
2493 message = '%s\n\nChange-Id: %s' % (message, change_id)
2494 print(
2495 'WARNING: issue %s has Change-Id footer(s):\n'
2496 ' %s\n'
2497 'but issue has Change-Id %s, according to Gerrit.\n'
2498 'Please, check the proposed correction to the description, '
2499 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2500 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2501 change_id))
2502 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2503 if not options.force:
2504 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002505 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002506 message = change_desc.description
2507 if not message:
2508 DieWithError("Description is empty. Aborting...")
2509 # Continue the while loop.
2510 # Sanity check of this code - we should end up with proper message
2511 # footer.
2512 assert [change_id] == git_footers.get_footer_change_id(message)
2513 change_desc = ChangeDescription(message)
2514 else:
2515 change_desc = ChangeDescription(
2516 options.message or CreateDescriptionFromLog(args))
2517 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002518 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 if not change_desc.description:
2520 DieWithError("Description is empty. Aborting...")
2521 message = change_desc.description
2522 change_ids = git_footers.get_footer_change_id(message)
2523 if len(change_ids) > 1:
2524 DieWithError('too many Change-Id footers, at most 1 allowed.')
2525 if not change_ids:
2526 # Generate the Change-Id automatically.
2527 message = git_footers.add_footer_change_id(
2528 message, GenerateGerritChangeId(message))
2529 change_desc.set_description(message)
2530 change_ids = git_footers.get_footer_change_id(message)
2531 assert len(change_ids) == 1
2532 change_id = change_ids[0]
2533
2534 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2535 if remote is '.':
2536 # If our upstream branch is local, we base our squashed commit on its
2537 # squashed version.
2538 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2539 # Check the squashed hash of the parent.
2540 parent = RunGit(['config',
2541 'branch.%s.gerritsquashhash' % upstream_branch_name],
2542 error_ok=True).strip()
2543 # Verify that the upstream branch has been uploaded too, otherwise
2544 # Gerrit will create additional CLs when uploading.
2545 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2546 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002547 DieWithError(
2548 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002549 'Note: maybe you\'ve uploaded it with --no-squash. '
2550 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002551 ' git cl upload --squash\n' % upstream_branch_name)
2552 else:
2553 parent = self.GetCommonAncestorWithUpstream()
2554
2555 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2556 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2557 '-m', message]).strip()
2558 else:
2559 change_desc = ChangeDescription(
2560 options.message or CreateDescriptionFromLog(args))
2561 if not change_desc.description:
2562 DieWithError("Description is empty. Aborting...")
2563
2564 if not git_footers.get_footer_change_id(change_desc.description):
2565 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002566 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2567 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002568 ref_to_push = 'HEAD'
2569 parent = '%s/%s' % (gerrit_remote, branch)
2570 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2571
2572 assert change_desc
2573 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2574 ref_to_push)]).splitlines()
2575 if len(commits) > 1:
2576 print('WARNING: This will upload %d commits. Run the following command '
2577 'to see which commits will be uploaded: ' % len(commits))
2578 print('git log %s..%s' % (parent, ref_to_push))
2579 print('You can also use `git squash-branch` to squash these into a '
2580 'single commit.')
2581 ask_for_data('About to upload; enter to confirm.')
2582
2583 if options.reviewers or options.tbr_owners:
2584 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2585 change)
2586
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002587 # Extra options that can be specified at push time. Doc:
2588 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2589 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002590 if change_desc.get_reviewers(tbr_only=True):
2591 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2592 refspec_opts.append('l=Code-Review+1')
2593
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002594 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002595 if not re.match(r'^[\w ]+$', options.title):
2596 options.title = re.sub(r'[^\w ]', '', options.title)
2597 print('WARNING: Patchset title may only contain alphanumeric chars '
2598 'and spaces. Cleaned up title:\n%s' % options.title)
2599 if not options.force:
2600 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002601 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2602 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002603 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2604
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002605 if options.send_mail:
2606 if not change_desc.get_reviewers():
2607 DieWithError('Must specify reviewers to send email.')
2608 refspec_opts.append('notify=ALL')
2609 else:
2610 refspec_opts.append('notify=NONE')
2611
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002612 cc = self.GetCCList().split(',')
2613 if options.cc:
2614 cc.extend(options.cc)
2615 cc = filter(None, cc)
2616 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002617 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002618
tandrii99a72f22016-08-17 14:33:24 -07002619 reviewers = change_desc.get_reviewers()
2620 if reviewers:
2621 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002622
agablec6787972016-09-09 16:13:34 -07002623 if options.private:
2624 refspec_opts.append('draft')
2625
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002626 refspec_suffix = ''
2627 if refspec_opts:
2628 refspec_suffix = '%' + ','.join(refspec_opts)
2629 assert ' ' not in refspec_suffix, (
2630 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002631 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002632
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002633 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002634 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002635 print_stdout=True,
2636 # Flush after every line: useful for seeing progress when running as
2637 # recipe.
2638 filter_fn=lambda _: sys.stdout.flush())
2639
2640 if options.squash:
2641 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2642 change_numbers = [m.group(1)
2643 for m in map(regex.match, push_stdout.splitlines())
2644 if m]
2645 if len(change_numbers) != 1:
2646 DieWithError(
2647 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2648 'Change-Id: %s') % (len(change_numbers), change_id))
2649 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002650 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002651 return 0
2652
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002653 def _AddChangeIdToCommitMessage(self, options, args):
2654 """Re-commits using the current message, assumes the commit hook is in
2655 place.
2656 """
2657 log_desc = options.message or CreateDescriptionFromLog(args)
2658 git_command = ['commit', '--amend', '-m', log_desc]
2659 RunGit(git_command)
2660 new_log_desc = CreateDescriptionFromLog(args)
2661 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002662 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002663 return new_log_desc
2664 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002665 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002666
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002667 def SetCQState(self, new_state):
2668 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002669 vote_map = {
2670 _CQState.NONE: 0,
2671 _CQState.DRY_RUN: 1,
2672 _CQState.COMMIT : 2,
2673 }
2674 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2675 labels={'Commit-Queue': vote_map[new_state]})
2676
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002677
2678_CODEREVIEW_IMPLEMENTATIONS = {
2679 'rietveld': _RietveldChangelistImpl,
2680 'gerrit': _GerritChangelistImpl,
2681}
2682
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002683
iannuccie53c9352016-08-17 14:40:40 -07002684def _add_codereview_issue_select_options(parser, extra=""):
2685 _add_codereview_select_options(parser)
2686
2687 text = ('Operate on this issue number instead of the current branch\'s '
2688 'implicit issue.')
2689 if extra:
2690 text += ' '+extra
2691 parser.add_option('-i', '--issue', type=int, help=text)
2692
2693
2694def _process_codereview_issue_select_options(parser, options):
2695 _process_codereview_select_options(parser, options)
2696 if options.issue is not None and not options.forced_codereview:
2697 parser.error('--issue must be specified with either --rietveld or --gerrit')
2698
2699
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002700def _add_codereview_select_options(parser):
2701 """Appends --gerrit and --rietveld options to force specific codereview."""
2702 parser.codereview_group = optparse.OptionGroup(
2703 parser, 'EXPERIMENTAL! Codereview override options')
2704 parser.add_option_group(parser.codereview_group)
2705 parser.codereview_group.add_option(
2706 '--gerrit', action='store_true',
2707 help='Force the use of Gerrit for codereview')
2708 parser.codereview_group.add_option(
2709 '--rietveld', action='store_true',
2710 help='Force the use of Rietveld for codereview')
2711
2712
2713def _process_codereview_select_options(parser, options):
2714 if options.gerrit and options.rietveld:
2715 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2716 options.forced_codereview = None
2717 if options.gerrit:
2718 options.forced_codereview = 'gerrit'
2719 elif options.rietveld:
2720 options.forced_codereview = 'rietveld'
2721
2722
tandriif9aefb72016-07-01 09:06:51 -07002723def _get_bug_line_values(default_project, bugs):
2724 """Given default_project and comma separated list of bugs, yields bug line
2725 values.
2726
2727 Each bug can be either:
2728 * a number, which is combined with default_project
2729 * string, which is left as is.
2730
2731 This function may produce more than one line, because bugdroid expects one
2732 project per line.
2733
2734 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2735 ['v8:123', 'chromium:789']
2736 """
2737 default_bugs = []
2738 others = []
2739 for bug in bugs.split(','):
2740 bug = bug.strip()
2741 if bug:
2742 try:
2743 default_bugs.append(int(bug))
2744 except ValueError:
2745 others.append(bug)
2746
2747 if default_bugs:
2748 default_bugs = ','.join(map(str, default_bugs))
2749 if default_project:
2750 yield '%s:%s' % (default_project, default_bugs)
2751 else:
2752 yield default_bugs
2753 for other in sorted(others):
2754 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2755 yield other
2756
2757
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002758class ChangeDescription(object):
2759 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002760 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002761 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002762
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002763 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002764 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002765
agable@chromium.org42c20792013-09-12 17:34:49 +00002766 @property # www.logilab.org/ticket/89786
2767 def description(self): # pylint: disable=E0202
2768 return '\n'.join(self._description_lines)
2769
2770 def set_description(self, desc):
2771 if isinstance(desc, basestring):
2772 lines = desc.splitlines()
2773 else:
2774 lines = [line.rstrip() for line in desc]
2775 while lines and not lines[0]:
2776 lines.pop(0)
2777 while lines and not lines[-1]:
2778 lines.pop(-1)
2779 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002780
piman@chromium.org336f9122014-09-04 02:16:55 +00002781 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002782 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002783 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002784 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002785 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002786 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002787
agable@chromium.org42c20792013-09-12 17:34:49 +00002788 # Get the set of R= and TBR= lines and remove them from the desciption.
2789 regexp = re.compile(self.R_LINE)
2790 matches = [regexp.match(line) for line in self._description_lines]
2791 new_desc = [l for i, l in enumerate(self._description_lines)
2792 if not matches[i]]
2793 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002794
agable@chromium.org42c20792013-09-12 17:34:49 +00002795 # Construct new unified R= and TBR= lines.
2796 r_names = []
2797 tbr_names = []
2798 for match in matches:
2799 if not match:
2800 continue
2801 people = cleanup_list([match.group(2).strip()])
2802 if match.group(1) == 'TBR':
2803 tbr_names.extend(people)
2804 else:
2805 r_names.extend(people)
2806 for name in r_names:
2807 if name not in reviewers:
2808 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002809 if add_owners_tbr:
2810 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002811 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002812 all_reviewers = set(tbr_names + reviewers)
2813 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2814 all_reviewers)
2815 tbr_names.extend(owners_db.reviewers_for(missing_files,
2816 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002817 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2818 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2819
2820 # Put the new lines in the description where the old first R= line was.
2821 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2822 if 0 <= line_loc < len(self._description_lines):
2823 if new_tbr_line:
2824 self._description_lines.insert(line_loc, new_tbr_line)
2825 if new_r_line:
2826 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002827 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002828 if new_r_line:
2829 self.append_footer(new_r_line)
2830 if new_tbr_line:
2831 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002832
tandriif9aefb72016-07-01 09:06:51 -07002833 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002834 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002835 self.set_description([
2836 '# Enter a description of the change.',
2837 '# This will be displayed on the codereview site.',
2838 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002839 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002840 '--------------------',
2841 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002842
agable@chromium.org42c20792013-09-12 17:34:49 +00002843 regexp = re.compile(self.BUG_LINE)
2844 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002845 prefix = settings.GetBugPrefix()
2846 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2847 for value in values:
2848 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2849 self.append_footer('BUG=%s' % value)
2850
agable@chromium.org42c20792013-09-12 17:34:49 +00002851 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002852 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002853 if not content:
2854 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002855 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002856
2857 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002858 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2859 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002860 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002861 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002862
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002863 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002864 """Adds a footer line to the description.
2865
2866 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2867 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2868 that Gerrit footers are always at the end.
2869 """
2870 parsed_footer_line = git_footers.parse_footer(line)
2871 if parsed_footer_line:
2872 # Line is a gerrit footer in the form: Footer-Key: any value.
2873 # Thus, must be appended observing Gerrit footer rules.
2874 self.set_description(
2875 git_footers.add_footer(self.description,
2876 key=parsed_footer_line[0],
2877 value=parsed_footer_line[1]))
2878 return
2879
2880 if not self._description_lines:
2881 self._description_lines.append(line)
2882 return
2883
2884 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2885 if gerrit_footers:
2886 # git_footers.split_footers ensures that there is an empty line before
2887 # actual (gerrit) footers, if any. We have to keep it that way.
2888 assert top_lines and top_lines[-1] == ''
2889 top_lines, separator = top_lines[:-1], top_lines[-1:]
2890 else:
2891 separator = [] # No need for separator if there are no gerrit_footers.
2892
2893 prev_line = top_lines[-1] if top_lines else ''
2894 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2895 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2896 top_lines.append('')
2897 top_lines.append(line)
2898 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002899
tandrii99a72f22016-08-17 14:33:24 -07002900 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002901 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002902 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002903 reviewers = [match.group(2).strip()
2904 for match in matches
2905 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002906 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002907
2908
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002909def get_approving_reviewers(props):
2910 """Retrieves the reviewers that approved a CL from the issue properties with
2911 messages.
2912
2913 Note that the list may contain reviewers that are not committer, thus are not
2914 considered by the CQ.
2915 """
2916 return sorted(
2917 set(
2918 message['sender']
2919 for message in props['messages']
2920 if message['approval'] and message['sender'] in props['reviewers']
2921 )
2922 )
2923
2924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002925def FindCodereviewSettingsFile(filename='codereview.settings'):
2926 """Finds the given file starting in the cwd and going up.
2927
2928 Only looks up to the top of the repository unless an
2929 'inherit-review-settings-ok' file exists in the root of the repository.
2930 """
2931 inherit_ok_file = 'inherit-review-settings-ok'
2932 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002933 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2935 root = '/'
2936 while True:
2937 if filename in os.listdir(cwd):
2938 if os.path.isfile(os.path.join(cwd, filename)):
2939 return open(os.path.join(cwd, filename))
2940 if cwd == root:
2941 break
2942 cwd = os.path.dirname(cwd)
2943
2944
2945def LoadCodereviewSettingsFromFile(fileobj):
2946 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002947 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949 def SetProperty(name, setting, unset_error_ok=False):
2950 fullname = 'rietveld.' + name
2951 if setting in keyvals:
2952 RunGit(['config', fullname, keyvals[setting]])
2953 else:
2954 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2955
2956 SetProperty('server', 'CODE_REVIEW_SERVER')
2957 # Only server setting is required. Other settings can be absent.
2958 # In that case, we ignore errors raised during option deletion attempt.
2959 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002960 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2962 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002963 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002964 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002965 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2966 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002967 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002968 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002969 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002970 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2971 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002973 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002974 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002975
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002976 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002977 RunGit(['config', 'gerrit.squash-uploads',
2978 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002979
tandrii@chromium.org28253532016-04-14 13:46:56 +00002980 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002981 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002982 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002984 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2985 #should be of the form
2986 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2987 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2988 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2989 keyvals['ORIGIN_URL_CONFIG']])
2990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002992def urlretrieve(source, destination):
2993 """urllib is broken for SSL connections via a proxy therefore we
2994 can't use urllib.urlretrieve()."""
2995 with open(destination, 'w') as f:
2996 f.write(urllib2.urlopen(source).read())
2997
2998
ukai@chromium.org712d6102013-11-27 00:52:58 +00002999def hasSheBang(fname):
3000 """Checks fname is a #! script."""
3001 with open(fname) as f:
3002 return f.read(2).startswith('#!')
3003
3004
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003005# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3006def DownloadHooks(*args, **kwargs):
3007 pass
3008
3009
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003010def DownloadGerritHook(force):
3011 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003012
3013 Args:
3014 force: True to update hooks. False to install hooks if not present.
3015 """
3016 if not settings.GetIsGerrit():
3017 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003018 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003019 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3020 if not os.access(dst, os.X_OK):
3021 if os.path.exists(dst):
3022 if not force:
3023 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003024 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003025 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003026 if not hasSheBang(dst):
3027 DieWithError('Not a script: %s\n'
3028 'You need to download from\n%s\n'
3029 'into .git/hooks/commit-msg and '
3030 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003031 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3032 except Exception:
3033 if os.path.exists(dst):
3034 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003035 DieWithError('\nFailed to download hooks.\n'
3036 'You need to download from\n%s\n'
3037 'into .git/hooks/commit-msg and '
3038 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003039
3040
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003041
3042def GetRietveldCodereviewSettingsInteractively():
3043 """Prompt the user for settings."""
3044 server = settings.GetDefaultServerUrl(error_ok=True)
3045 prompt = 'Rietveld server (host[:port])'
3046 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3047 newserver = ask_for_data(prompt + ':')
3048 if not server and not newserver:
3049 newserver = DEFAULT_SERVER
3050 if newserver:
3051 newserver = gclient_utils.UpgradeToHttps(newserver)
3052 if newserver != server:
3053 RunGit(['config', 'rietveld.server', newserver])
3054
3055 def SetProperty(initial, caption, name, is_url):
3056 prompt = caption
3057 if initial:
3058 prompt += ' ("x" to clear) [%s]' % initial
3059 new_val = ask_for_data(prompt + ':')
3060 if new_val == 'x':
3061 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3062 elif new_val:
3063 if is_url:
3064 new_val = gclient_utils.UpgradeToHttps(new_val)
3065 if new_val != initial:
3066 RunGit(['config', 'rietveld.' + name, new_val])
3067
3068 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3069 SetProperty(settings.GetDefaultPrivateFlag(),
3070 'Private flag (rietveld only)', 'private', False)
3071 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3072 'tree-status-url', False)
3073 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3074 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3075 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3076 'run-post-upload-hook', False)
3077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003078@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003079def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003080 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003082 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003083 'For Gerrit, see http://crbug.com/603116.')
3084 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003085 parser.add_option('--activate-update', action='store_true',
3086 help='activate auto-updating [rietveld] section in '
3087 '.git/config')
3088 parser.add_option('--deactivate-update', action='store_true',
3089 help='deactivate auto-updating [rietveld] section in '
3090 '.git/config')
3091 options, args = parser.parse_args(args)
3092
3093 if options.deactivate_update:
3094 RunGit(['config', 'rietveld.autoupdate', 'false'])
3095 return
3096
3097 if options.activate_update:
3098 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3099 return
3100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003101 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003102 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103 return 0
3104
3105 url = args[0]
3106 if not url.endswith('codereview.settings'):
3107 url = os.path.join(url, 'codereview.settings')
3108
3109 # Load code review settings and download hooks (if available).
3110 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3111 return 0
3112
3113
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003114def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003115 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003116 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3117 branch = ShortBranchName(branchref)
3118 _, args = parser.parse_args(args)
3119 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003120 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003121 return RunGit(['config', 'branch.%s.base-url' % branch],
3122 error_ok=False).strip()
3123 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003124 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003125 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3126 error_ok=False).strip()
3127
3128
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003129def color_for_status(status):
3130 """Maps a Changelist status to color, for CMDstatus and other tools."""
3131 return {
3132 'unsent': Fore.RED,
3133 'waiting': Fore.BLUE,
3134 'reply': Fore.YELLOW,
3135 'lgtm': Fore.GREEN,
3136 'commit': Fore.MAGENTA,
3137 'closed': Fore.CYAN,
3138 'error': Fore.WHITE,
3139 }.get(status, Fore.WHITE)
3140
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003141
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003142def get_cl_statuses(changes, fine_grained, max_processes=None):
3143 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003144
3145 If fine_grained is true, this will fetch CL statuses from the server.
3146 Otherwise, simply indicate if there's a matching url for the given branches.
3147
3148 If max_processes is specified, it is used as the maximum number of processes
3149 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3150 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003151
3152 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003153 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003154 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003155 upload.verbosity = 0
3156
3157 if fine_grained:
3158 # Process one branch synchronously to work through authentication, then
3159 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003160 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003161 def fetch(cl):
3162 try:
3163 return (cl, cl.GetStatus())
3164 except:
3165 # See http://crbug.com/629863.
3166 logging.exception('failed to fetch status for %s:', cl)
3167 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003168 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003169
tandriiea9514a2016-08-17 12:32:37 -07003170 changes_to_fetch = changes[1:]
3171 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003172 # Exit early if there was only one branch to fetch.
3173 return
3174
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003175 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003176 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003177 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003178 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003179
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003180 fetched_cls = set()
3181 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003182 while True:
3183 try:
3184 row = it.next(timeout=5)
3185 except multiprocessing.TimeoutError:
3186 break
3187
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003188 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003189 yield row
3190
3191 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003192 for cl in set(changes_to_fetch) - fetched_cls:
3193 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003194
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003195 else:
3196 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003197 for cl in changes:
3198 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003199
rmistry@google.com2dd99862015-06-22 12:22:18 +00003200
3201def upload_branch_deps(cl, args):
3202 """Uploads CLs of local branches that are dependents of the current branch.
3203
3204 If the local branch dependency tree looks like:
3205 test1 -> test2.1 -> test3.1
3206 -> test3.2
3207 -> test2.2 -> test3.3
3208
3209 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3210 run on the dependent branches in this order:
3211 test2.1, test3.1, test3.2, test2.2, test3.3
3212
3213 Note: This function does not rebase your local dependent branches. Use it when
3214 you make a change to the parent branch that will not conflict with its
3215 dependent branches, and you would like their dependencies updated in
3216 Rietveld.
3217 """
3218 if git_common.is_dirty_git_tree('upload-branch-deps'):
3219 return 1
3220
3221 root_branch = cl.GetBranch()
3222 if root_branch is None:
3223 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3224 'Get on a branch!')
3225 if not cl.GetIssue() or not cl.GetPatchset():
3226 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3227 'patchset dependencies without an uploaded CL.')
3228
3229 branches = RunGit(['for-each-ref',
3230 '--format=%(refname:short) %(upstream:short)',
3231 'refs/heads'])
3232 if not branches:
3233 print('No local branches found.')
3234 return 0
3235
3236 # Create a dictionary of all local branches to the branches that are dependent
3237 # on it.
3238 tracked_to_dependents = collections.defaultdict(list)
3239 for b in branches.splitlines():
3240 tokens = b.split()
3241 if len(tokens) == 2:
3242 branch_name, tracked = tokens
3243 tracked_to_dependents[tracked].append(branch_name)
3244
vapiera7fbd5a2016-06-16 09:17:49 -07003245 print()
3246 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003247 dependents = []
3248 def traverse_dependents_preorder(branch, padding=''):
3249 dependents_to_process = tracked_to_dependents.get(branch, [])
3250 padding += ' '
3251 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003252 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003253 dependents.append(dependent)
3254 traverse_dependents_preorder(dependent, padding)
3255 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003256 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003257
3258 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003259 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003260 return 0
3261
vapiera7fbd5a2016-06-16 09:17:49 -07003262 print('This command will checkout all dependent branches and run '
3263 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003264 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3265
andybons@chromium.org962f9462016-02-03 20:00:42 +00003266 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003267 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003268 args.extend(['-t', 'Updated patchset dependency'])
3269
rmistry@google.com2dd99862015-06-22 12:22:18 +00003270 # Record all dependents that failed to upload.
3271 failures = {}
3272 # Go through all dependents, checkout the branch and upload.
3273 try:
3274 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003275 print()
3276 print('--------------------------------------')
3277 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003278 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003279 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003280 try:
3281 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003282 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003283 failures[dependent_branch] = 1
3284 except: # pylint: disable=W0702
3285 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003286 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003287 finally:
3288 # Swap back to the original root branch.
3289 RunGit(['checkout', '-q', root_branch])
3290
vapiera7fbd5a2016-06-16 09:17:49 -07003291 print()
3292 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003293 for dependent_branch in dependents:
3294 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003295 print(' %s : %s' % (dependent_branch, upload_status))
3296 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003297
3298 return 0
3299
3300
kmarshall3bff56b2016-06-06 18:31:47 -07003301def CMDarchive(parser, args):
3302 """Archives and deletes branches associated with closed changelists."""
3303 parser.add_option(
3304 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003305 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003306 parser.add_option(
3307 '-f', '--force', action='store_true',
3308 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003309 parser.add_option(
3310 '-d', '--dry-run', action='store_true',
3311 help='Skip the branch tagging and removal steps.')
3312 parser.add_option(
3313 '-t', '--notags', action='store_true',
3314 help='Do not tag archived branches. '
3315 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003316
3317 auth.add_auth_options(parser)
3318 options, args = parser.parse_args(args)
3319 if args:
3320 parser.error('Unsupported args: %s' % ' '.join(args))
3321 auth_config = auth.extract_auth_config_from_options(options)
3322
3323 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3324 if not branches:
3325 return 0
3326
vapiera7fbd5a2016-06-16 09:17:49 -07003327 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003328 changes = [Changelist(branchref=b, auth_config=auth_config)
3329 for b in branches.splitlines()]
3330 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3331 statuses = get_cl_statuses(changes,
3332 fine_grained=True,
3333 max_processes=options.maxjobs)
3334 proposal = [(cl.GetBranch(),
3335 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3336 for cl, status in statuses
3337 if status == 'closed']
3338 proposal.sort()
3339
3340 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003341 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003342 return 0
3343
3344 current_branch = GetCurrentBranch()
3345
vapiera7fbd5a2016-06-16 09:17:49 -07003346 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003347 if options.notags:
3348 for next_item in proposal:
3349 print(' ' + next_item[0])
3350 else:
3351 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3352 for next_item in proposal:
3353 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003354
kmarshall9249e012016-08-23 12:02:16 -07003355 # Quit now on precondition failure or if instructed by the user, either
3356 # via an interactive prompt or by command line flags.
3357 if options.dry_run:
3358 print('\nNo changes were made (dry run).\n')
3359 return 0
3360 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003361 print('You are currently on a branch \'%s\' which is associated with a '
3362 'closed codereview issue, so archive cannot proceed. Please '
3363 'checkout another branch and run this command again.' %
3364 current_branch)
3365 return 1
kmarshall9249e012016-08-23 12:02:16 -07003366 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003367 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3368 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003369 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003370 return 1
3371
3372 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003373 if not options.notags:
3374 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003375 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003376
vapiera7fbd5a2016-06-16 09:17:49 -07003377 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003378
3379 return 0
3380
3381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003383 """Show status of changelists.
3384
3385 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003386 - Red not sent for review or broken
3387 - Blue waiting for review
3388 - Yellow waiting for you to reply to review
3389 - Green LGTM'ed
3390 - Magenta in the commit queue
3391 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003392
3393 Also see 'git cl comments'.
3394 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003396 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003397 parser.add_option('-f', '--fast', action='store_true',
3398 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003399 parser.add_option(
3400 '-j', '--maxjobs', action='store', type=int,
3401 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003402
3403 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003404 _add_codereview_issue_select_options(
3405 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003406 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003407 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003408 if args:
3409 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003410 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003411
iannuccie53c9352016-08-17 14:40:40 -07003412 if options.issue is not None and not options.field:
3413 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003416 cl = Changelist(auth_config=auth_config, issue=options.issue,
3417 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003418 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003420 elif options.field == 'id':
3421 issueid = cl.GetIssue()
3422 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003423 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003424 elif options.field == 'patch':
3425 patchset = cl.GetPatchset()
3426 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003427 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003428 elif options.field == 'status':
3429 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003430 elif options.field == 'url':
3431 url = cl.GetIssueURL()
3432 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003433 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003434 return 0
3435
3436 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3437 if not branches:
3438 print('No local branch found.')
3439 return 0
3440
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003441 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003442 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003443 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003444 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003445 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003446 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003447 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003448
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003449 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003450 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3451 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3452 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003453 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003454 c, status = output.next()
3455 branch_statuses[c.GetBranch()] = status
3456 status = branch_statuses.pop(branch)
3457 url = cl.GetIssueURL()
3458 if url and (not status or status == 'error'):
3459 # The issue probably doesn't exist anymore.
3460 url += ' (broken)'
3461
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003462 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003463 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003464 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003465 color = ''
3466 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003467 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003468 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003469 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003470 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003471
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003472 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print()
3474 print('Current branch:',)
3475 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003476 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003477 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003478 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003479 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003480 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003481 print('Issue description:')
3482 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003483 return 0
3484
3485
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003486def colorize_CMDstatus_doc():
3487 """To be called once in main() to add colors to git cl status help."""
3488 colors = [i for i in dir(Fore) if i[0].isupper()]
3489
3490 def colorize_line(line):
3491 for color in colors:
3492 if color in line.upper():
3493 # Extract whitespaces first and the leading '-'.
3494 indent = len(line) - len(line.lstrip(' ')) + 1
3495 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3496 return line
3497
3498 lines = CMDstatus.__doc__.splitlines()
3499 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3500
3501
phajdan.jre328cf92016-08-22 04:12:17 -07003502def write_json(path, contents):
3503 with open(path, 'w') as f:
3504 json.dump(contents, f)
3505
3506
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003507@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003509 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003510
3511 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003512 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003513 parser.add_option('-r', '--reverse', action='store_true',
3514 help='Lookup the branch(es) for the specified issues. If '
3515 'no issues are specified, all branches with mapped '
3516 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003517 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003518 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003519 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003520 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003521
dnj@chromium.org406c4402015-03-03 17:22:28 +00003522 if options.reverse:
3523 branches = RunGit(['for-each-ref', 'refs/heads',
3524 '--format=%(refname:short)']).splitlines()
3525
3526 # Reverse issue lookup.
3527 issue_branch_map = {}
3528 for branch in branches:
3529 cl = Changelist(branchref=branch)
3530 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3531 if not args:
3532 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003533 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003534 for issue in args:
3535 if not issue:
3536 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003537 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003538 print('Branch for issue number %s: %s' % (
3539 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003540 if options.json:
3541 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003542 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003543 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003544 if len(args) > 0:
3545 try:
3546 issue = int(args[0])
3547 except ValueError:
3548 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003549 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003550 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003552 if options.json:
3553 write_json(options.json, {
3554 'issue': cl.GetIssue(),
3555 'issue_url': cl.GetIssueURL(),
3556 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 return 0
3558
3559
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003560def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003561 """Shows or posts review comments for any changelist."""
3562 parser.add_option('-a', '--add-comment', dest='comment',
3563 help='comment to add to an issue')
3564 parser.add_option('-i', dest='issue',
3565 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003566 parser.add_option('-j', '--json-file',
3567 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003568 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003569 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003570 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003571
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003572 issue = None
3573 if options.issue:
3574 try:
3575 issue = int(options.issue)
3576 except ValueError:
3577 DieWithError('A review issue id is expected to be a number')
3578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003579 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003580
3581 if options.comment:
3582 cl.AddComment(options.comment)
3583 return 0
3584
3585 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003586 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003587 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003588 summary.append({
3589 'date': message['date'],
3590 'lgtm': False,
3591 'message': message['text'],
3592 'not_lgtm': False,
3593 'sender': message['sender'],
3594 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003595 if message['disapproval']:
3596 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003597 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003598 elif message['approval']:
3599 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003600 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003601 elif message['sender'] == data['owner_email']:
3602 color = Fore.MAGENTA
3603 else:
3604 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003605 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003606 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003607 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003608 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003609 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003610 if options.json_file:
3611 with open(options.json_file, 'wb') as f:
3612 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003613 return 0
3614
3615
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003616@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003617def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003618 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003619 parser.add_option('-d', '--display', action='store_true',
3620 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003621 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003622 help='New description to set for this issue (- for stdin, '
3623 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003624 parser.add_option('-f', '--force', action='store_true',
3625 help='Delete any unpublished Gerrit edits for this issue '
3626 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003627
3628 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003629 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003630 options, args = parser.parse_args(args)
3631 _process_codereview_select_options(parser, options)
3632
3633 target_issue = None
3634 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003635 target_issue = ParseIssueNumberArgument(args[0])
3636 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003637 parser.print_help()
3638 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003639
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003640 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003641
martiniss6eda05f2016-06-30 10:18:35 -07003642 kwargs = {
3643 'auth_config': auth_config,
3644 'codereview': options.forced_codereview,
3645 }
3646 if target_issue:
3647 kwargs['issue'] = target_issue.issue
3648 if options.forced_codereview == 'rietveld':
3649 kwargs['rietveld_server'] = target_issue.hostname
3650
3651 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003652
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003653 if not cl.GetIssue():
3654 DieWithError('This branch has no associated changelist.')
3655 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003656
smut@google.com34fb6b12015-07-13 20:03:26 +00003657 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003658 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003659 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003660
3661 if options.new_description:
3662 text = options.new_description
3663 if text == '-':
3664 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003665 elif text == '+':
3666 base_branch = cl.GetCommonAncestorWithUpstream()
3667 change = cl.GetChange(base_branch, None, local_description=True)
3668 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003669
3670 description.set_description(text)
3671 else:
3672 description.prompt()
3673
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003674 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003675 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003676 return 0
3677
3678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679def CreateDescriptionFromLog(args):
3680 """Pulls out the commit log to use as a base for the CL description."""
3681 log_args = []
3682 if len(args) == 1 and not args[0].endswith('.'):
3683 log_args = [args[0] + '..']
3684 elif len(args) == 1 and args[0].endswith('...'):
3685 log_args = [args[0][:-1]]
3686 elif len(args) == 2:
3687 log_args = [args[0] + '..' + args[1]]
3688 else:
3689 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003690 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003691
3692
thestig@chromium.org44202a22014-03-11 19:22:18 +00003693def CMDlint(parser, args):
3694 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003695 parser.add_option('--filter', action='append', metavar='-x,+y',
3696 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003697 auth.add_auth_options(parser)
3698 options, args = parser.parse_args(args)
3699 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003700
3701 # Access to a protected member _XX of a client class
3702 # pylint: disable=W0212
3703 try:
3704 import cpplint
3705 import cpplint_chromium
3706 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003707 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003708 return 1
3709
3710 # Change the current working directory before calling lint so that it
3711 # shows the correct base.
3712 previous_cwd = os.getcwd()
3713 os.chdir(settings.GetRoot())
3714 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003715 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003716 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3717 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003718 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003719 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003720 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003721
3722 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003723 command = args + files
3724 if options.filter:
3725 command = ['--filter=' + ','.join(options.filter)] + command
3726 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003727
3728 white_regex = re.compile(settings.GetLintRegex())
3729 black_regex = re.compile(settings.GetLintIgnoreRegex())
3730 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3731 for filename in filenames:
3732 if white_regex.match(filename):
3733 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003734 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003735 else:
3736 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3737 extra_check_functions)
3738 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003740 finally:
3741 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003742 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003743 if cpplint._cpplint_state.error_count != 0:
3744 return 1
3745 return 0
3746
3747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003748def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003749 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003750 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003752 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003753 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003754 auth.add_auth_options(parser)
3755 options, args = parser.parse_args(args)
3756 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003757
sbc@chromium.org71437c02015-04-09 19:29:40 +00003758 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003759 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003760 return 1
3761
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003762 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763 if args:
3764 base_branch = args[0]
3765 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003766 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003767 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003768
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003769 cl.RunHook(
3770 committing=not options.upload,
3771 may_prompt=False,
3772 verbose=options.verbose,
3773 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003774 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775
3776
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003777def GenerateGerritChangeId(message):
3778 """Returns Ixxxxxx...xxx change id.
3779
3780 Works the same way as
3781 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3782 but can be called on demand on all platforms.
3783
3784 The basic idea is to generate git hash of a state of the tree, original commit
3785 message, author/committer info and timestamps.
3786 """
3787 lines = []
3788 tree_hash = RunGitSilent(['write-tree'])
3789 lines.append('tree %s' % tree_hash.strip())
3790 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3791 if code == 0:
3792 lines.append('parent %s' % parent.strip())
3793 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3794 lines.append('author %s' % author.strip())
3795 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3796 lines.append('committer %s' % committer.strip())
3797 lines.append('')
3798 # Note: Gerrit's commit-hook actually cleans message of some lines and
3799 # whitespace. This code is not doing this, but it clearly won't decrease
3800 # entropy.
3801 lines.append(message)
3802 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3803 stdin='\n'.join(lines))
3804 return 'I%s' % change_hash.strip()
3805
3806
wittman@chromium.org455dc922015-01-26 20:15:50 +00003807def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3808 """Computes the remote branch ref to use for the CL.
3809
3810 Args:
3811 remote (str): The git remote for the CL.
3812 remote_branch (str): The git remote branch for the CL.
3813 target_branch (str): The target branch specified by the user.
3814 pending_prefix (str): The pending prefix from the settings.
3815 """
3816 if not (remote and remote_branch):
3817 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003818
wittman@chromium.org455dc922015-01-26 20:15:50 +00003819 if target_branch:
3820 # Cannonicalize branch references to the equivalent local full symbolic
3821 # refs, which are then translated into the remote full symbolic refs
3822 # below.
3823 if '/' not in target_branch:
3824 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3825 else:
3826 prefix_replacements = (
3827 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3828 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3829 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3830 )
3831 match = None
3832 for regex, replacement in prefix_replacements:
3833 match = re.search(regex, target_branch)
3834 if match:
3835 remote_branch = target_branch.replace(match.group(0), replacement)
3836 break
3837 if not match:
3838 # This is a branch path but not one we recognize; use as-is.
3839 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003840 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3841 # Handle the refs that need to land in different refs.
3842 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003843
wittman@chromium.org455dc922015-01-26 20:15:50 +00003844 # Create the true path to the remote branch.
3845 # Does the following translation:
3846 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3847 # * refs/remotes/origin/master -> refs/heads/master
3848 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3849 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3850 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3851 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3852 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3853 'refs/heads/')
3854 elif remote_branch.startswith('refs/remotes/branch-heads'):
3855 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3856 # If a pending prefix exists then replace refs/ with it.
3857 if pending_prefix:
3858 remote_branch = remote_branch.replace('refs/', pending_prefix)
3859 return remote_branch
3860
3861
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003862def cleanup_list(l):
3863 """Fixes a list so that comma separated items are put as individual items.
3864
3865 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3866 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3867 """
3868 items = sum((i.split(',') for i in l), [])
3869 stripped_items = (i.strip() for i in items)
3870 return sorted(filter(None, stripped_items))
3871
3872
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003873@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003874def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003875 """Uploads the current changelist to codereview.
3876
3877 Can skip dependency patchset uploads for a branch by running:
3878 git config branch.branch_name.skip-deps-uploads True
3879 To unset run:
3880 git config --unset branch.branch_name.skip-deps-uploads
3881 Can also set the above globally by using the --global flag.
3882 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003883 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3884 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003885 parser.add_option('--bypass-watchlists', action='store_true',
3886 dest='bypass_watchlists',
3887 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003888 parser.add_option('-f', action='store_true', dest='force',
3889 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003890 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003891 parser.add_option('-b', '--bug',
3892 help='pre-populate the bug number(s) for this issue. '
3893 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003894 parser.add_option('--message-file', dest='message_file',
3895 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003896 parser.add_option('-t', dest='title',
3897 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003898 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003899 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003900 help='reviewer email addresses')
3901 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003902 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003903 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003904 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003905 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003906 parser.add_option('--emulate_svn_auto_props',
3907 '--emulate-svn-auto-props',
3908 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003909 dest="emulate_svn_auto_props",
3910 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003911 parser.add_option('-c', '--use-commit-queue', action='store_true',
3912 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003913 parser.add_option('--private', action='store_true',
3914 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003915 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003916 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003917 metavar='TARGET',
3918 help='Apply CL to remote ref TARGET. ' +
3919 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003920 parser.add_option('--squash', action='store_true',
3921 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003922 parser.add_option('--no-squash', action='store_true',
3923 help='Don\'t squash multiple commits into one ' +
3924 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003925 parser.add_option('--email', default=None,
3926 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003927 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3928 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003929 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3930 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003931 help='Send the patchset to do a CQ dry run right after '
3932 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003933 parser.add_option('--dependencies', action='store_true',
3934 help='Uploads CLs of all the local branches that depend on '
3935 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003936
rmistry@google.com2dd99862015-06-22 12:22:18 +00003937 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003938 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003939 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003940 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003941 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003942 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003943 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003944
sbc@chromium.org71437c02015-04-09 19:29:40 +00003945 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003946 return 1
3947
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003948 options.reviewers = cleanup_list(options.reviewers)
3949 options.cc = cleanup_list(options.cc)
3950
tandriib80458a2016-06-23 12:20:07 -07003951 if options.message_file:
3952 if options.message:
3953 parser.error('only one of --message and --message-file allowed.')
3954 options.message = gclient_utils.FileRead(options.message_file)
3955 options.message_file = None
3956
tandrii4d0545a2016-07-06 03:56:49 -07003957 if options.cq_dry_run and options.use_commit_queue:
3958 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3959
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003960 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3961 settings.GetIsGerrit()
3962
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003963 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003964 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003965
3966
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003967def IsSubmoduleMergeCommit(ref):
3968 # When submodules are added to the repo, we expect there to be a single
3969 # non-git-svn merge commit at remote HEAD with a signature comment.
3970 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003971 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003972 return RunGit(cmd) != ''
3973
3974
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003975def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003976 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003978 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3979 upstream and closes the issue automatically and atomically.
3980
3981 Otherwise (in case of Rietveld):
3982 Squashes branch into a single commit.
3983 Updates changelog with metadata (e.g. pointer to review).
3984 Pushes/dcommits the code upstream.
3985 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986 """
3987 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3988 help='bypass upload presubmit hook')
3989 parser.add_option('-m', dest='message',
3990 help="override review description")
3991 parser.add_option('-f', action='store_true', dest='force',
3992 help="force yes to questions (don't prompt)")
3993 parser.add_option('-c', dest='contributor',
3994 help="external contributor for patch (appended to " +
3995 "description and used as author for git). Should be " +
3996 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003997 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003998 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004000 auth_config = auth.extract_auth_config_from_options(options)
4001
4002 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004004 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4005 if cl.IsGerrit():
4006 if options.message:
4007 # This could be implemented, but it requires sending a new patch to
4008 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4009 # Besides, Gerrit has the ability to change the commit message on submit
4010 # automatically, thus there is no need to support this option (so far?).
4011 parser.error('-m MESSAGE option is not supported for Gerrit.')
4012 if options.contributor:
4013 parser.error(
4014 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4015 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4016 'the contributor\'s "name <email>". If you can\'t upload such a '
4017 'commit for review, contact your repository admin and request'
4018 '"Forge-Author" permission.')
4019 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4020 options.verbose)
4021
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004022 current = cl.GetBranch()
4023 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4024 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004025 print()
4026 print('Attempting to push branch %r into another local branch!' % current)
4027 print()
4028 print('Either reparent this branch on top of origin/master:')
4029 print(' git reparent-branch --root')
4030 print()
4031 print('OR run `git rebase-update` if you think the parent branch is ')
4032 print('already committed.')
4033 print()
4034 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004035 return 1
4036
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004037 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038 # Default to merging against our best guess of the upstream branch.
4039 args = [cl.GetUpstreamBranch()]
4040
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004041 if options.contributor:
4042 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004043 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004044 return 1
4045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004046 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004047 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
sbc@chromium.org71437c02015-04-09 19:29:40 +00004049 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 return 1
4051
4052 # This rev-list syntax means "show all commits not in my branch that
4053 # are in base_branch".
4054 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4055 base_branch]).splitlines()
4056 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004057 print('Base branch "%s" has %d commits '
4058 'not in this branch.' % (base_branch, len(upstream_commits)))
4059 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060 return 1
4061
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004062 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004063 svn_head = None
4064 if cmd == 'dcommit' or base_has_submodules:
4065 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4066 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004069 # If the base_head is a submodule merge commit, the first parent of the
4070 # base_head should be a git-svn commit, which is what we're interested in.
4071 base_svn_head = base_branch
4072 if base_has_submodules:
4073 base_svn_head += '^1'
4074
4075 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('This branch has %d additional commits not upstreamed yet.'
4078 % len(extra_commits.splitlines()))
4079 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4080 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004081 return 1
4082
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004083 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004084 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004085 author = None
4086 if options.contributor:
4087 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004088 hook_results = cl.RunHook(
4089 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004090 may_prompt=not options.force,
4091 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004092 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004093 if not hook_results.should_continue():
4094 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004096 # Check the tree status if the tree status URL is set.
4097 status = GetTreeStatus()
4098 if 'closed' == status:
4099 print('The tree is closed. Please wait for it to reopen. Use '
4100 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4101 return 1
4102 elif 'unknown' == status:
4103 print('Unable to determine tree status. Please verify manually and '
4104 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4105 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004106
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004107 change_desc = ChangeDescription(options.message)
4108 if not change_desc.description and cl.GetIssue():
4109 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004110
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004111 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004112 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004113 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004114 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004115 print('No description set.')
4116 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004117 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004119 # Keep a separate copy for the commit message, because the commit message
4120 # contains the link to the Rietveld issue, while the Rietveld message contains
4121 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004122 # Keep a separate copy for the commit message.
4123 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004124 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004125
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004126 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004127 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004128 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004129 # after it. Add a period on a new line to circumvent this. Also add a space
4130 # before the period to make sure that Gitiles continues to correctly resolve
4131 # the URL.
4132 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004133 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004134 commit_desc.append_footer('Patch from %s.' % options.contributor)
4135
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004136 print('Description:')
4137 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004139 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004140 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004141 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004143 # We want to squash all this branch's commits into one commit with the proper
4144 # description. We do this by doing a "reset --soft" to the base branch (which
4145 # keeps the working copy the same), then dcommitting that. If origin/master
4146 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4147 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004148 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004149 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4150 # Delete the branches if they exist.
4151 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4152 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4153 result = RunGitWithCode(showref_cmd)
4154 if result[0] == 0:
4155 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004156
4157 # We might be in a directory that's present in this branch but not in the
4158 # trunk. Move up to the top of the tree so that git commands that expect a
4159 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004160 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004161 if rel_base_path:
4162 os.chdir(rel_base_path)
4163
4164 # Stuff our change into the merge branch.
4165 # We wrap in a try...finally block so if anything goes wrong,
4166 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004167 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004168 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004169 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004170 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004172 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004173 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004174 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004175 RunGit(
4176 [
4177 'commit', '--author', options.contributor,
4178 '-m', commit_desc.description,
4179 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004181 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004182 if base_has_submodules:
4183 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4184 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4185 RunGit(['checkout', CHERRY_PICK_BRANCH])
4186 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004187 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004188 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004189 mirror = settings.GetGitMirror(remote)
4190 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004191 pending_prefix = settings.GetPendingRefPrefix()
4192 if not pending_prefix or branch.startswith(pending_prefix):
4193 # If not using refs/pending/heads/* at all, or target ref is already set
4194 # to pending, then push to the target ref directly.
4195 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004196 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004197 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004198 else:
4199 # Cherry-pick the change on top of pending ref and then push it.
4200 assert branch.startswith('refs/'), branch
4201 assert pending_prefix[-1] == '/', pending_prefix
4202 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004203 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004204 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004205 if retcode == 0:
4206 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207 else:
4208 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004209 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004210 'svn', 'dcommit',
4211 '-C%s' % options.similarity,
4212 '--no-rebase', '--rmdir',
4213 ]
4214 if settings.GetForceHttpsCommitUrl():
4215 # Allow forcing https commit URLs for some projects that don't allow
4216 # committing to http URLs (like Google Code).
4217 remote_url = cl.GetGitSvnRemoteUrl()
4218 if urlparse.urlparse(remote_url).scheme == 'http':
4219 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004220 cmd_args.append('--commit-url=%s' % remote_url)
4221 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004222 if 'Committed r' in output:
4223 revision = re.match(
4224 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4225 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226 finally:
4227 # And then swap back to the original branch and clean up.
4228 RunGit(['checkout', '-q', cl.GetBranch()])
4229 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004230 if base_has_submodules:
4231 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004232
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004233 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004234 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004235 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004236
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004237 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004238 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004239 try:
4240 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4241 # We set pushed_to_pending to False, since it made it all the way to the
4242 # real ref.
4243 pushed_to_pending = False
4244 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004245 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004247 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004248 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004250 if not to_pending:
4251 if viewvc_url and revision:
4252 change_desc.append_footer(
4253 'Committed: %s%s' % (viewvc_url, revision))
4254 elif revision:
4255 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004256 print('Closing issue '
4257 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004258 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004259 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004260 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004261 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004262 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004263 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004264 if options.bypass_hooks:
4265 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4266 else:
4267 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004268 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004269
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004270 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004271 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004272 print('The commit is in the pending queue (%s).' % pending_ref)
4273 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4274 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004275
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004276 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4277 if os.path.isfile(hook):
4278 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004279
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004280 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281
4282
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004283def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004284 print()
4285 print('Waiting for commit to be landed on %s...' % real_ref)
4286 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004287 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4288 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004289 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004290
4291 loop = 0
4292 while True:
4293 sys.stdout.write('fetching (%d)... \r' % loop)
4294 sys.stdout.flush()
4295 loop += 1
4296
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004297 if mirror:
4298 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004299 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4300 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4301 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4302 for commit in commits.splitlines():
4303 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004305 return commit
4306
4307 current_rev = to_rev
4308
4309
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004310def PushToGitPending(remote, pending_ref, upstream_ref):
4311 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4312
4313 Returns:
4314 (retcode of last operation, output log of last operation).
4315 """
4316 assert pending_ref.startswith('refs/'), pending_ref
4317 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4318 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4319 code = 0
4320 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004321 max_attempts = 3
4322 attempts_left = max_attempts
4323 while attempts_left:
4324 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004325 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004326 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004327
4328 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004329 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004330 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004331 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004332 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004334 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004335 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004336 continue
4337
4338 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004339 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004340 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004341 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004342 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004343 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4344 'the following files have merge conflicts:' % pending_ref)
4345 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4346 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004347 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004348 return code, out
4349
4350 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004351 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004352 code, out = RunGitWithCode(
4353 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4354 if code == 0:
4355 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004357 return code, out
4358
vapiera7fbd5a2016-06-16 09:17:49 -07004359 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004360 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004361 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004362 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004363 print('Fatal push error. Make sure your .netrc credentials and git '
4364 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004365 return code, out
4366
vapiera7fbd5a2016-06-16 09:17:49 -07004367 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004368 return code, out
4369
4370
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004371def IsFatalPushFailure(push_stdout):
4372 """True if retrying push won't help."""
4373 return '(prohibited by Gerrit)' in push_stdout
4374
4375
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004376@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004377def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004378 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004379 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004380 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004381 # If it looks like previous commits were mirrored with git-svn.
4382 message = """This repository appears to be a git-svn mirror, but no
4383upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4384 else:
4385 message = """This doesn't appear to be an SVN repository.
4386If your project has a true, writeable git repository, you probably want to run
4387'git cl land' instead.
4388If your project has a git mirror of an upstream SVN master, you probably need
4389to run 'git svn init'.
4390
4391Using the wrong command might cause your commit to appear to succeed, and the
4392review to be closed, without actually landing upstream. If you choose to
4393proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004394 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004395 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004396 # TODO(tandrii): kill this post SVN migration with
4397 # https://codereview.chromium.org/2076683002
4398 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4399 'Please let us know of this project you are committing to:'
4400 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401 return SendUpstream(parser, args, 'dcommit')
4402
4403
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004404@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004405def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004406 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004407 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004408 print('This appears to be an SVN repository.')
4409 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004410 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004411 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004412 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004413
4414
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004415@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004416def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004417 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418 parser.add_option('-b', dest='newbranch',
4419 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004420 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004421 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004422 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4423 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004424 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004425 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004426 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004427 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004428 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004429 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004430
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004431
4432 group = optparse.OptionGroup(
4433 parser,
4434 'Options for continuing work on the current issue uploaded from a '
4435 'different clone (e.g. different machine). Must be used independently '
4436 'from the other options. No issue number should be specified, and the '
4437 'branch must have an issue number associated with it')
4438 group.add_option('--reapply', action='store_true', dest='reapply',
4439 help='Reset the branch and reapply the issue.\n'
4440 'CAUTION: This will undo any local changes in this '
4441 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004442
4443 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004444 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004445 parser.add_option_group(group)
4446
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004447 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004448 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004449 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004450 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004451 auth_config = auth.extract_auth_config_from_options(options)
4452
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004453
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004454 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004455 if options.newbranch:
4456 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004457 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004458 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004459
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004460 cl = Changelist(auth_config=auth_config,
4461 codereview=options.forced_codereview)
4462 if not cl.GetIssue():
4463 parser.error('current branch must have an associated issue')
4464
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004465 upstream = cl.GetUpstreamBranch()
4466 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004467 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004468
4469 RunGit(['reset', '--hard', upstream])
4470 if options.pull:
4471 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004472
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004473 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4474 options.directory)
4475
4476 if len(args) != 1 or not args[0]:
4477 parser.error('Must specify issue number or url')
4478
4479 # We don't want uncommitted changes mixed up with the patch.
4480 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004481 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004483 if options.newbranch:
4484 if options.force:
4485 RunGit(['branch', '-D', options.newbranch],
4486 stderr=subprocess2.PIPE, error_ok=True)
4487 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004488 elif not GetCurrentBranch():
4489 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004490
4491 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4492
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004493 if cl.IsGerrit():
4494 if options.reject:
4495 parser.error('--reject is not supported with Gerrit codereview.')
4496 if options.nocommit:
4497 parser.error('--nocommit is not supported with Gerrit codereview.')
4498 if options.directory:
4499 parser.error('--directory is not supported with Gerrit codereview.')
4500
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004501 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004502 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004503
4504
4505def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004506 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004507 # Provide a wrapper for git svn rebase to help avoid accidental
4508 # git svn dcommit.
4509 # It's the only command that doesn't use parser at all since we just defer
4510 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004511
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004512 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004513
4514
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004515def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004516 """Fetches the tree status and returns either 'open', 'closed',
4517 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004518 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004519 if url:
4520 status = urllib2.urlopen(url).read().lower()
4521 if status.find('closed') != -1 or status == '0':
4522 return 'closed'
4523 elif status.find('open') != -1 or status == '1':
4524 return 'open'
4525 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526 return 'unset'
4527
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004528
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529def GetTreeStatusReason():
4530 """Fetches the tree status from a json url and returns the message
4531 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004532 url = settings.GetTreeStatusUrl()
4533 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004534 connection = urllib2.urlopen(json_url)
4535 status = json.loads(connection.read())
4536 connection.close()
4537 return status['message']
4538
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004539
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004540def GetBuilderMaster(bot_list):
4541 """For a given builder, fetch the master from AE if available."""
4542 map_url = 'https://builders-map.appspot.com/'
4543 try:
4544 master_map = json.load(urllib2.urlopen(map_url))
4545 except urllib2.URLError as e:
4546 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4547 (map_url, e))
4548 except ValueError as e:
4549 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4550 if not master_map:
4551 return None, 'Failed to build master map.'
4552
4553 result_master = ''
4554 for bot in bot_list:
4555 builder = bot.split(':', 1)[0]
4556 master_list = master_map.get(builder, [])
4557 if not master_list:
4558 return None, ('No matching master for builder %s.' % builder)
4559 elif len(master_list) > 1:
4560 return None, ('The builder name %s exists in multiple masters %s.' %
4561 (builder, master_list))
4562 else:
4563 cur_master = master_list[0]
4564 if not result_master:
4565 result_master = cur_master
4566 elif result_master != cur_master:
4567 return None, 'The builders do not belong to the same master.'
4568 return result_master, None
4569
4570
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004571def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004572 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004573 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004574 status = GetTreeStatus()
4575 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004577 return 2
4578
vapiera7fbd5a2016-06-16 09:17:49 -07004579 print('The tree is %s' % status)
4580 print()
4581 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582 if status != 'open':
4583 return 1
4584 return 0
4585
4586
maruel@chromium.org15192402012-09-06 12:38:29 +00004587def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004588 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004589 group = optparse.OptionGroup(parser, "Try job options")
4590 group.add_option(
4591 "-b", "--bot", action="append",
4592 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4593 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004594 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004595 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004596 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004597 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004598 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004599 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004600 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004601 "-r", "--revision",
4602 help="Revision to use for the try job; default: the "
4603 "revision will be determined by the try server; see "
4604 "its waterfall for more info")
4605 group.add_option(
4606 "-c", "--clobber", action="store_true", default=False,
4607 help="Force a clobber before building; e.g. don't do an "
4608 "incremental build")
4609 group.add_option(
4610 "--project",
4611 help="Override which project to use. Projects are defined "
4612 "server-side to define what default bot set to use")
4613 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004614 "-p", "--property", dest="properties", action="append", default=[],
4615 help="Specify generic properties in the form -p key1=value1 -p "
4616 "key2=value2 etc (buildbucket only). The value will be treated as "
4617 "json if decodable, or as string otherwise.")
4618 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004619 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004620 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004621 "--use-rietveld", action="store_true", default=False,
4622 help="Use Rietveld to trigger try jobs.")
4623 group.add_option(
4624 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4625 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004626 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004627 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004628 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004629 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004630
machenbach@chromium.org45453142015-09-15 08:45:22 +00004631 if options.use_rietveld and options.properties:
4632 parser.error('Properties can only be specified with buildbucket')
4633
4634 # Make sure that all properties are prop=value pairs.
4635 bad_params = [x for x in options.properties if '=' not in x]
4636 if bad_params:
4637 parser.error('Got properties with missing "=": %s' % bad_params)
4638
maruel@chromium.org15192402012-09-06 12:38:29 +00004639 if args:
4640 parser.error('Unknown arguments: %s' % args)
4641
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004642 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004643 if not cl.GetIssue():
4644 parser.error('Need to upload first')
4645
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004646 if cl.IsGerrit():
4647 parser.error(
4648 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4649 'If your project has Commit Queue, dry run is a workaround:\n'
4650 ' git cl set-commit --dry-run')
4651 # Code below assumes Rietveld issue.
4652 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4653
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004654 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004655 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004656 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004657
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004658 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004659 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004660
maruel@chromium.org15192402012-09-06 12:38:29 +00004661 if not options.name:
4662 options.name = cl.GetBranch()
4663
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004664 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004665 options.master, err_msg = GetBuilderMaster(options.bot)
4666 if err_msg:
4667 parser.error('Tryserver master cannot be found because: %s\n'
4668 'Please manually specify the tryserver master'
4669 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004670
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004671 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004672 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004673 if not options.bot:
4674 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004675
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004676 # Get try masters from PRESUBMIT.py files.
4677 masters = presubmit_support.DoGetTryMasters(
4678 change,
4679 change.LocalPaths(),
4680 settings.GetRoot(),
4681 None,
4682 None,
4683 options.verbose,
4684 sys.stdout)
4685 if masters:
4686 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004687
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004688 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4689 options.bot = presubmit_support.DoGetTrySlaves(
4690 change,
4691 change.LocalPaths(),
4692 settings.GetRoot(),
4693 None,
4694 None,
4695 options.verbose,
4696 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004697
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004698 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004699 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004700
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004701 builders_and_tests = {}
4702 # TODO(machenbach): The old style command-line options don't support
4703 # multiple try masters yet.
4704 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4705 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4706
4707 for bot in old_style:
4708 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004709 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004710 elif ',' in bot:
4711 parser.error('Specify one bot per --bot flag')
4712 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004713 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004714
4715 for bot, tests in new_style:
4716 builders_and_tests.setdefault(bot, []).extend(tests)
4717
4718 # Return a master map with one master to be backwards compatible. The
4719 # master name defaults to an empty string, which will cause the master
4720 # not to be set on rietveld (deprecated).
4721 return {options.master: builders_and_tests}
4722
4723 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004724 if not masters:
4725 # Default to triggering Dry Run (see http://crbug.com/625697).
4726 if options.verbose:
4727 print('git cl try with no bots now defaults to CQ Dry Run.')
4728 try:
4729 cl.SetCQState(_CQState.DRY_RUN)
4730 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4731 return 0
4732 except KeyboardInterrupt:
4733 raise
4734 except:
4735 print('WARNING: failed to trigger CQ Dry Run.\n'
4736 'Either:\n'
4737 ' * your project has no CQ\n'
4738 ' * you don\'t have permission to trigger Dry Run\n'
4739 ' * bug in this code (see stack trace below).\n'
4740 'Consider specifying which bots to trigger manually '
4741 'or asking your project owners for permissions '
4742 'or contacting Chrome Infrastructure team at '
4743 'https://www.chromium.org/infra\n\n')
4744 # Still raise exception so that stack trace is printed.
4745 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004746
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004747 for builders in masters.itervalues():
4748 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004749 print('ERROR You are trying to send a job to a triggered bot. This type '
4750 'of bot requires an\ninitial job from a parent (usually a builder).'
4751 ' Instead send your job to the parent.\n'
4752 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004753 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004754
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004755 patchset = cl.GetMostRecentPatchset()
4756 if patchset and patchset != cl.GetPatchset():
4757 print(
4758 '\nWARNING Mismatch between local config and server. Did a previous '
4759 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4760 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004761 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004762 try:
4763 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4764 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004765 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004766 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004767 except Exception as e:
4768 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004769 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004770 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004771 return 1
4772 else:
4773 try:
4774 cl.RpcServer().trigger_distributed_try_jobs(
4775 cl.GetIssue(), patchset, options.name, options.clobber,
4776 options.revision, masters)
4777 except urllib2.HTTPError as e:
4778 if e.code == 404:
4779 print('404 from rietveld; '
4780 'did you mean to use "git try" instead of "git cl try"?')
4781 return 1
4782 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004783
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004784 for (master, builders) in sorted(masters.iteritems()):
4785 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004786 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004787 length = max(len(builder) for builder in builders)
4788 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004789 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004790 return 0
4791
4792
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004793def CMDtry_results(parser, args):
4794 group = optparse.OptionGroup(parser, "Try job results options")
4795 group.add_option(
4796 "-p", "--patchset", type=int, help="patchset number if not current.")
4797 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004798 "--print-master", action='store_true', help="print master name as well.")
4799 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004800 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004801 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004802 group.add_option(
4803 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4804 help="Host of buildbucket. The default host is %default.")
qyearsley53f48a12016-09-01 10:45:13 -07004805 group.add_option(
4806 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004807 parser.add_option_group(group)
4808 auth.add_auth_options(parser)
4809 options, args = parser.parse_args(args)
4810 if args:
4811 parser.error('Unrecognized args: %s' % ' '.join(args))
4812
4813 auth_config = auth.extract_auth_config_from_options(options)
4814 cl = Changelist(auth_config=auth_config)
4815 if not cl.GetIssue():
4816 parser.error('Need to upload first')
4817
4818 if not options.patchset:
4819 options.patchset = cl.GetMostRecentPatchset()
4820 if options.patchset and options.patchset != cl.GetPatchset():
4821 print(
4822 '\nWARNING Mismatch between local config and server. Did a previous '
4823 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4824 'Continuing using\npatchset %s.\n' % options.patchset)
4825 try:
4826 jobs = fetch_try_jobs(auth_config, cl, options)
4827 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004828 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004829 return 1
4830 except Exception as e:
4831 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004832 print('ERROR: Exception when trying to fetch try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004833 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004834 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004835 if options.json:
4836 write_try_results_json(options.json, jobs)
4837 else:
4838 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004839 return 0
4840
4841
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004842@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004843def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004844 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004845 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004846 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004847 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004849 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004850 if args:
4851 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004852 branch = cl.GetBranch()
4853 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004854 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004855 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004856
4857 # Clear configured merge-base, if there is one.
4858 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004859 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004860 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004861 return 0
4862
4863
thestig@chromium.org00858c82013-12-02 23:08:03 +00004864def CMDweb(parser, args):
4865 """Opens the current CL in the web browser."""
4866 _, args = parser.parse_args(args)
4867 if args:
4868 parser.error('Unrecognized args: %s' % ' '.join(args))
4869
4870 issue_url = Changelist().GetIssueURL()
4871 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004872 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004873 return 1
4874
4875 webbrowser.open(issue_url)
4876 return 0
4877
4878
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004879def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004880 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004881 parser.add_option('-d', '--dry-run', action='store_true',
4882 help='trigger in dry run mode')
4883 parser.add_option('-c', '--clear', action='store_true',
4884 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004885 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004886 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004887 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004888 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004889 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004890 if args:
4891 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004892 if options.dry_run and options.clear:
4893 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4894
iannuccie53c9352016-08-17 14:40:40 -07004895 cl = Changelist(auth_config=auth_config, issue=options.issue,
4896 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004897 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004898 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004899 elif options.dry_run:
4900 state = _CQState.DRY_RUN
4901 else:
4902 state = _CQState.COMMIT
4903 if not cl.GetIssue():
4904 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004905 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004906 return 0
4907
4908
groby@chromium.org411034a2013-02-26 15:12:01 +00004909def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004910 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004911 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004912 auth.add_auth_options(parser)
4913 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004914 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004915 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004916 if args:
4917 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004918 cl = Changelist(auth_config=auth_config, issue=options.issue,
4919 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004920 # Ensure there actually is an issue to close.
4921 cl.GetDescription()
4922 cl.CloseIssue()
4923 return 0
4924
4925
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004926def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004927 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004928 parser.add_option(
4929 '--stat',
4930 action='store_true',
4931 dest='stat',
4932 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004933 auth.add_auth_options(parser)
4934 options, args = parser.parse_args(args)
4935 auth_config = auth.extract_auth_config_from_options(options)
4936 if args:
4937 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004938
4939 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004940 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004941 # Staged changes would be committed along with the patch from last
4942 # upload, hence counted toward the "last upload" side in the final
4943 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004944 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004945 return 1
4946
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004947 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004948 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004949 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004950 if not issue:
4951 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004952 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004953 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004954
4955 # Create a new branch based on the merge-base
4956 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004957 # Clear cached branch in cl object, to avoid overwriting original CL branch
4958 # properties.
4959 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004960 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004961 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004962 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004963 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004964 return rtn
4965
wychen@chromium.org06928532015-02-03 02:11:29 +00004966 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004967 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004968 cmd = ['git', 'diff']
4969 if options.stat:
4970 cmd.append('--stat')
4971 cmd.extend([TMP_BRANCH, branch, '--'])
4972 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004973 finally:
4974 RunGit(['checkout', '-q', branch])
4975 RunGit(['branch', '-D', TMP_BRANCH])
4976
4977 return 0
4978
4979
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004980def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004981 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004982 parser.add_option(
4983 '--no-color',
4984 action='store_true',
4985 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004986 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004987 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004988 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004989
4990 author = RunGit(['config', 'user.email']).strip() or None
4991
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004992 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004993
4994 if args:
4995 if len(args) > 1:
4996 parser.error('Unknown args')
4997 base_branch = args[0]
4998 else:
4999 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005000 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005001
5002 change = cl.GetChange(base_branch, None)
5003 return owners_finder.OwnersFinder(
5004 [f.LocalPath() for f in
5005 cl.GetChange(base_branch, None).AffectedFiles()],
5006 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005007 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005008 disable_color=options.no_color).run()
5009
5010
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005011def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005012 """Generates a diff command."""
5013 # Generate diff for the current branch's changes.
5014 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5015 upstream_commit, '--' ]
5016
5017 if args:
5018 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005019 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005020 diff_cmd.append(arg)
5021 else:
5022 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005023
5024 return diff_cmd
5025
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005026def MatchingFileType(file_name, extensions):
5027 """Returns true if the file name ends with one of the given extensions."""
5028 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005029
enne@chromium.org555cfe42014-01-29 18:21:39 +00005030@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005031def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005032 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005033 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005034 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005035 parser.add_option('--full', action='store_true',
5036 help='Reformat the full content of all touched files')
5037 parser.add_option('--dry-run', action='store_true',
5038 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005039 parser.add_option('--python', action='store_true',
5040 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005041 parser.add_option('--diff', action='store_true',
5042 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005043 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005044
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005045 # git diff generates paths against the root of the repository. Change
5046 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005047 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005048 if rel_base_path:
5049 os.chdir(rel_base_path)
5050
digit@chromium.org29e47272013-05-17 17:01:46 +00005051 # Grab the merge-base commit, i.e. the upstream commit of the current
5052 # branch when it was created or the last time it was rebased. This is
5053 # to cover the case where the user may have called "git fetch origin",
5054 # moving the origin branch to a newer commit, but hasn't rebased yet.
5055 upstream_commit = None
5056 cl = Changelist()
5057 upstream_branch = cl.GetUpstreamBranch()
5058 if upstream_branch:
5059 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5060 upstream_commit = upstream_commit.strip()
5061
5062 if not upstream_commit:
5063 DieWithError('Could not find base commit for this branch. '
5064 'Are you in detached state?')
5065
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005066 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5067 diff_output = RunGit(changed_files_cmd)
5068 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005069 # Filter out files deleted by this CL
5070 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005071
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005072 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5073 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5074 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005075 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005076
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005077 top_dir = os.path.normpath(
5078 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5079
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005080 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5081 # formatted. This is used to block during the presubmit.
5082 return_value = 0
5083
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005084 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005085 # Locate the clang-format binary in the checkout
5086 try:
5087 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005088 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005089 DieWithError(e)
5090
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005091 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005092 cmd = [clang_format_tool]
5093 if not opts.dry_run and not opts.diff:
5094 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005095 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005096 if opts.diff:
5097 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005098 else:
5099 env = os.environ.copy()
5100 env['PATH'] = str(os.path.dirname(clang_format_tool))
5101 try:
5102 script = clang_format.FindClangFormatScriptInChromiumTree(
5103 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005104 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005105 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005106
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005107 cmd = [sys.executable, script, '-p0']
5108 if not opts.dry_run and not opts.diff:
5109 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005110
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005111 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5112 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005113
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005114 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5115 if opts.diff:
5116 sys.stdout.write(stdout)
5117 if opts.dry_run and len(stdout) > 0:
5118 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005119
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005120 # Similar code to above, but using yapf on .py files rather than clang-format
5121 # on C/C++ files
5122 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005123 yapf_tool = gclient_utils.FindExecutable('yapf')
5124 if yapf_tool is None:
5125 DieWithError('yapf not found in PATH')
5126
5127 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005129 cmd = [yapf_tool]
5130 if not opts.dry_run and not opts.diff:
5131 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005132 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005133 if opts.diff:
5134 sys.stdout.write(stdout)
5135 else:
5136 # TODO(sbc): yapf --lines mode still has some issues.
5137 # https://github.com/google/yapf/issues/154
5138 DieWithError('--python currently only works with --full')
5139
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005140 # Dart's formatter does not have the nice property of only operating on
5141 # modified chunks, so hard code full.
5142 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005143 try:
5144 command = [dart_format.FindDartFmtToolInChromiumTree()]
5145 if not opts.dry_run and not opts.diff:
5146 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005147 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005148
ppi@chromium.org6593d932016-03-03 15:41:15 +00005149 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005150 if opts.dry_run and stdout:
5151 return_value = 2
5152 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005153 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5154 'found in this checkout. Files in other languages are still '
5155 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005156
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005157 # Format GN build files. Always run on full build files for canonical form.
5158 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005159 cmd = ['gn', 'format' ]
5160 if opts.dry_run or opts.diff:
5161 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005162 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005163 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5164 shell=sys.platform == 'win32',
5165 cwd=top_dir)
5166 if opts.dry_run and gn_ret == 2:
5167 return_value = 2 # Not formatted.
5168 elif opts.diff and gn_ret == 2:
5169 # TODO this should compute and print the actual diff.
5170 print("This change has GN build file diff for " + gn_diff_file)
5171 elif gn_ret != 0:
5172 # For non-dry run cases (and non-2 return values for dry-run), a
5173 # nonzero error code indicates a failure, probably because the file
5174 # doesn't parse.
5175 DieWithError("gn format failed on " + gn_diff_file +
5176 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005177
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005178 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005179
5180
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005181@subcommand.usage('<codereview url or issue id>')
5182def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005183 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005184 _, args = parser.parse_args(args)
5185
5186 if len(args) != 1:
5187 parser.print_help()
5188 return 1
5189
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005190 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005191 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005192 parser.print_help()
5193 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005194 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005195
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005196 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005197 output = RunGit(['config', '--local', '--get-regexp',
5198 r'branch\..*\.%s' % issueprefix],
5199 error_ok=True)
5200 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005201 if issue == target_issue:
5202 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005203
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005204 branches = []
5205 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005206 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005207 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005208 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005209 return 1
5210 if len(branches) == 1:
5211 RunGit(['checkout', branches[0]])
5212 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005213 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005214 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005215 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005216 which = raw_input('Choose by index: ')
5217 try:
5218 RunGit(['checkout', branches[int(which)]])
5219 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005220 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005221 return 1
5222
5223 return 0
5224
5225
maruel@chromium.org29404b52014-09-08 22:58:00 +00005226def CMDlol(parser, args):
5227 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005228 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005229 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5230 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5231 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005232 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005233 return 0
5234
5235
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005236class OptionParser(optparse.OptionParser):
5237 """Creates the option parse and add --verbose support."""
5238 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005239 optparse.OptionParser.__init__(
5240 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005241 self.add_option(
5242 '-v', '--verbose', action='count', default=0,
5243 help='Use 2 times for more debugging info')
5244
5245 def parse_args(self, args=None, values=None):
5246 options, args = optparse.OptionParser.parse_args(self, args, values)
5247 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5248 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5249 return options, args
5250
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005251
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005252def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005253 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005254 print('\nYour python version %s is unsupported, please upgrade.\n' %
5255 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005256 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005257
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005258 # Reload settings.
5259 global settings
5260 settings = Settings()
5261
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005262 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005263 dispatcher = subcommand.CommandDispatcher(__name__)
5264 try:
5265 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005266 except auth.AuthenticationError as e:
5267 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005268 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005269 if e.code != 500:
5270 raise
5271 DieWithError(
5272 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5273 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005274 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005275
5276
5277if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005278 # These affect sys.stdout so do it outside of main() to simplify mocks in
5279 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005280 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005281 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005282 try:
5283 sys.exit(main(sys.argv[1:]))
5284 except KeyboardInterrupt:
5285 sys.stderr.write('interrupted\n')
5286 sys.exit(1)