blob: 7913e989d0345483b70b65e303a711adcf5365fd [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
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000016import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000024import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000027import time
28import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000037 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000045from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000068DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000069POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000071GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
87
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070089 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000090 sys.exit(1)
91
92
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000093def GetNoGitPagerEnv():
94 env = os.environ.copy()
95 # 'cat' is a magical git string that disables pagers on all platforms.
96 env['GIT_PAGER'] = 'cat'
97 return env
98
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000099
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000102 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000103 except subprocess2.CalledProcessError as e:
104 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 'Command "%s" failed.\n%s' % (
108 ' '.join(args), error_message or e.stdout or ''))
109 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
112def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000114 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115
116
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000118 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000119 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000124 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000125 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000126 stdout=subprocess2.PIPE,
127 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000128 return code, out[0]
129 except ValueError:
130 # When the subprocess fails, it returns None. That triggers a ValueError
131 # when trying to unpack the return value into (out, code).
132 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
maruel@chromium.org90541732011-04-01 17:54:18 +0000154def ask_for_data(prompt):
155 try:
156 return raw_input(prompt)
157 except KeyboardInterrupt:
158 # Hide the exception.
159 sys.exit(1)
160
161
iannucci@chromium.org79540052012-10-19 23:15:26 +0000162def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000163 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000164 if not branch:
165 return
166
167 cmd = ['config']
168 if isinstance(value, int):
169 cmd.append('--int')
170 git_key = 'branch.%s.%s' % (branch, key)
171 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172
173
174def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000175 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 if branch:
177 git_key = 'branch.%s.%s' % (branch, key)
178 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
179 try:
180 return int(stdout.strip())
181 except ValueError:
182 pass
183 return default
184
185
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186def add_git_similarity(parser):
187 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000189 help='Sets the percentage that a pair of files need to match in order to'
190 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000191 parser.add_option(
192 '--find-copies', action='store_true',
193 help='Allows git to look for copies.')
194 parser.add_option(
195 '--no-find-copies', action='store_false', dest='find_copies',
196 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197
198 old_parser_args = parser.parse_args
199 def Parse(args):
200 options, args = old_parser_args(args)
201
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 print('Note: Saving similarity of %d%% in git config.'
206 % options.similarity)
207 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000208
iannucci@chromium.org79540052012-10-19 23:15:26 +0000209 options.similarity = max(0, min(options.similarity, 100))
210
211 if options.find_copies is None:
212 options.find_copies = bool(
213 git_get_branch_default('git-find-copies', True))
214 else:
215 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000216
217 print('Using %d%% similarity for rename/copy detection. '
218 'Override with --similarity.' % options.similarity)
219
220 return options, args
221 parser.parse_args = Parse
222
223
machenbach@chromium.org45453142015-09-15 08:45:22 +0000224def _get_properties_from_options(options):
225 properties = dict(x.split('=', 1) for x in options.properties)
226 for key, val in properties.iteritems():
227 try:
228 properties[key] = json.loads(val)
229 except ValueError:
230 pass # If a value couldn't be evaluated, treat it as a string.
231 return properties
232
233
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000234def _prefix_master(master):
235 """Convert user-specified master name to full master name.
236
237 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
238 name, while the developers always use shortened master name
239 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
240 function does the conversion for buildbucket migration.
241 """
242 prefix = 'master.'
243 if master.startswith(prefix):
244 return master
245 return '%s%s' % (prefix, master)
246
247
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000248def _buildbucket_retry(operation_name, http, *args, **kwargs):
249 """Retries requests to buildbucket service and returns parsed json content."""
250 try_count = 0
251 while True:
252 response, content = http.request(*args, **kwargs)
253 try:
254 content_json = json.loads(content)
255 except ValueError:
256 content_json = None
257
258 # Buildbucket could return an error even if status==200.
259 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000260 error = content_json.get('error')
261 if error.get('code') == 403:
262 raise BuildbucketResponseException(
263 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000265 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000266 raise BuildbucketResponseException(msg)
267
268 if response.status == 200:
269 if not content_json:
270 raise BuildbucketResponseException(
271 'Buildbucket returns invalid json content: %s.\n'
272 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
273 content)
274 return content_json
275 if response.status < 500 or try_count >= 2:
276 raise httplib2.HttpLib2Error(content)
277
278 # status >= 500 means transient failures.
279 logging.debug('Transient errors when %s. Will retry.', operation_name)
280 time.sleep(0.5 + 1.5*try_count)
281 try_count += 1
282 assert False, 'unreachable'
283
284
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000285def trigger_luci_job(changelist, masters, options):
286 """Send a job to run on LUCI."""
287 issue_props = changelist.GetIssueProperties()
288 issue = changelist.GetIssue()
289 patchset = changelist.GetMostRecentPatchset()
290 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000291 # TODO(hinoka et al): add support for other properties.
292 # Currently, this completely ignores testfilter and other properties.
293 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000294 luci_trigger.trigger(
295 builder, 'HEAD', issue, patchset, issue_props['project'])
296
297
machenbach@chromium.org45453142015-09-15 08:45:22 +0000298def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000299 rietveld_url = settings.GetDefaultServerUrl()
300 rietveld_host = urlparse.urlparse(rietveld_url).hostname
301 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
302 http = authenticator.authorize(httplib2.Http())
303 http.force_exception_to_status_code = True
304 issue_props = changelist.GetIssueProperties()
305 issue = changelist.GetIssue()
306 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308
309 buildbucket_put_url = (
310 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000311 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000312 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
313 hostname=rietveld_host,
314 issue=issue,
315 patch=patchset)
316
317 batch_req_body = {'builds': []}
318 print_text = []
319 print_text.append('Tried jobs on:')
320 for master, builders_and_tests in sorted(masters.iteritems()):
321 print_text.append('Master: %s' % master)
322 bucket = _prefix_master(master)
323 for builder, tests in sorted(builders_and_tests.iteritems()):
324 print_text.append(' %s: %s' % (builder, tests))
325 parameters = {
326 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000327 'changes': [{
328 'author': {'email': issue_props['owner_email']},
329 'revision': options.revision,
330 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331 'properties': {
332 'category': category,
333 'issue': issue,
334 'master': master,
335 'patch_project': issue_props['project'],
336 'patch_storage': 'rietveld',
337 'patchset': patchset,
338 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000340 },
341 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000342 if 'presubmit' in builder.lower():
343 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000344 if tests:
345 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000346 if properties:
347 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000348 if options.clobber:
349 parameters['properties']['clobber'] = True
350 batch_req_body['builds'].append(
351 {
352 'bucket': bucket,
353 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000354 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000355 'tags': ['builder:%s' % builder,
356 'buildset:%s' % buildset,
357 'master:%s' % master,
358 'user_agent:git_cl_try']
359 }
360 )
361
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 _buildbucket_retry(
363 'triggering tryjobs',
364 http,
365 buildbucket_put_url,
366 'PUT',
367 body=json.dumps(batch_req_body),
368 headers={'Content-Type': 'application/json'}
369 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000370 print_text.append('To see results here, run: git cl try-results')
371 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700372 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000373
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000374
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375def fetch_try_jobs(auth_config, changelist, options):
376 """Fetches tryjobs from buildbucket.
377
378 Returns a map from build id to build info as json dictionary.
379 """
380 rietveld_url = settings.GetDefaultServerUrl()
381 rietveld_host = urlparse.urlparse(rietveld_url).hostname
382 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
383 if authenticator.has_cached_credentials():
384 http = authenticator.authorize(httplib2.Http())
385 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700386 print('Warning: Some results might be missing because %s' %
387 # Get the message on how to login.
388 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 http = httplib2.Http()
390
391 http.force_exception_to_status_code = True
392
393 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
394 hostname=rietveld_host,
395 issue=changelist.GetIssue(),
396 patch=options.patchset)
397 params = {'tag': 'buildset:%s' % buildset}
398
399 builds = {}
400 while True:
401 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
402 hostname=options.buildbucket_host,
403 params=urllib.urlencode(params))
404 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
405 for build in content.get('builds', []):
406 builds[build['id']] = build
407 if 'next_cursor' in content:
408 params['start_cursor'] = content['next_cursor']
409 else:
410 break
411 return builds
412
413
414def print_tryjobs(options, builds):
415 """Prints nicely result of fetch_try_jobs."""
416 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700417 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000418 return
419
420 # Make a copy, because we'll be modifying builds dictionary.
421 builds = builds.copy()
422 builder_names_cache = {}
423
424 def get_builder(b):
425 try:
426 return builder_names_cache[b['id']]
427 except KeyError:
428 try:
429 parameters = json.loads(b['parameters_json'])
430 name = parameters['builder_name']
431 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700432 print('WARNING: failed to get builder name for build %s: %s' % (
433 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000434 name = None
435 builder_names_cache[b['id']] = name
436 return name
437
438 def get_bucket(b):
439 bucket = b['bucket']
440 if bucket.startswith('master.'):
441 return bucket[len('master.'):]
442 return bucket
443
444 if options.print_master:
445 name_fmt = '%%-%ds %%-%ds' % (
446 max(len(str(get_bucket(b))) for b in builds.itervalues()),
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % (get_bucket(b), get_builder(b))
450 else:
451 name_fmt = '%%-%ds' % (
452 max(len(str(get_builder(b))) for b in builds.itervalues()))
453 def get_name(b):
454 return name_fmt % get_builder(b)
455
456 def sort_key(b):
457 return b['status'], b.get('result'), get_name(b), b.get('url')
458
459 def pop(title, f, color=None, **kwargs):
460 """Pop matching builds from `builds` dict and print them."""
461
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000462 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000463 colorize = str
464 else:
465 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
466
467 result = []
468 for b in builds.values():
469 if all(b.get(k) == v for k, v in kwargs.iteritems()):
470 builds.pop(b['id'])
471 result.append(b)
472 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700473 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000474 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700475 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000476
477 total = len(builds)
478 pop(status='COMPLETED', result='SUCCESS',
479 title='Successes:', color=Fore.GREEN,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
482 title='Infra Failures:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b), b.get('url')))
484 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
485 title='Failures:', color=Fore.RED,
486 f=lambda b: (get_name(b), b.get('url')))
487 pop(status='COMPLETED', result='CANCELED',
488 title='Canceled:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 failure_reason='INVALID_BUILD_DEFINITION',
492 title='Wrong master/builder name:', color=Fore.MAGENTA,
493 f=lambda b: (get_name(b),))
494 pop(status='COMPLETED', result='FAILURE',
495 title='Other failures:',
496 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
497 pop(status='COMPLETED',
498 title='Other finished:',
499 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
500 pop(status='STARTED',
501 title='Started:', color=Fore.YELLOW,
502 f=lambda b: (get_name(b), b.get('url')))
503 pop(status='SCHEDULED',
504 title='Scheduled:',
505 f=lambda b: (get_name(b), 'id=%s' % b['id']))
506 # The last section is just in case buildbucket API changes OR there is a bug.
507 pop(title='Other:',
508 f=lambda b: (get_name(b), 'id=%s' % b['id']))
509 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700510 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511
512
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000513def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
514 """Return the corresponding git ref if |base_url| together with |glob_spec|
515 matches the full |url|.
516
517 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
518 """
519 fetch_suburl, as_ref = glob_spec.split(':')
520 if allow_wildcards:
521 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
522 if glob_match:
523 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
524 # "branches/{472,597,648}/src:refs/remotes/svn/*".
525 branch_re = re.escape(base_url)
526 if glob_match.group(1):
527 branch_re += '/' + re.escape(glob_match.group(1))
528 wildcard = glob_match.group(2)
529 if wildcard == '*':
530 branch_re += '([^/]*)'
531 else:
532 # Escape and replace surrounding braces with parentheses and commas
533 # with pipe symbols.
534 wildcard = re.escape(wildcard)
535 wildcard = re.sub('^\\\\{', '(', wildcard)
536 wildcard = re.sub('\\\\,', '|', wildcard)
537 wildcard = re.sub('\\\\}$', ')', wildcard)
538 branch_re += wildcard
539 if glob_match.group(3):
540 branch_re += re.escape(glob_match.group(3))
541 match = re.match(branch_re, url)
542 if match:
543 return re.sub('\*$', match.group(1), as_ref)
544
545 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
546 if fetch_suburl:
547 full_url = base_url + '/' + fetch_suburl
548 else:
549 full_url = base_url
550 if full_url == url:
551 return as_ref
552 return None
553
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000554
iannucci@chromium.org79540052012-10-19 23:15:26 +0000555def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 """Prints statistics about the change to the user."""
557 # --no-ext-diff is broken in some versions of Git, so try to work around
558 # this by overriding the environment (but there is still a problem if the
559 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000560 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000561 if 'GIT_EXTERNAL_DIFF' in env:
562 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000563
564 if find_copies:
565 similarity_options = ['--find-copies-harder', '-l100000',
566 '-C%s' % similarity]
567 else:
568 similarity_options = ['-M%s' % similarity]
569
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000570 try:
571 stdout = sys.stdout.fileno()
572 except AttributeError:
573 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000575 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000576 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000577 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000578
579
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000580class BuildbucketResponseException(Exception):
581 pass
582
583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584class Settings(object):
585 def __init__(self):
586 self.default_server = None
587 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000588 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 self.is_git_svn = None
590 self.svn_branch = None
591 self.tree_status_url = None
592 self.viewvc_url = None
593 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000594 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000595 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000596 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000597 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000598 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000599 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000600 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601
602 def LazyUpdateIfNeeded(self):
603 """Updates the settings from a codereview.settings file, if available."""
604 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 # The only value that actually changes the behavior is
606 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000607 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000608 error_ok=True
609 ).strip().lower()
610
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000612 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 LoadCodereviewSettingsFromFile(cr_settings_file)
614 self.updated = True
615
616 def GetDefaultServerUrl(self, error_ok=False):
617 if not self.default_server:
618 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 if error_ok:
622 return self.default_server
623 if not self.default_server:
624 error_message = ('Could not find settings file. You must configure '
625 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000626 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return self.default_server
629
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 @staticmethod
631 def GetRelativeRoot():
632 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000635 if self.root is None:
636 self.root = os.path.abspath(self.GetRelativeRoot())
637 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000639 def GetGitMirror(self, remote='origin'):
640 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000641 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000642 if not os.path.isdir(local_url):
643 return None
644 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
645 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
646 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
647 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
648 if mirror.exists():
649 return mirror
650 return None
651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 def GetIsGitSvn(self):
653 """Return true if this repo looks like it's using git-svn."""
654 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000655 if self.GetPendingRefPrefix():
656 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
657 self.is_git_svn = False
658 else:
659 # If you have any "svn-remote.*" config keys, we think you're using svn.
660 self.is_git_svn = RunGitWithCode(
661 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 return self.is_git_svn
663
664 def GetSVNBranch(self):
665 if self.svn_branch is None:
666 if not self.GetIsGitSvn():
667 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
668
669 # Try to figure out which remote branch we're based on.
670 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # 1) iterate through our branch history and find the svn URL.
672 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673
674 # regexp matching the git-svn line that contains the URL.
675 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
676
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 # We don't want to go through all of history, so read a line from the
678 # pipe at a time.
679 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000680 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000681 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
682 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000683 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 for line in proc.stdout:
685 match = git_svn_re.match(line)
686 if match:
687 url = match.group(1)
688 proc.stdout.close() # Cut pipe.
689 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 if url:
692 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
693 remotes = RunGit(['config', '--get-regexp',
694 r'^svn-remote\..*\.url']).splitlines()
695 for remote in remotes:
696 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000698 remote = match.group(1)
699 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000700 rewrite_root = RunGit(
701 ['config', 'svn-remote.%s.rewriteRoot' % remote],
702 error_ok=True).strip()
703 if rewrite_root:
704 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000705 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000706 ['config', 'svn-remote.%s.fetch' % remote],
707 error_ok=True).strip()
708 if fetch_spec:
709 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
710 if self.svn_branch:
711 break
712 branch_spec = RunGit(
713 ['config', 'svn-remote.%s.branches' % remote],
714 error_ok=True).strip()
715 if branch_spec:
716 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
717 if self.svn_branch:
718 break
719 tag_spec = RunGit(
720 ['config', 'svn-remote.%s.tags' % remote],
721 error_ok=True).strip()
722 if tag_spec:
723 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
724 if self.svn_branch:
725 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726
727 if not self.svn_branch:
728 DieWithError('Can\'t guess svn branch -- try specifying it on the '
729 'command line')
730
731 return self.svn_branch
732
733 def GetTreeStatusUrl(self, error_ok=False):
734 if not self.tree_status_url:
735 error_message = ('You must configure your tree status URL by running '
736 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.tree_status_url = self._GetRietveldConfig(
738 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.tree_status_url
740
741 def GetViewVCUrl(self):
742 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000743 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 return self.viewvc_url
745
rmistry@google.com90752582014-01-14 21:04:50 +0000746 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000747 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000748
rmistry@google.com78948ed2015-07-08 23:09:57 +0000749 def GetIsSkipDependencyUpload(self, branch_name):
750 """Returns true if specified branch should skip dep uploads."""
751 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
752 error_ok=True)
753
rmistry@google.com5626a922015-02-26 14:03:30 +0000754 def GetRunPostUploadHook(self):
755 run_post_upload_hook = self._GetRietveldConfig(
756 'run-post-upload-hook', error_ok=True)
757 return run_post_upload_hook == "True"
758
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000759 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000761
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000762 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000764
ukai@chromium.orge8077812012-02-03 03:41:46 +0000765 def GetIsGerrit(self):
766 """Return true if this repo is assosiated with gerrit code review system."""
767 if self.is_gerrit is None:
768 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
769 return self.is_gerrit
770
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000771 def GetSquashGerritUploads(self):
772 """Return true if uploads to Gerrit should be squashed by default."""
773 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700774 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
775 if self.squash_gerrit_uploads is None:
776 # Default is squash now (http://crbug.com/611892#c23).
777 self.squash_gerrit_uploads = not (
778 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
779 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000780 return self.squash_gerrit_uploads
781
tandriia60502f2016-06-20 02:01:53 -0700782 def GetSquashGerritUploadsOverride(self):
783 """Return True or False if codereview.settings should be overridden.
784
785 Returns None if no override has been defined.
786 """
787 # See also http://crbug.com/611892#c23
788 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
789 error_ok=True).strip()
790 if result == 'true':
791 return True
792 if result == 'false':
793 return False
794 return None
795
tandrii@chromium.org28253532016-04-14 13:46:56 +0000796 def GetGerritSkipEnsureAuthenticated(self):
797 """Return True if EnsureAuthenticated should not be done for Gerrit
798 uploads."""
799 if self.gerrit_skip_ensure_authenticated is None:
800 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000801 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000802 error_ok=True).strip() == 'true')
803 return self.gerrit_skip_ensure_authenticated
804
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000805 def GetGitEditor(self):
806 """Return the editor specified in the git config, or None if none is."""
807 if self.git_editor is None:
808 self.git_editor = self._GetConfig('core.editor', error_ok=True)
809 return self.git_editor or None
810
thestig@chromium.org44202a22014-03-11 19:22:18 +0000811 def GetLintRegex(self):
812 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
813 DEFAULT_LINT_REGEX)
814
815 def GetLintIgnoreRegex(self):
816 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
817 DEFAULT_LINT_IGNORE_REGEX)
818
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000819 def GetProject(self):
820 if not self.project:
821 self.project = self._GetRietveldConfig('project', error_ok=True)
822 return self.project
823
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000824 def GetForceHttpsCommitUrl(self):
825 if not self.force_https_commit_url:
826 self.force_https_commit_url = self._GetRietveldConfig(
827 'force-https-commit-url', error_ok=True)
828 return self.force_https_commit_url
829
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000830 def GetPendingRefPrefix(self):
831 if not self.pending_ref_prefix:
832 self.pending_ref_prefix = self._GetRietveldConfig(
833 'pending-ref-prefix', error_ok=True)
834 return self.pending_ref_prefix
835
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 def _GetRietveldConfig(self, param, **kwargs):
837 return self._GetConfig('rietveld.' + param, **kwargs)
838
rmistry@google.com78948ed2015-07-08 23:09:57 +0000839 def _GetBranchConfig(self, branch_name, param, **kwargs):
840 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def _GetConfig(self, param, **kwargs):
843 self.LazyUpdateIfNeeded()
844 return RunGit(['config', param], **kwargs).strip()
845
846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847def ShortBranchName(branch):
848 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000849 return branch.replace('refs/heads/', '', 1)
850
851
852def GetCurrentBranchRef():
853 """Returns branch ref (e.g., refs/heads/master) or None."""
854 return RunGit(['symbolic-ref', 'HEAD'],
855 stderr=subprocess2.VOID, error_ok=True).strip() or None
856
857
858def GetCurrentBranch():
859 """Returns current branch or None.
860
861 For refs/heads/* branches, returns just last part. For others, full ref.
862 """
863 branchref = GetCurrentBranchRef()
864 if branchref:
865 return ShortBranchName(branchref)
866 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867
868
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000869class _CQState(object):
870 """Enum for states of CL with respect to Commit Queue."""
871 NONE = 'none'
872 DRY_RUN = 'dry_run'
873 COMMIT = 'commit'
874
875 ALL_STATES = [NONE, DRY_RUN, COMMIT]
876
877
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000878class _ParsedIssueNumberArgument(object):
879 def __init__(self, issue=None, patchset=None, hostname=None):
880 self.issue = issue
881 self.patchset = patchset
882 self.hostname = hostname
883
884 @property
885 def valid(self):
886 return self.issue is not None
887
888
889class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
890 def __init__(self, *args, **kwargs):
891 self.patch_url = kwargs.pop('patch_url', None)
892 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
893
894
895def ParseIssueNumberArgument(arg):
896 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
897 fail_result = _ParsedIssueNumberArgument()
898
899 if arg.isdigit():
900 return _ParsedIssueNumberArgument(issue=int(arg))
901 if not arg.startswith('http'):
902 return fail_result
903 url = gclient_utils.UpgradeToHttps(arg)
904 try:
905 parsed_url = urlparse.urlparse(url)
906 except ValueError:
907 return fail_result
908 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
909 tmp = cls.ParseIssueURL(parsed_url)
910 if tmp is not None:
911 return tmp
912 return fail_result
913
914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000916 """Changelist works with one changelist in local branch.
917
918 Supports two codereview backends: Rietveld or Gerrit, selected at object
919 creation.
920
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000921 Notes:
922 * Not safe for concurrent multi-{thread,process} use.
923 * Caches values from current branch. Therefore, re-use after branch change
924 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000925 """
926
927 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
928 """Create a new ChangeList instance.
929
930 If issue is given, the codereview must be given too.
931
932 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
933 Otherwise, it's decided based on current configuration of the local branch,
934 with default being 'rietveld' for backwards compatibility.
935 See _load_codereview_impl for more details.
936
937 **kwargs will be passed directly to codereview implementation.
938 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000940 global settings
941 if not settings:
942 # Happens when git_cl.py is used as a utility library.
943 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000944
945 if issue:
946 assert codereview, 'codereview must be known, if issue is known'
947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 self.branchref = branchref
949 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000950 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 self.branch = ShortBranchName(self.branchref)
952 else:
953 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000955 self.lookedup_issue = False
956 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.has_description = False
958 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000959 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000961 self.cc = None
962 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000963 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000964
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000968 assert self._codereview_impl
969 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970
971 def _load_codereview_impl(self, codereview=None, **kwargs):
972 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000973 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
974 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
975 self._codereview = codereview
976 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000977 return
978
979 # Automatic selection based on issue number set for a current branch.
980 # Rietveld takes precedence over Gerrit.
981 assert not self.issue
982 # Whether we find issue or not, we are doing the lookup.
983 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000984 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000985 setting = cls.IssueSetting(self.GetBranch())
986 issue = RunGit(['config', setting], error_ok=True).strip()
987 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000988 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000989 self._codereview_impl = cls(self, **kwargs)
990 self.issue = int(issue)
991 return
992
993 # No issue is set for this branch, so decide based on repo-wide settings.
994 return self._load_codereview_impl(
995 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
996 **kwargs)
997
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000998 def IsGerrit(self):
999 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001000
1001 def GetCCList(self):
1002 """Return the users cc'd on this CL.
1003
1004 Return is a string suitable for passing to gcl with the --cc flag.
1005 """
1006 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001007 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001008 more_cc = ','.join(self.watchers)
1009 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1010 return self.cc
1011
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001012 def GetCCListWithoutDefault(self):
1013 """Return the users cc'd on this CL excluding default ones."""
1014 if self.cc is None:
1015 self.cc = ','.join(self.watchers)
1016 return self.cc
1017
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001018 def SetWatchers(self, watchers):
1019 """Set the list of email addresses that should be cc'd based on the changed
1020 files in this CL.
1021 """
1022 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
1024 def GetBranch(self):
1025 """Returns the short branch name, e.g. 'master'."""
1026 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001028 if not branchref:
1029 return None
1030 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 self.branch = ShortBranchName(self.branchref)
1032 return self.branch
1033
1034 def GetBranchRef(self):
1035 """Returns the full branch name, e.g. 'refs/heads/master'."""
1036 self.GetBranch() # Poke the lazy loader.
1037 return self.branchref
1038
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001039 def ClearBranch(self):
1040 """Clears cached branch data of this object."""
1041 self.branch = self.branchref = None
1042
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001043 @staticmethod
1044 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001045 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 e.g. 'origin', 'refs/heads/master'
1047 """
1048 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1050 error_ok=True).strip()
1051 if upstream_branch:
1052 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1053 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001054 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1055 error_ok=True).strip()
1056 if upstream_branch:
1057 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001059 # Fall back on trying a git-svn upstream branch.
1060 if settings.GetIsGitSvn():
1061 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001063 # Else, try to guess the origin remote.
1064 remote_branches = RunGit(['branch', '-r']).split()
1065 if 'origin/master' in remote_branches:
1066 # Fall back on origin/master if it exits.
1067 remote = 'origin'
1068 upstream_branch = 'refs/heads/master'
1069 elif 'origin/trunk' in remote_branches:
1070 # Fall back on origin/trunk if it exists. Generally a shared
1071 # git-svn clone
1072 remote = 'origin'
1073 upstream_branch = 'refs/heads/trunk'
1074 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001075 DieWithError(
1076 'Unable to determine default branch to diff against.\n'
1077 'Either pass complete "git diff"-style arguments, like\n'
1078 ' git cl upload origin/master\n'
1079 'or verify this branch is set up to track another \n'
1080 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 return remote, upstream_branch
1083
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001084 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001085 upstream_branch = self.GetUpstreamBranch()
1086 if not BranchExists(upstream_branch):
1087 DieWithError('The upstream for the current branch (%s) does not exist '
1088 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001089 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001090 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 def GetUpstreamBranch(self):
1093 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001094 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001096 upstream_branch = upstream_branch.replace('refs/heads/',
1097 'refs/remotes/%s/' % remote)
1098 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1099 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.upstream_branch = upstream_branch
1101 return self.upstream_branch
1102
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001104 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001105 remote, branch = None, self.GetBranch()
1106 seen_branches = set()
1107 while branch not in seen_branches:
1108 seen_branches.add(branch)
1109 remote, branch = self.FetchUpstreamTuple(branch)
1110 branch = ShortBranchName(branch)
1111 if remote != '.' or branch.startswith('refs/remotes'):
1112 break
1113 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001114 remotes = RunGit(['remote'], error_ok=True).split()
1115 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001116 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001117 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001118 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001119 logging.warning('Could not determine which remote this change is '
1120 'associated with, so defaulting to "%s". This may '
1121 'not be what you want. You may prevent this message '
1122 'by running "git svn info" as documented here: %s',
1123 self._remote,
1124 GIT_INSTRUCTIONS_URL)
1125 else:
1126 logging.warn('Could not determine which remote this change is '
1127 'associated with. You may prevent this message by '
1128 'running "git svn info" as documented here: %s',
1129 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001130 branch = 'HEAD'
1131 if branch.startswith('refs/remotes'):
1132 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001133 elif branch.startswith('refs/branch-heads/'):
1134 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 else:
1136 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001137 return self._remote
1138
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001139 def GitSanityChecks(self, upstream_git_obj):
1140 """Checks git repo status and ensures diff is from local commits."""
1141
sbc@chromium.org79706062015-01-14 21:18:12 +00001142 if upstream_git_obj is None:
1143 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001144 print('ERROR: unable to determine current branch (detached HEAD?)',
1145 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001146 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001147 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001148 return False
1149
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001150 # Verify the commit we're diffing against is in our current branch.
1151 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1152 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1153 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001154 print('ERROR: %s is not in the current branch. You may need to rebase '
1155 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001156 return False
1157
1158 # List the commits inside the diff, and verify they are all local.
1159 commits_in_diff = RunGit(
1160 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1161 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1162 remote_branch = remote_branch.strip()
1163 if code != 0:
1164 _, remote_branch = self.GetRemoteBranch()
1165
1166 commits_in_remote = RunGit(
1167 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1168
1169 common_commits = set(commits_in_diff) & set(commits_in_remote)
1170 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001171 print('ERROR: Your diff contains %d commits already in %s.\n'
1172 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1173 'the diff. If you are using a custom git flow, you can override'
1174 ' the reference used for this check with "git config '
1175 'gitcl.remotebranch <git-ref>".' % (
1176 len(common_commits), remote_branch, upstream_git_obj),
1177 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 return False
1179 return True
1180
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001181 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001182 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001183
1184 Returns None if it is not set.
1185 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001186 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1187 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001188
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001189 def GetGitSvnRemoteUrl(self):
1190 """Return the configured git-svn remote URL parsed from git svn info.
1191
1192 Returns None if it is not set.
1193 """
1194 # URL is dependent on the current directory.
1195 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1196 if data:
1197 keys = dict(line.split(': ', 1) for line in data.splitlines()
1198 if ': ' in line)
1199 return keys.get('URL', None)
1200 return None
1201
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 def GetRemoteUrl(self):
1203 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1204
1205 Returns None if there is no remote.
1206 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001208 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1209
1210 # If URL is pointing to a local directory, it is probably a git cache.
1211 if os.path.isdir(url):
1212 url = RunGit(['config', 'remote.%s.url' % remote],
1213 error_ok=True,
1214 cwd=url).strip()
1215 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001217 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001218 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001219 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 issue = RunGit(['config',
1221 self._codereview_impl.IssueSetting(self.GetBranch())],
1222 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001223 self.issue = int(issue) or None if issue else None
1224 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 return self.issue
1226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 def GetIssueURL(self):
1228 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 issue = self.GetIssue()
1230 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001231 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
1234 def GetDescription(self, pretty=False):
1235 if not self.has_description:
1236 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001237 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 self.has_description = True
1239 if pretty:
1240 wrapper = textwrap.TextWrapper()
1241 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1242 return wrapper.fill(self.description)
1243 return self.description
1244
1245 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001246 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001247 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001248 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001250 self.patchset = int(patchset) or None if patchset else None
1251 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 return self.patchset
1253
1254 def SetPatchset(self, patchset):
1255 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001258 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001259 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001261 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001262 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001263 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001265 def SetIssue(self, issue=None):
1266 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001267 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1268 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001270 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001271 RunGit(['config', issue_setting, str(issue)])
1272 codereview_server = self._codereview_impl.GetCodereviewServer()
1273 if codereview_server:
1274 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001276 # Reset it regardless. It doesn't hurt.
1277 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1278 for prop in (['last-upload-hash'] +
1279 self._codereview_impl._PostUnsetIssueProperties()):
1280 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1281 for setting in config_settings:
1282 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001283 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001284 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001286 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 if not self.GitSanityChecks(upstream_branch):
1288 DieWithError('\nGit sanity check failure')
1289
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001291 if not root:
1292 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001293 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001294
1295 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001296 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001297 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001298 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001299 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001300 except subprocess2.CalledProcessError:
1301 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001302 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001303 'This branch probably doesn\'t exist anymore. To reset the\n'
1304 'tracking branch, please run\n'
1305 ' git branch --set-upstream %s trunk\n'
1306 'replacing trunk with origin/master or the relevant branch') %
1307 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001308
maruel@chromium.org52424302012-08-29 15:14:30 +00001309 issue = self.GetIssue()
1310 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001311 if issue:
1312 description = self.GetDescription()
1313 else:
1314 # If the change was never uploaded, use the log messages of all commits
1315 # up to the branch point, as git cl upload will prefill the description
1316 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001317 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1318 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001319
1320 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001321 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001322 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001323 name,
1324 description,
1325 absroot,
1326 files,
1327 issue,
1328 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001329 author,
1330 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001331
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001332 def UpdateDescription(self, description):
1333 self.description = description
1334 return self._codereview_impl.UpdateDescriptionRemote(description)
1335
1336 def RunHook(self, committing, may_prompt, verbose, change):
1337 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1338 try:
1339 return presubmit_support.DoPresubmitChecks(change, committing,
1340 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1341 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001342 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1343 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001344 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001345 DieWithError(
1346 ('%s\nMaybe your depot_tools is out of date?\n'
1347 'If all fails, contact maruel@') % e)
1348
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001349 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1350 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001351 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1352 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001353 else:
1354 # Assume url.
1355 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1356 urlparse.urlparse(issue_arg))
1357 if not parsed_issue_arg or not parsed_issue_arg.valid:
1358 DieWithError('Failed to parse issue argument "%s". '
1359 'Must be an issue number or a valid URL.' % issue_arg)
1360 return self._codereview_impl.CMDPatchWithParsedIssue(
1361 parsed_issue_arg, reject, nocommit, directory)
1362
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001363 def CMDUpload(self, options, git_diff_args, orig_args):
1364 """Uploads a change to codereview."""
1365 if git_diff_args:
1366 # TODO(ukai): is it ok for gerrit case?
1367 base_branch = git_diff_args[0]
1368 else:
1369 if self.GetBranch() is None:
1370 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1371
1372 # Default to diffing against common ancestor of upstream branch
1373 base_branch = self.GetCommonAncestorWithUpstream()
1374 git_diff_args = [base_branch, 'HEAD']
1375
1376 # Make sure authenticated to codereview before running potentially expensive
1377 # hooks. It is a fast, best efforts check. Codereview still can reject the
1378 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001379 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001380
1381 # Apply watchlists on upload.
1382 change = self.GetChange(base_branch, None)
1383 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1384 files = [f.LocalPath() for f in change.AffectedFiles()]
1385 if not options.bypass_watchlists:
1386 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1387
1388 if not options.bypass_hooks:
1389 if options.reviewers or options.tbr_owners:
1390 # Set the reviewer list now so that presubmit checks can access it.
1391 change_description = ChangeDescription(change.FullDescriptionText())
1392 change_description.update_reviewers(options.reviewers,
1393 options.tbr_owners,
1394 change)
1395 change.SetDescriptionText(change_description.description)
1396 hook_results = self.RunHook(committing=False,
1397 may_prompt=not options.force,
1398 verbose=options.verbose,
1399 change=change)
1400 if not hook_results.should_continue():
1401 return 1
1402 if not options.reviewers and hook_results.reviewers:
1403 options.reviewers = hook_results.reviewers.split(',')
1404
1405 if self.GetIssue():
1406 latest_patchset = self.GetMostRecentPatchset()
1407 local_patchset = self.GetPatchset()
1408 if (latest_patchset and local_patchset and
1409 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001410 print('The last upload made from this repository was patchset #%d but '
1411 'the most recent patchset on the server is #%d.'
1412 % (local_patchset, latest_patchset))
1413 print('Uploading will still work, but if you\'ve uploaded to this '
1414 'issue from another machine or branch the patch you\'re '
1415 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001416 ask_for_data('About to upload; enter to confirm.')
1417
1418 print_stats(options.similarity, options.find_copies, git_diff_args)
1419 ret = self.CMDUploadChange(options, git_diff_args, change)
1420 if not ret:
1421 git_set_branch_value('last-upload-hash',
1422 RunGit(['rev-parse', 'HEAD']).strip())
1423 # Run post upload hooks, if specified.
1424 if settings.GetRunPostUploadHook():
1425 presubmit_support.DoPostUploadExecuter(
1426 change,
1427 self,
1428 settings.GetRoot(),
1429 options.verbose,
1430 sys.stdout)
1431
1432 # Upload all dependencies if specified.
1433 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001434 print()
1435 print('--dependencies has been specified.')
1436 print('All dependent local branches will be re-uploaded.')
1437 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001438 # Remove the dependencies flag from args so that we do not end up in a
1439 # loop.
1440 orig_args.remove('--dependencies')
1441 ret = upload_branch_deps(self, orig_args)
1442 return ret
1443
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001444 def SetCQState(self, new_state):
1445 """Update the CQ state for latest patchset.
1446
1447 Issue must have been already uploaded and known.
1448 """
1449 assert new_state in _CQState.ALL_STATES
1450 assert self.GetIssue()
1451 return self._codereview_impl.SetCQState(new_state)
1452
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001453 # Forward methods to codereview specific implementation.
1454
1455 def CloseIssue(self):
1456 return self._codereview_impl.CloseIssue()
1457
1458 def GetStatus(self):
1459 return self._codereview_impl.GetStatus()
1460
1461 def GetCodereviewServer(self):
1462 return self._codereview_impl.GetCodereviewServer()
1463
1464 def GetApprovingReviewers(self):
1465 return self._codereview_impl.GetApprovingReviewers()
1466
1467 def GetMostRecentPatchset(self):
1468 return self._codereview_impl.GetMostRecentPatchset()
1469
1470 def __getattr__(self, attr):
1471 # This is because lots of untested code accesses Rietveld-specific stuff
1472 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001473 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001474 return getattr(self._codereview_impl, attr)
1475
1476
1477class _ChangelistCodereviewBase(object):
1478 """Abstract base class encapsulating codereview specifics of a changelist."""
1479 def __init__(self, changelist):
1480 self._changelist = changelist # instance of Changelist
1481
1482 def __getattr__(self, attr):
1483 # Forward methods to changelist.
1484 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1485 # _RietveldChangelistImpl to avoid this hack?
1486 return getattr(self._changelist, attr)
1487
1488 def GetStatus(self):
1489 """Apply a rough heuristic to give a simple summary of an issue's review
1490 or CQ status, assuming adherence to a common workflow.
1491
1492 Returns None if no issue for this branch, or specific string keywords.
1493 """
1494 raise NotImplementedError()
1495
1496 def GetCodereviewServer(self):
1497 """Returns server URL without end slash, like "https://codereview.com"."""
1498 raise NotImplementedError()
1499
1500 def FetchDescription(self):
1501 """Fetches and returns description from the codereview server."""
1502 raise NotImplementedError()
1503
1504 def GetCodereviewServerSetting(self):
1505 """Returns git config setting for the codereview server."""
1506 raise NotImplementedError()
1507
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001508 @classmethod
1509 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001510 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001511
1512 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001513 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001514 """Returns name of git config setting which stores issue number for a given
1515 branch."""
1516 raise NotImplementedError()
1517
1518 def PatchsetSetting(self):
1519 """Returns name of git config setting which stores issue number."""
1520 raise NotImplementedError()
1521
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001522 def _PostUnsetIssueProperties(self):
1523 """Which branch-specific properties to erase when unsettin issue."""
1524 raise NotImplementedError()
1525
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526 def GetRieveldObjForPresubmit(self):
1527 # This is an unfortunate Rietveld-embeddedness in presubmit.
1528 # For non-Rietveld codereviews, this probably should return a dummy object.
1529 raise NotImplementedError()
1530
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001531 def GetGerritObjForPresubmit(self):
1532 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1533 return None
1534
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001535 def UpdateDescriptionRemote(self, description):
1536 """Update the description on codereview site."""
1537 raise NotImplementedError()
1538
1539 def CloseIssue(self):
1540 """Closes the issue."""
1541 raise NotImplementedError()
1542
1543 def GetApprovingReviewers(self):
1544 """Returns a list of reviewers approving the change.
1545
1546 Note: not necessarily committers.
1547 """
1548 raise NotImplementedError()
1549
1550 def GetMostRecentPatchset(self):
1551 """Returns the most recent patchset number from the codereview site."""
1552 raise NotImplementedError()
1553
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001554 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1555 directory):
1556 """Fetches and applies the issue.
1557
1558 Arguments:
1559 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1560 reject: if True, reject the failed patch instead of switching to 3-way
1561 merge. Rietveld only.
1562 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1563 only.
1564 directory: switch to directory before applying the patch. Rietveld only.
1565 """
1566 raise NotImplementedError()
1567
1568 @staticmethod
1569 def ParseIssueURL(parsed_url):
1570 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1571 failed."""
1572 raise NotImplementedError()
1573
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001574 def EnsureAuthenticated(self, force):
1575 """Best effort check that user is authenticated with codereview server.
1576
1577 Arguments:
1578 force: whether to skip confirmation questions.
1579 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 raise NotImplementedError()
1581
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001582 def CMDUploadChange(self, options, args, change):
1583 """Uploads a change to codereview."""
1584 raise NotImplementedError()
1585
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001586 def SetCQState(self, new_state):
1587 """Update the CQ state for latest patchset.
1588
1589 Issue must have been already uploaded and known.
1590 """
1591 raise NotImplementedError()
1592
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001593
1594class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1595 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1596 super(_RietveldChangelistImpl, self).__init__(changelist)
1597 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1598 settings.GetDefaultServerUrl()
1599
1600 self._rietveld_server = rietveld_server
1601 self._auth_config = auth_config
1602 self._props = None
1603 self._rpc_server = None
1604
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001605 def GetCodereviewServer(self):
1606 if not self._rietveld_server:
1607 # If we're on a branch then get the server potentially associated
1608 # with that branch.
1609 if self.GetIssue():
1610 rietveld_server_setting = self.GetCodereviewServerSetting()
1611 if rietveld_server_setting:
1612 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1613 ['config', rietveld_server_setting], error_ok=True).strip())
1614 if not self._rietveld_server:
1615 self._rietveld_server = settings.GetDefaultServerUrl()
1616 return self._rietveld_server
1617
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001618 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001619 """Best effort check that user is authenticated with Rietveld server."""
1620 if self._auth_config.use_oauth2:
1621 authenticator = auth.get_authenticator_for_host(
1622 self.GetCodereviewServer(), self._auth_config)
1623 if not authenticator.has_cached_credentials():
1624 raise auth.LoginRequiredError(self.GetCodereviewServer())
1625
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001626 def FetchDescription(self):
1627 issue = self.GetIssue()
1628 assert issue
1629 try:
1630 return self.RpcServer().get_description(issue).strip()
1631 except urllib2.HTTPError as e:
1632 if e.code == 404:
1633 DieWithError(
1634 ('\nWhile fetching the description for issue %d, received a '
1635 '404 (not found)\n'
1636 'error. It is likely that you deleted this '
1637 'issue on the server. If this is the\n'
1638 'case, please run\n\n'
1639 ' git cl issue 0\n\n'
1640 'to clear the association with the deleted issue. Then run '
1641 'this command again.') % issue)
1642 else:
1643 DieWithError(
1644 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1645 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001646 print('Warning: Failed to retrieve CL description due to network '
1647 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001648 return ''
1649
1650 def GetMostRecentPatchset(self):
1651 return self.GetIssueProperties()['patchsets'][-1]
1652
1653 def GetPatchSetDiff(self, issue, patchset):
1654 return self.RpcServer().get(
1655 '/download/issue%s_%s.diff' % (issue, patchset))
1656
1657 def GetIssueProperties(self):
1658 if self._props is None:
1659 issue = self.GetIssue()
1660 if not issue:
1661 self._props = {}
1662 else:
1663 self._props = self.RpcServer().get_issue_properties(issue, True)
1664 return self._props
1665
1666 def GetApprovingReviewers(self):
1667 return get_approving_reviewers(self.GetIssueProperties())
1668
1669 def AddComment(self, message):
1670 return self.RpcServer().add_comment(self.GetIssue(), message)
1671
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001672 def GetStatus(self):
1673 """Apply a rough heuristic to give a simple summary of an issue's review
1674 or CQ status, assuming adherence to a common workflow.
1675
1676 Returns None if no issue for this branch, or one of the following keywords:
1677 * 'error' - error from review tool (including deleted issues)
1678 * 'unsent' - not sent for review
1679 * 'waiting' - waiting for review
1680 * 'reply' - waiting for owner to reply to review
1681 * 'lgtm' - LGTM from at least one approved reviewer
1682 * 'commit' - in the commit queue
1683 * 'closed' - closed
1684 """
1685 if not self.GetIssue():
1686 return None
1687
1688 try:
1689 props = self.GetIssueProperties()
1690 except urllib2.HTTPError:
1691 return 'error'
1692
1693 if props.get('closed'):
1694 # Issue is closed.
1695 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001696 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001697 # Issue is in the commit queue.
1698 return 'commit'
1699
1700 try:
1701 reviewers = self.GetApprovingReviewers()
1702 except urllib2.HTTPError:
1703 return 'error'
1704
1705 if reviewers:
1706 # Was LGTM'ed.
1707 return 'lgtm'
1708
1709 messages = props.get('messages') or []
1710
tandrii9d2c7a32016-06-22 03:42:45 -07001711 # Skip CQ messages that don't require owner's action.
1712 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1713 if 'Dry run:' in messages[-1]['text']:
1714 messages.pop()
1715 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1716 # This message always follows prior messages from CQ,
1717 # so skip this too.
1718 messages.pop()
1719 else:
1720 # This is probably a CQ messages warranting user attention.
1721 break
1722
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001723 if not messages:
1724 # No message was sent.
1725 return 'unsent'
1726 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001727 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001728 return 'reply'
1729 return 'waiting'
1730
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001731 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001732 return self.RpcServer().update_description(
1733 self.GetIssue(), self.description)
1734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001736 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001738 def SetFlag(self, flag, value):
1739 """Patchset must match."""
1740 if not self.GetPatchset():
1741 DieWithError('The patchset needs to match. Send another patchset.')
1742 try:
1743 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001744 self.GetIssue(), self.GetPatchset(), flag, value)
vapierfd77ac72016-06-16 08:33:57 -07001745 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001746 if e.code == 404:
1747 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1748 if e.code == 403:
1749 DieWithError(
1750 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1751 'match?') % (self.GetIssue(), self.GetPatchset()))
1752 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001754 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 """Returns an upload.RpcServer() to access this review's rietveld instance.
1756 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001757 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001758 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001760 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001761 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001763 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001764 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001765 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 """Return the git setting that stores this change's most recent patchset."""
1769 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1770
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001773 branch = self.GetBranch()
1774 if branch:
1775 return 'branch.%s.rietveldserver' % branch
1776 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001778 def _PostUnsetIssueProperties(self):
1779 """Which branch-specific properties to erase when unsetting issue."""
1780 return ['rietveldserver']
1781
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 def GetRieveldObjForPresubmit(self):
1783 return self.RpcServer()
1784
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001785 def SetCQState(self, new_state):
1786 props = self.GetIssueProperties()
1787 if props.get('private'):
1788 DieWithError('Cannot set-commit on private issue')
1789
1790 if new_state == _CQState.COMMIT:
1791 self.SetFlag('commit', '1')
1792 elif new_state == _CQState.NONE:
1793 self.SetFlag('commit', '0')
1794 else:
1795 raise NotImplementedError()
1796
1797
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001798 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1799 directory):
1800 # TODO(maruel): Use apply_issue.py
1801
1802 # PatchIssue should never be called with a dirty tree. It is up to the
1803 # caller to check this, but just in case we assert here since the
1804 # consequences of the caller not checking this could be dire.
1805 assert(not git_common.is_dirty_git_tree('apply'))
1806 assert(parsed_issue_arg.valid)
1807 self._changelist.issue = parsed_issue_arg.issue
1808 if parsed_issue_arg.hostname:
1809 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1810
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001811 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1812 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001813 assert parsed_issue_arg.patchset
1814 patchset = parsed_issue_arg.patchset
1815 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1816 else:
1817 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1818 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1819
1820 # Switch up to the top-level directory, if necessary, in preparation for
1821 # applying the patch.
1822 top = settings.GetRelativeRoot()
1823 if top:
1824 os.chdir(top)
1825
1826 # Git patches have a/ at the beginning of source paths. We strip that out
1827 # with a sed script rather than the -p flag to patch so we can feed either
1828 # Git or svn-style patches into the same apply command.
1829 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1830 try:
1831 patch_data = subprocess2.check_output(
1832 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1833 except subprocess2.CalledProcessError:
1834 DieWithError('Git patch mungling failed.')
1835 logging.info(patch_data)
1836
1837 # We use "git apply" to apply the patch instead of "patch" so that we can
1838 # pick up file adds.
1839 # The --index flag means: also insert into the index (so we catch adds).
1840 cmd = ['git', 'apply', '--index', '-p0']
1841 if directory:
1842 cmd.extend(('--directory', directory))
1843 if reject:
1844 cmd.append('--reject')
1845 elif IsGitVersionAtLeast('1.7.12'):
1846 cmd.append('--3way')
1847 try:
1848 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1849 stdin=patch_data, stdout=subprocess2.VOID)
1850 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001851 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 return 1
1853
1854 # If we had an issue, commit the current state and register the issue.
1855 if not nocommit:
1856 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1857 'patch from issue %(i)s at patchset '
1858 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1859 % {'i': self.GetIssue(), 'p': patchset})])
1860 self.SetIssue(self.GetIssue())
1861 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001862 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001863 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001864 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001865 return 0
1866
1867 @staticmethod
1868 def ParseIssueURL(parsed_url):
1869 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1870 return None
1871 # Typical url: https://domain/<issue_number>[/[other]]
1872 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1873 if match:
1874 return _RietveldParsedIssueNumberArgument(
1875 issue=int(match.group(1)),
1876 hostname=parsed_url.netloc)
1877 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1878 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1879 if match:
1880 return _RietveldParsedIssueNumberArgument(
1881 issue=int(match.group(1)),
1882 patchset=int(match.group(2)),
1883 hostname=parsed_url.netloc,
1884 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1885 return None
1886
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001887 def CMDUploadChange(self, options, args, change):
1888 """Upload the patch to Rietveld."""
1889 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1890 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001891 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1892 if options.emulate_svn_auto_props:
1893 upload_args.append('--emulate_svn_auto_props')
1894
1895 change_desc = None
1896
1897 if options.email is not None:
1898 upload_args.extend(['--email', options.email])
1899
1900 if self.GetIssue():
1901 if options.title:
1902 upload_args.extend(['--title', options.title])
1903 if options.message:
1904 upload_args.extend(['--message', options.message])
1905 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001906 print('This branch is associated with issue %s. '
1907 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001908 else:
1909 if options.title:
1910 upload_args.extend(['--title', options.title])
1911 message = (options.title or options.message or
1912 CreateDescriptionFromLog(args))
1913 change_desc = ChangeDescription(message)
1914 if options.reviewers or options.tbr_owners:
1915 change_desc.update_reviewers(options.reviewers,
1916 options.tbr_owners,
1917 change)
1918 if not options.force:
1919 change_desc.prompt()
1920
1921 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001922 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001923 return 1
1924
1925 upload_args.extend(['--message', change_desc.description])
1926 if change_desc.get_reviewers():
1927 upload_args.append('--reviewers=%s' % ','.join(
1928 change_desc.get_reviewers()))
1929 if options.send_mail:
1930 if not change_desc.get_reviewers():
1931 DieWithError("Must specify reviewers to send email.")
1932 upload_args.append('--send_mail')
1933
1934 # We check this before applying rietveld.private assuming that in
1935 # rietveld.cc only addresses which we can send private CLs to are listed
1936 # if rietveld.private is set, and so we should ignore rietveld.cc only
1937 # when --private is specified explicitly on the command line.
1938 if options.private:
1939 logging.warn('rietveld.cc is ignored since private flag is specified. '
1940 'You need to review and add them manually if necessary.')
1941 cc = self.GetCCListWithoutDefault()
1942 else:
1943 cc = self.GetCCList()
1944 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1945 if cc:
1946 upload_args.extend(['--cc', cc])
1947
1948 if options.private or settings.GetDefaultPrivateFlag() == "True":
1949 upload_args.append('--private')
1950
1951 upload_args.extend(['--git_similarity', str(options.similarity)])
1952 if not options.find_copies:
1953 upload_args.extend(['--git_no_find_copies'])
1954
1955 # Include the upstream repo's URL in the change -- this is useful for
1956 # projects that have their source spread across multiple repos.
1957 remote_url = self.GetGitBaseUrlFromConfig()
1958 if not remote_url:
1959 if settings.GetIsGitSvn():
1960 remote_url = self.GetGitSvnRemoteUrl()
1961 else:
1962 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1963 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1964 self.GetUpstreamBranch().split('/')[-1])
1965 if remote_url:
1966 upload_args.extend(['--base_url', remote_url])
1967 remote, remote_branch = self.GetRemoteBranch()
1968 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1969 settings.GetPendingRefPrefix())
1970 if target_ref:
1971 upload_args.extend(['--target_ref', target_ref])
1972
1973 # Look for dependent patchsets. See crbug.com/480453 for more details.
1974 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1975 upstream_branch = ShortBranchName(upstream_branch)
1976 if remote is '.':
1977 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001978 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001979 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001980 print()
1981 print('Skipping dependency patchset upload because git config '
1982 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1983 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001984 else:
1985 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001986 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001987 auth_config=auth_config)
1988 branch_cl_issue_url = branch_cl.GetIssueURL()
1989 branch_cl_issue = branch_cl.GetIssue()
1990 branch_cl_patchset = branch_cl.GetPatchset()
1991 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1992 upload_args.extend(
1993 ['--depends_on_patchset', '%s:%s' % (
1994 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001995 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001996 '\n'
1997 'The current branch (%s) is tracking a local branch (%s) with '
1998 'an associated CL.\n'
1999 'Adding %s/#ps%s as a dependency patchset.\n'
2000 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2001 branch_cl_patchset))
2002
2003 project = settings.GetProject()
2004 if project:
2005 upload_args.extend(['--project', project])
2006
2007 if options.cq_dry_run:
2008 upload_args.extend(['--cq_dry_run'])
2009
2010 try:
2011 upload_args = ['upload'] + upload_args + args
2012 logging.info('upload.RealMain(%s)', upload_args)
2013 issue, patchset = upload.RealMain(upload_args)
2014 issue = int(issue)
2015 patchset = int(patchset)
2016 except KeyboardInterrupt:
2017 sys.exit(1)
2018 except:
2019 # If we got an exception after the user typed a description for their
2020 # change, back up the description before re-raising.
2021 if change_desc:
2022 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2023 print('\nGot exception while uploading -- saving description to %s\n' %
2024 backup_path)
2025 backup_file = open(backup_path, 'w')
2026 backup_file.write(change_desc.description)
2027 backup_file.close()
2028 raise
2029
2030 if not self.GetIssue():
2031 self.SetIssue(issue)
2032 self.SetPatchset(patchset)
2033
2034 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002035 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002036 return 0
2037
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002038
2039class _GerritChangelistImpl(_ChangelistCodereviewBase):
2040 def __init__(self, changelist, auth_config=None):
2041 # auth_config is Rietveld thing, kept here to preserve interface only.
2042 super(_GerritChangelistImpl, self).__init__(changelist)
2043 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002044 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002047
2048 def _GetGerritHost(self):
2049 # Lazy load of configs.
2050 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002051 if self._gerrit_host and '.' not in self._gerrit_host:
2052 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2053 # This happens for internal stuff http://crbug.com/614312.
2054 parsed = urlparse.urlparse(self.GetRemoteUrl())
2055 if parsed.scheme == 'sso':
2056 print('WARNING: using non https URLs for remote is likely broken\n'
2057 ' Your current remote is: %s' % self.GetRemoteUrl())
2058 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2059 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002060 return self._gerrit_host
2061
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002062 def _GetGitHost(self):
2063 """Returns git host to be used when uploading change to Gerrit."""
2064 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2065
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002066 def GetCodereviewServer(self):
2067 if not self._gerrit_server:
2068 # If we're on a branch then get the server potentially associated
2069 # with that branch.
2070 if self.GetIssue():
2071 gerrit_server_setting = self.GetCodereviewServerSetting()
2072 if gerrit_server_setting:
2073 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2074 error_ok=True).strip()
2075 if self._gerrit_server:
2076 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2077 if not self._gerrit_server:
2078 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2079 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002080 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081 parts[0] = parts[0] + '-review'
2082 self._gerrit_host = '.'.join(parts)
2083 self._gerrit_server = 'https://%s' % self._gerrit_host
2084 return self._gerrit_server
2085
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002086 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002087 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002088 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002090 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002091 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002092 if settings.GetGerritSkipEnsureAuthenticated():
2093 # For projects with unusual authentication schemes.
2094 # See http://crbug.com/603378.
2095 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002096 # Lazy-loader to identify Gerrit and Git hosts.
2097 if gerrit_util.GceAuthenticator.is_gce():
2098 return
2099 self.GetCodereviewServer()
2100 git_host = self._GetGitHost()
2101 assert self._gerrit_server and self._gerrit_host
2102 cookie_auth = gerrit_util.CookiesAuthenticator()
2103
2104 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2105 git_auth = cookie_auth.get_auth_header(git_host)
2106 if gerrit_auth and git_auth:
2107 if gerrit_auth == git_auth:
2108 return
2109 print((
2110 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2111 ' Check your %s or %s file for credentials of hosts:\n'
2112 ' %s\n'
2113 ' %s\n'
2114 ' %s') %
2115 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2116 git_host, self._gerrit_host,
2117 cookie_auth.get_new_password_message(git_host)))
2118 if not force:
2119 ask_for_data('If you know what you are doing, press Enter to continue, '
2120 'Ctrl+C to abort.')
2121 return
2122 else:
2123 missing = (
2124 [] if gerrit_auth else [self._gerrit_host] +
2125 [] if git_auth else [git_host])
2126 DieWithError('Credentials for the following hosts are required:\n'
2127 ' %s\n'
2128 'These are read from %s (or legacy %s)\n'
2129 '%s' % (
2130 '\n '.join(missing),
2131 cookie_auth.get_gitcookies_path(),
2132 cookie_auth.get_netrc_path(),
2133 cookie_auth.get_new_password_message(git_host)))
2134
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002135
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002136 def PatchsetSetting(self):
2137 """Return the git setting that stores this change's most recent patchset."""
2138 return 'branch.%s.gerritpatchset' % self.GetBranch()
2139
2140 def GetCodereviewServerSetting(self):
2141 """Returns the git setting that stores this change's Gerrit server."""
2142 branch = self.GetBranch()
2143 if branch:
2144 return 'branch.%s.gerritserver' % branch
2145 return None
2146
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002147 def _PostUnsetIssueProperties(self):
2148 """Which branch-specific properties to erase when unsetting issue."""
2149 return [
2150 'gerritserver',
2151 'gerritsquashhash',
2152 ]
2153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002154 def GetRieveldObjForPresubmit(self):
2155 class ThisIsNotRietveldIssue(object):
2156 def __nonzero__(self):
2157 # This is a hack to make presubmit_support think that rietveld is not
2158 # defined, yet still ensure that calls directly result in a decent
2159 # exception message below.
2160 return False
2161
2162 def __getattr__(self, attr):
2163 print(
2164 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2165 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2166 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2167 'or use Rietveld for codereview.\n'
2168 'See also http://crbug.com/579160.' % attr)
2169 raise NotImplementedError()
2170 return ThisIsNotRietveldIssue()
2171
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002172 def GetGerritObjForPresubmit(self):
2173 return presubmit_support.GerritAccessor(self._GetGerritHost())
2174
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002176 """Apply a rough heuristic to give a simple summary of an issue's review
2177 or CQ status, assuming adherence to a common workflow.
2178
2179 Returns None if no issue for this branch, or one of the following keywords:
2180 * 'error' - error from review tool (including deleted issues)
2181 * 'unsent' - no reviewers added
2182 * 'waiting' - waiting for review
2183 * 'reply' - waiting for owner to reply to review
2184 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2185 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2186 * 'commit' - in the commit queue
2187 * 'closed' - abandoned
2188 """
2189 if not self.GetIssue():
2190 return None
2191
2192 try:
2193 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2194 except httplib.HTTPException:
2195 return 'error'
2196
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002197 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002198 return 'closed'
2199
2200 cq_label = data['labels'].get('Commit-Queue', {})
2201 if cq_label:
2202 # Vote value is a stringified integer, which we expect from 0 to 2.
2203 vote_value = cq_label.get('value', '0')
2204 vote_text = cq_label.get('values', {}).get(vote_value, '')
2205 if vote_text.lower() == 'commit':
2206 return 'commit'
2207
2208 lgtm_label = data['labels'].get('Code-Review', {})
2209 if lgtm_label:
2210 if 'rejected' in lgtm_label:
2211 return 'not lgtm'
2212 if 'approved' in lgtm_label:
2213 return 'lgtm'
2214
2215 if not data.get('reviewers', {}).get('REVIEWER', []):
2216 return 'unsent'
2217
2218 messages = data.get('messages', [])
2219 if messages:
2220 owner = data['owner'].get('_account_id')
2221 last_message_author = messages[-1].get('author', {}).get('_account_id')
2222 if owner != last_message_author:
2223 # Some reply from non-owner.
2224 return 'reply'
2225
2226 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002227
2228 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002229 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002230 return data['revisions'][data['current_revision']]['_number']
2231
2232 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002233 data = self._GetChangeDetail(['CURRENT_REVISION'])
2234 current_rev = data['current_revision']
2235 url = data['revisions'][current_rev]['fetch']['http']['url']
2236 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002237
2238 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002239 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2240 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002241
2242 def CloseIssue(self):
2243 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2244
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002245 def GetApprovingReviewers(self):
2246 """Returns a list of reviewers approving the change.
2247
2248 Note: not necessarily committers.
2249 """
2250 raise NotImplementedError()
2251
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002252 def SubmitIssue(self, wait_for_merge=True):
2253 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2254 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002255
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002256 def _GetChangeDetail(self, options=None, issue=None):
2257 options = options or []
2258 issue = issue or self.GetIssue()
2259 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002260 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2261 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002262
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002263 def CMDLand(self, force, bypass_hooks, verbose):
2264 if git_common.is_dirty_git_tree('land'):
2265 return 1
tandriid60367b2016-06-22 05:25:12 -07002266 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2267 if u'Commit-Queue' in detail.get('labels', {}):
2268 if not force:
2269 ask_for_data('\nIt seems this repository has a Commit Queue, '
2270 'which can test and land changes for you. '
2271 'Are you sure you wish to bypass it?\n'
2272 'Press Enter to continue, Ctrl+C to abort.')
2273
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002274 differs = True
2275 last_upload = RunGit(['config',
2276 'branch.%s.gerritsquashhash' % self.GetBranch()],
2277 error_ok=True).strip()
2278 # Note: git diff outputs nothing if there is no diff.
2279 if not last_upload or RunGit(['diff', last_upload]).strip():
2280 print('WARNING: some changes from local branch haven\'t been uploaded')
2281 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002282 if detail['current_revision'] == last_upload:
2283 differs = False
2284 else:
2285 print('WARNING: local branch contents differ from latest uploaded '
2286 'patchset')
2287 if differs:
2288 if not force:
2289 ask_for_data(
2290 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2291 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2292 elif not bypass_hooks:
2293 hook_results = self.RunHook(
2294 committing=True,
2295 may_prompt=not force,
2296 verbose=verbose,
2297 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2298 if not hook_results.should_continue():
2299 return 1
2300
2301 self.SubmitIssue(wait_for_merge=True)
2302 print('Issue %s has been submitted.' % self.GetIssueURL())
2303 return 0
2304
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002305 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2306 directory):
2307 assert not reject
2308 assert not nocommit
2309 assert not directory
2310 assert parsed_issue_arg.valid
2311
2312 self._changelist.issue = parsed_issue_arg.issue
2313
2314 if parsed_issue_arg.hostname:
2315 self._gerrit_host = parsed_issue_arg.hostname
2316 self._gerrit_server = 'https://%s' % self._gerrit_host
2317
2318 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2319
2320 if not parsed_issue_arg.patchset:
2321 # Use current revision by default.
2322 revision_info = detail['revisions'][detail['current_revision']]
2323 patchset = int(revision_info['_number'])
2324 else:
2325 patchset = parsed_issue_arg.patchset
2326 for revision_info in detail['revisions'].itervalues():
2327 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2328 break
2329 else:
2330 DieWithError('Couldn\'t find patchset %i in issue %i' %
2331 (parsed_issue_arg.patchset, self.GetIssue()))
2332
2333 fetch_info = revision_info['fetch']['http']
2334 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2335 RunGit(['cherry-pick', 'FETCH_HEAD'])
2336 self.SetIssue(self.GetIssue())
2337 self.SetPatchset(patchset)
2338 print('Committed patch for issue %i pathset %i locally' %
2339 (self.GetIssue(), self.GetPatchset()))
2340 return 0
2341
2342 @staticmethod
2343 def ParseIssueURL(parsed_url):
2344 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2345 return None
2346 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2347 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2348 # Short urls like https://domain/<issue_number> can be used, but don't allow
2349 # specifying the patchset (you'd 404), but we allow that here.
2350 if parsed_url.path == '/':
2351 part = parsed_url.fragment
2352 else:
2353 part = parsed_url.path
2354 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2355 if match:
2356 return _ParsedIssueNumberArgument(
2357 issue=int(match.group(2)),
2358 patchset=int(match.group(4)) if match.group(4) else None,
2359 hostname=parsed_url.netloc)
2360 return None
2361
tandrii16e0b4e2016-06-07 10:34:28 -07002362 def _GerritCommitMsgHookCheck(self, offer_removal):
2363 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2364 if not os.path.exists(hook):
2365 return
2366 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2367 # custom developer made one.
2368 data = gclient_utils.FileRead(hook)
2369 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2370 return
2371 print('Warning: you have Gerrit commit-msg hook installed.\n'
2372 'It is not neccessary for uploading with git cl in squash mode, '
2373 'and may interfere with it in subtle ways.\n'
2374 'We recommend you remove the commit-msg hook.')
2375 if offer_removal:
2376 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2377 if reply.lower().startswith('y'):
2378 gclient_utils.rm_file_or_tree(hook)
2379 print('Gerrit commit-msg hook removed.')
2380 else:
2381 print('OK, will keep Gerrit commit-msg hook in place.')
2382
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002383 def CMDUploadChange(self, options, args, change):
2384 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002385 if options.squash and options.no_squash:
2386 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002387
2388 if not options.squash and not options.no_squash:
2389 # Load default for user, repo, squash=true, in this order.
2390 options.squash = settings.GetSquashGerritUploads()
2391 elif options.no_squash:
2392 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002393
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002394 # We assume the remote called "origin" is the one we want.
2395 # It is probably not worthwhile to support different workflows.
2396 gerrit_remote = 'origin'
2397
2398 remote, remote_branch = self.GetRemoteBranch()
2399 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2400 pending_prefix='')
2401
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002402 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002403 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002404 if not self.GetIssue():
2405 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2406 # with shadow branch, which used to contain change-id for a given
2407 # branch, using which we can fetch actual issue number and set it as the
2408 # property of the branch, which is the new way.
2409 message = RunGitSilent([
2410 'show', '--format=%B', '-s',
2411 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2412 if message:
2413 change_ids = git_footers.get_footer_change_id(message.strip())
2414 if change_ids and len(change_ids) == 1:
2415 details = self._GetChangeDetail(issue=change_ids[0])
2416 if details:
2417 print('WARNING: found old upload in branch git_cl_uploads/%s '
2418 'corresponding to issue %s' %
2419 (self.GetBranch(), details['_number']))
2420 self.SetIssue(details['_number'])
2421 if not self.GetIssue():
2422 DieWithError(
2423 '\n' # For readability of the blob below.
2424 'Found old upload in branch git_cl_uploads/%s, '
2425 'but failed to find corresponding Gerrit issue.\n'
2426 'If you know the issue number, set it manually first:\n'
2427 ' git cl issue 123456\n'
2428 'If you intended to upload this CL as new issue, '
2429 'just delete or rename the old upload branch:\n'
2430 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2431 'After that, please run git cl upload again.' %
2432 tuple([self.GetBranch()] * 3))
2433 # End of backwards compatability.
2434
2435 if self.GetIssue():
2436 # Try to get the message from a previous upload.
2437 message = self.GetDescription()
2438 if not message:
2439 DieWithError(
2440 'failed to fetch description from current Gerrit issue %d\n'
2441 '%s' % (self.GetIssue(), self.GetIssueURL()))
2442 change_id = self._GetChangeDetail()['change_id']
2443 while True:
2444 footer_change_ids = git_footers.get_footer_change_id(message)
2445 if footer_change_ids == [change_id]:
2446 break
2447 if not footer_change_ids:
2448 message = git_footers.add_footer_change_id(message, change_id)
2449 print('WARNING: appended missing Change-Id to issue description')
2450 continue
2451 # There is already a valid footer but with different or several ids.
2452 # Doing this automatically is non-trivial as we don't want to lose
2453 # existing other footers, yet we want to append just 1 desired
2454 # Change-Id. Thus, just create a new footer, but let user verify the
2455 # new description.
2456 message = '%s\n\nChange-Id: %s' % (message, change_id)
2457 print(
2458 'WARNING: issue %s has Change-Id footer(s):\n'
2459 ' %s\n'
2460 'but issue has Change-Id %s, according to Gerrit.\n'
2461 'Please, check the proposed correction to the description, '
2462 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2463 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2464 change_id))
2465 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2466 if not options.force:
2467 change_desc = ChangeDescription(message)
2468 change_desc.prompt()
2469 message = change_desc.description
2470 if not message:
2471 DieWithError("Description is empty. Aborting...")
2472 # Continue the while loop.
2473 # Sanity check of this code - we should end up with proper message
2474 # footer.
2475 assert [change_id] == git_footers.get_footer_change_id(message)
2476 change_desc = ChangeDescription(message)
2477 else:
2478 change_desc = ChangeDescription(
2479 options.message or CreateDescriptionFromLog(args))
2480 if not options.force:
2481 change_desc.prompt()
2482 if not change_desc.description:
2483 DieWithError("Description is empty. Aborting...")
2484 message = change_desc.description
2485 change_ids = git_footers.get_footer_change_id(message)
2486 if len(change_ids) > 1:
2487 DieWithError('too many Change-Id footers, at most 1 allowed.')
2488 if not change_ids:
2489 # Generate the Change-Id automatically.
2490 message = git_footers.add_footer_change_id(
2491 message, GenerateGerritChangeId(message))
2492 change_desc.set_description(message)
2493 change_ids = git_footers.get_footer_change_id(message)
2494 assert len(change_ids) == 1
2495 change_id = change_ids[0]
2496
2497 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2498 if remote is '.':
2499 # If our upstream branch is local, we base our squashed commit on its
2500 # squashed version.
2501 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2502 # Check the squashed hash of the parent.
2503 parent = RunGit(['config',
2504 'branch.%s.gerritsquashhash' % upstream_branch_name],
2505 error_ok=True).strip()
2506 # Verify that the upstream branch has been uploaded too, otherwise
2507 # Gerrit will create additional CLs when uploading.
2508 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2509 RunGitSilent(['rev-parse', parent + ':'])):
2510 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2511 DieWithError(
2512 'Upload upstream branch %s first.\n'
2513 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2514 'version of depot_tools. If so, then re-upload it with:\n'
2515 ' git cl upload --squash\n' % upstream_branch_name)
2516 else:
2517 parent = self.GetCommonAncestorWithUpstream()
2518
2519 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2520 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2521 '-m', message]).strip()
2522 else:
2523 change_desc = ChangeDescription(
2524 options.message or CreateDescriptionFromLog(args))
2525 if not change_desc.description:
2526 DieWithError("Description is empty. Aborting...")
2527
2528 if not git_footers.get_footer_change_id(change_desc.description):
2529 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002530 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2531 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002532 ref_to_push = 'HEAD'
2533 parent = '%s/%s' % (gerrit_remote, branch)
2534 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2535
2536 assert change_desc
2537 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2538 ref_to_push)]).splitlines()
2539 if len(commits) > 1:
2540 print('WARNING: This will upload %d commits. Run the following command '
2541 'to see which commits will be uploaded: ' % len(commits))
2542 print('git log %s..%s' % (parent, ref_to_push))
2543 print('You can also use `git squash-branch` to squash these into a '
2544 'single commit.')
2545 ask_for_data('About to upload; enter to confirm.')
2546
2547 if options.reviewers or options.tbr_owners:
2548 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2549 change)
2550
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002551 # Extra options that can be specified at push time. Doc:
2552 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2553 refspec_opts = []
2554 if options.title:
2555 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2556 # reverse on its side.
2557 if '_' in options.title:
2558 print('WARNING: underscores in title will be converted to spaces.')
2559 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2560
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002561 if options.send_mail:
2562 if not change_desc.get_reviewers():
2563 DieWithError('Must specify reviewers to send email.')
2564 refspec_opts.append('notify=ALL')
2565 else:
2566 refspec_opts.append('notify=NONE')
2567
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002568 cc = self.GetCCList().split(',')
2569 if options.cc:
2570 cc.extend(options.cc)
2571 cc = filter(None, cc)
2572 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002573 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002574
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002575 if change_desc.get_reviewers():
2576 refspec_opts.extend('r=' + email.strip()
2577 for email in change_desc.get_reviewers())
2578
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002579 refspec_suffix = ''
2580 if refspec_opts:
2581 refspec_suffix = '%' + ','.join(refspec_opts)
2582 assert ' ' not in refspec_suffix, (
2583 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002584 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002585
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002586 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002587 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002588 print_stdout=True,
2589 # Flush after every line: useful for seeing progress when running as
2590 # recipe.
2591 filter_fn=lambda _: sys.stdout.flush())
2592
2593 if options.squash:
2594 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2595 change_numbers = [m.group(1)
2596 for m in map(regex.match, push_stdout.splitlines())
2597 if m]
2598 if len(change_numbers) != 1:
2599 DieWithError(
2600 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2601 'Change-Id: %s') % (len(change_numbers), change_id))
2602 self.SetIssue(change_numbers[0])
2603 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2604 ref_to_push])
2605 return 0
2606
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002607 def _AddChangeIdToCommitMessage(self, options, args):
2608 """Re-commits using the current message, assumes the commit hook is in
2609 place.
2610 """
2611 log_desc = options.message or CreateDescriptionFromLog(args)
2612 git_command = ['commit', '--amend', '-m', log_desc]
2613 RunGit(git_command)
2614 new_log_desc = CreateDescriptionFromLog(args)
2615 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002616 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002617 return new_log_desc
2618 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002619 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002620
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002621 def SetCQState(self, new_state):
2622 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2623 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2624 # self-discovery of label config for this CL using REST API.
2625 vote_map = {
2626 _CQState.NONE: 0,
2627 _CQState.DRY_RUN: 1,
2628 _CQState.COMMIT : 2,
2629 }
2630 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2631 labels={'Commit-Queue': vote_map[new_state]})
2632
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002633
2634_CODEREVIEW_IMPLEMENTATIONS = {
2635 'rietveld': _RietveldChangelistImpl,
2636 'gerrit': _GerritChangelistImpl,
2637}
2638
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002639
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002640def _add_codereview_select_options(parser):
2641 """Appends --gerrit and --rietveld options to force specific codereview."""
2642 parser.codereview_group = optparse.OptionGroup(
2643 parser, 'EXPERIMENTAL! Codereview override options')
2644 parser.add_option_group(parser.codereview_group)
2645 parser.codereview_group.add_option(
2646 '--gerrit', action='store_true',
2647 help='Force the use of Gerrit for codereview')
2648 parser.codereview_group.add_option(
2649 '--rietveld', action='store_true',
2650 help='Force the use of Rietveld for codereview')
2651
2652
2653def _process_codereview_select_options(parser, options):
2654 if options.gerrit and options.rietveld:
2655 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2656 options.forced_codereview = None
2657 if options.gerrit:
2658 options.forced_codereview = 'gerrit'
2659 elif options.rietveld:
2660 options.forced_codereview = 'rietveld'
2661
2662
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002663class ChangeDescription(object):
2664 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002665 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002666 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002667
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002668 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002669 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670
agable@chromium.org42c20792013-09-12 17:34:49 +00002671 @property # www.logilab.org/ticket/89786
2672 def description(self): # pylint: disable=E0202
2673 return '\n'.join(self._description_lines)
2674
2675 def set_description(self, desc):
2676 if isinstance(desc, basestring):
2677 lines = desc.splitlines()
2678 else:
2679 lines = [line.rstrip() for line in desc]
2680 while lines and not lines[0]:
2681 lines.pop(0)
2682 while lines and not lines[-1]:
2683 lines.pop(-1)
2684 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002685
piman@chromium.org336f9122014-09-04 02:16:55 +00002686 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002687 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002688 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002689 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002690 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002691 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002692
agable@chromium.org42c20792013-09-12 17:34:49 +00002693 # Get the set of R= and TBR= lines and remove them from the desciption.
2694 regexp = re.compile(self.R_LINE)
2695 matches = [regexp.match(line) for line in self._description_lines]
2696 new_desc = [l for i, l in enumerate(self._description_lines)
2697 if not matches[i]]
2698 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002699
agable@chromium.org42c20792013-09-12 17:34:49 +00002700 # Construct new unified R= and TBR= lines.
2701 r_names = []
2702 tbr_names = []
2703 for match in matches:
2704 if not match:
2705 continue
2706 people = cleanup_list([match.group(2).strip()])
2707 if match.group(1) == 'TBR':
2708 tbr_names.extend(people)
2709 else:
2710 r_names.extend(people)
2711 for name in r_names:
2712 if name not in reviewers:
2713 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002714 if add_owners_tbr:
2715 owners_db = owners.Database(change.RepositoryRoot(),
2716 fopen=file, os_path=os.path, glob=glob.glob)
2717 all_reviewers = set(tbr_names + reviewers)
2718 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2719 all_reviewers)
2720 tbr_names.extend(owners_db.reviewers_for(missing_files,
2721 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002722 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2723 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2724
2725 # Put the new lines in the description where the old first R= line was.
2726 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2727 if 0 <= line_loc < len(self._description_lines):
2728 if new_tbr_line:
2729 self._description_lines.insert(line_loc, new_tbr_line)
2730 if new_r_line:
2731 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002732 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002733 if new_r_line:
2734 self.append_footer(new_r_line)
2735 if new_tbr_line:
2736 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002737
2738 def prompt(self):
2739 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002740 self.set_description([
2741 '# Enter a description of the change.',
2742 '# This will be displayed on the codereview site.',
2743 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002744 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002745 '--------------------',
2746 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002747
agable@chromium.org42c20792013-09-12 17:34:49 +00002748 regexp = re.compile(self.BUG_LINE)
2749 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002750 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002751 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002752 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002753 if not content:
2754 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002755 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002756
2757 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002758 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2759 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002760 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002761 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002762
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002763 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002764 """Adds a footer line to the description.
2765
2766 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2767 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2768 that Gerrit footers are always at the end.
2769 """
2770 parsed_footer_line = git_footers.parse_footer(line)
2771 if parsed_footer_line:
2772 # Line is a gerrit footer in the form: Footer-Key: any value.
2773 # Thus, must be appended observing Gerrit footer rules.
2774 self.set_description(
2775 git_footers.add_footer(self.description,
2776 key=parsed_footer_line[0],
2777 value=parsed_footer_line[1]))
2778 return
2779
2780 if not self._description_lines:
2781 self._description_lines.append(line)
2782 return
2783
2784 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2785 if gerrit_footers:
2786 # git_footers.split_footers ensures that there is an empty line before
2787 # actual (gerrit) footers, if any. We have to keep it that way.
2788 assert top_lines and top_lines[-1] == ''
2789 top_lines, separator = top_lines[:-1], top_lines[-1:]
2790 else:
2791 separator = [] # No need for separator if there are no gerrit_footers.
2792
2793 prev_line = top_lines[-1] if top_lines else ''
2794 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2795 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2796 top_lines.append('')
2797 top_lines.append(line)
2798 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002799
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002800 def get_reviewers(self):
2801 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002802 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2803 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002804 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002805
2806
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002807def get_approving_reviewers(props):
2808 """Retrieves the reviewers that approved a CL from the issue properties with
2809 messages.
2810
2811 Note that the list may contain reviewers that are not committer, thus are not
2812 considered by the CQ.
2813 """
2814 return sorted(
2815 set(
2816 message['sender']
2817 for message in props['messages']
2818 if message['approval'] and message['sender'] in props['reviewers']
2819 )
2820 )
2821
2822
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002823def FindCodereviewSettingsFile(filename='codereview.settings'):
2824 """Finds the given file starting in the cwd and going up.
2825
2826 Only looks up to the top of the repository unless an
2827 'inherit-review-settings-ok' file exists in the root of the repository.
2828 """
2829 inherit_ok_file = 'inherit-review-settings-ok'
2830 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002831 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2833 root = '/'
2834 while True:
2835 if filename in os.listdir(cwd):
2836 if os.path.isfile(os.path.join(cwd, filename)):
2837 return open(os.path.join(cwd, filename))
2838 if cwd == root:
2839 break
2840 cwd = os.path.dirname(cwd)
2841
2842
2843def LoadCodereviewSettingsFromFile(fileobj):
2844 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002845 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002847 def SetProperty(name, setting, unset_error_ok=False):
2848 fullname = 'rietveld.' + name
2849 if setting in keyvals:
2850 RunGit(['config', fullname, keyvals[setting]])
2851 else:
2852 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2853
2854 SetProperty('server', 'CODE_REVIEW_SERVER')
2855 # Only server setting is required. Other settings can be absent.
2856 # In that case, we ignore errors raised during option deletion attempt.
2857 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002858 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2860 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002861 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002862 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002863 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2864 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002865 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002866 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002867 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002868 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2869 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002871 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002872 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002873
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002874 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002875 RunGit(['config', 'gerrit.squash-uploads',
2876 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002877
tandrii@chromium.org28253532016-04-14 13:46:56 +00002878 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002879 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002880 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002882 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2883 #should be of the form
2884 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2885 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2886 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2887 keyvals['ORIGIN_URL_CONFIG']])
2888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002890def urlretrieve(source, destination):
2891 """urllib is broken for SSL connections via a proxy therefore we
2892 can't use urllib.urlretrieve()."""
2893 with open(destination, 'w') as f:
2894 f.write(urllib2.urlopen(source).read())
2895
2896
ukai@chromium.org712d6102013-11-27 00:52:58 +00002897def hasSheBang(fname):
2898 """Checks fname is a #! script."""
2899 with open(fname) as f:
2900 return f.read(2).startswith('#!')
2901
2902
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002903# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2904def DownloadHooks(*args, **kwargs):
2905 pass
2906
2907
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002908def DownloadGerritHook(force):
2909 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002910
2911 Args:
2912 force: True to update hooks. False to install hooks if not present.
2913 """
2914 if not settings.GetIsGerrit():
2915 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002916 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002917 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2918 if not os.access(dst, os.X_OK):
2919 if os.path.exists(dst):
2920 if not force:
2921 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002922 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002923 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002924 if not hasSheBang(dst):
2925 DieWithError('Not a script: %s\n'
2926 'You need to download from\n%s\n'
2927 'into .git/hooks/commit-msg and '
2928 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002929 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2930 except Exception:
2931 if os.path.exists(dst):
2932 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002933 DieWithError('\nFailed to download hooks.\n'
2934 'You need to download from\n%s\n'
2935 'into .git/hooks/commit-msg and '
2936 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002937
2938
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002939
2940def GetRietveldCodereviewSettingsInteractively():
2941 """Prompt the user for settings."""
2942 server = settings.GetDefaultServerUrl(error_ok=True)
2943 prompt = 'Rietveld server (host[:port])'
2944 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2945 newserver = ask_for_data(prompt + ':')
2946 if not server and not newserver:
2947 newserver = DEFAULT_SERVER
2948 if newserver:
2949 newserver = gclient_utils.UpgradeToHttps(newserver)
2950 if newserver != server:
2951 RunGit(['config', 'rietveld.server', newserver])
2952
2953 def SetProperty(initial, caption, name, is_url):
2954 prompt = caption
2955 if initial:
2956 prompt += ' ("x" to clear) [%s]' % initial
2957 new_val = ask_for_data(prompt + ':')
2958 if new_val == 'x':
2959 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2960 elif new_val:
2961 if is_url:
2962 new_val = gclient_utils.UpgradeToHttps(new_val)
2963 if new_val != initial:
2964 RunGit(['config', 'rietveld.' + name, new_val])
2965
2966 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2967 SetProperty(settings.GetDefaultPrivateFlag(),
2968 'Private flag (rietveld only)', 'private', False)
2969 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2970 'tree-status-url', False)
2971 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2972 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2973 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2974 'run-post-upload-hook', False)
2975
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002976@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002977def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002978 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002979
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002980 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002981 'For Gerrit, see http://crbug.com/603116.')
2982 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002983 parser.add_option('--activate-update', action='store_true',
2984 help='activate auto-updating [rietveld] section in '
2985 '.git/config')
2986 parser.add_option('--deactivate-update', action='store_true',
2987 help='deactivate auto-updating [rietveld] section in '
2988 '.git/config')
2989 options, args = parser.parse_args(args)
2990
2991 if options.deactivate_update:
2992 RunGit(['config', 'rietveld.autoupdate', 'false'])
2993 return
2994
2995 if options.activate_update:
2996 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2997 return
2998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002999 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003000 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003001 return 0
3002
3003 url = args[0]
3004 if not url.endswith('codereview.settings'):
3005 url = os.path.join(url, 'codereview.settings')
3006
3007 # Load code review settings and download hooks (if available).
3008 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3009 return 0
3010
3011
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003012def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003013 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003014 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3015 branch = ShortBranchName(branchref)
3016 _, args = parser.parse_args(args)
3017 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003018 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003019 return RunGit(['config', 'branch.%s.base-url' % branch],
3020 error_ok=False).strip()
3021 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003022 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003023 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3024 error_ok=False).strip()
3025
3026
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003027def color_for_status(status):
3028 """Maps a Changelist status to color, for CMDstatus and other tools."""
3029 return {
3030 'unsent': Fore.RED,
3031 'waiting': Fore.BLUE,
3032 'reply': Fore.YELLOW,
3033 'lgtm': Fore.GREEN,
3034 'commit': Fore.MAGENTA,
3035 'closed': Fore.CYAN,
3036 'error': Fore.WHITE,
3037 }.get(status, Fore.WHITE)
3038
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003039
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003040def get_cl_statuses(changes, fine_grained, max_processes=None):
3041 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003042
3043 If fine_grained is true, this will fetch CL statuses from the server.
3044 Otherwise, simply indicate if there's a matching url for the given branches.
3045
3046 If max_processes is specified, it is used as the maximum number of processes
3047 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3048 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003049
3050 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003051 """
3052 # Silence upload.py otherwise it becomes unwieldly.
3053 upload.verbosity = 0
3054
3055 if fine_grained:
3056 # Process one branch synchronously to work through authentication, then
3057 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003058 if changes:
3059 fetch = lambda cl: (cl, cl.GetStatus())
3060 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003061
kmarshall3bff56b2016-06-06 18:31:47 -07003062 if not changes:
3063 # Exit early if there was only one branch to fetch.
3064 return
3065
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003066 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003067 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003068 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003069 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003070 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003071
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003072 fetched_cls = set()
3073 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003074 while True:
3075 try:
3076 row = it.next(timeout=5)
3077 except multiprocessing.TimeoutError:
3078 break
3079
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003080 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003081 yield row
3082
3083 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003084 for cl in set(changes_to_fetch) - fetched_cls:
3085 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003086
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003087 else:
3088 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003089 for cl in changes:
3090 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003091
rmistry@google.com2dd99862015-06-22 12:22:18 +00003092
3093def upload_branch_deps(cl, args):
3094 """Uploads CLs of local branches that are dependents of the current branch.
3095
3096 If the local branch dependency tree looks like:
3097 test1 -> test2.1 -> test3.1
3098 -> test3.2
3099 -> test2.2 -> test3.3
3100
3101 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3102 run on the dependent branches in this order:
3103 test2.1, test3.1, test3.2, test2.2, test3.3
3104
3105 Note: This function does not rebase your local dependent branches. Use it when
3106 you make a change to the parent branch that will not conflict with its
3107 dependent branches, and you would like their dependencies updated in
3108 Rietveld.
3109 """
3110 if git_common.is_dirty_git_tree('upload-branch-deps'):
3111 return 1
3112
3113 root_branch = cl.GetBranch()
3114 if root_branch is None:
3115 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3116 'Get on a branch!')
3117 if not cl.GetIssue() or not cl.GetPatchset():
3118 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3119 'patchset dependencies without an uploaded CL.')
3120
3121 branches = RunGit(['for-each-ref',
3122 '--format=%(refname:short) %(upstream:short)',
3123 'refs/heads'])
3124 if not branches:
3125 print('No local branches found.')
3126 return 0
3127
3128 # Create a dictionary of all local branches to the branches that are dependent
3129 # on it.
3130 tracked_to_dependents = collections.defaultdict(list)
3131 for b in branches.splitlines():
3132 tokens = b.split()
3133 if len(tokens) == 2:
3134 branch_name, tracked = tokens
3135 tracked_to_dependents[tracked].append(branch_name)
3136
vapiera7fbd5a2016-06-16 09:17:49 -07003137 print()
3138 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003139 dependents = []
3140 def traverse_dependents_preorder(branch, padding=''):
3141 dependents_to_process = tracked_to_dependents.get(branch, [])
3142 padding += ' '
3143 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003144 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003145 dependents.append(dependent)
3146 traverse_dependents_preorder(dependent, padding)
3147 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003148 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003149
3150 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003151 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003152 return 0
3153
vapiera7fbd5a2016-06-16 09:17:49 -07003154 print('This command will checkout all dependent branches and run '
3155 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003156 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3157
andybons@chromium.org962f9462016-02-03 20:00:42 +00003158 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003159 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003160 args.extend(['-t', 'Updated patchset dependency'])
3161
rmistry@google.com2dd99862015-06-22 12:22:18 +00003162 # Record all dependents that failed to upload.
3163 failures = {}
3164 # Go through all dependents, checkout the branch and upload.
3165 try:
3166 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003167 print()
3168 print('--------------------------------------')
3169 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003170 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003171 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003172 try:
3173 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003174 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003175 failures[dependent_branch] = 1
3176 except: # pylint: disable=W0702
3177 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003178 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003179 finally:
3180 # Swap back to the original root branch.
3181 RunGit(['checkout', '-q', root_branch])
3182
vapiera7fbd5a2016-06-16 09:17:49 -07003183 print()
3184 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003185 for dependent_branch in dependents:
3186 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003187 print(' %s : %s' % (dependent_branch, upload_status))
3188 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003189
3190 return 0
3191
3192
kmarshall3bff56b2016-06-06 18:31:47 -07003193def CMDarchive(parser, args):
3194 """Archives and deletes branches associated with closed changelists."""
3195 parser.add_option(
3196 '-j', '--maxjobs', action='store', type=int,
3197 help='The maximum number of jobs to use when retrieving review status')
3198 parser.add_option(
3199 '-f', '--force', action='store_true',
3200 help='Bypasses the confirmation prompt.')
3201
3202 auth.add_auth_options(parser)
3203 options, args = parser.parse_args(args)
3204 if args:
3205 parser.error('Unsupported args: %s' % ' '.join(args))
3206 auth_config = auth.extract_auth_config_from_options(options)
3207
3208 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3209 if not branches:
3210 return 0
3211
vapiera7fbd5a2016-06-16 09:17:49 -07003212 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003213 changes = [Changelist(branchref=b, auth_config=auth_config)
3214 for b in branches.splitlines()]
3215 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3216 statuses = get_cl_statuses(changes,
3217 fine_grained=True,
3218 max_processes=options.maxjobs)
3219 proposal = [(cl.GetBranch(),
3220 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3221 for cl, status in statuses
3222 if status == 'closed']
3223 proposal.sort()
3224
3225 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003226 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003227 return 0
3228
3229 current_branch = GetCurrentBranch()
3230
vapiera7fbd5a2016-06-16 09:17:49 -07003231 print('\nBranches with closed issues that will be archived:\n')
3232 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003233 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003234 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003235
3236 if any(branch == current_branch for branch, _ in proposal):
3237 print('You are currently on a branch \'%s\' which is associated with a '
3238 'closed codereview issue, so archive cannot proceed. Please '
3239 'checkout another branch and run this command again.' %
3240 current_branch)
3241 return 1
3242
3243 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003244 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3245 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003246 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003247 return 1
3248
3249 for branch, tagname in proposal:
3250 RunGit(['tag', tagname, branch])
3251 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003252 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003253
3254 return 0
3255
3256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003258 """Show status of changelists.
3259
3260 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003261 - Red not sent for review or broken
3262 - Blue waiting for review
3263 - Yellow waiting for you to reply to review
3264 - Green LGTM'ed
3265 - Magenta in the commit queue
3266 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003267
3268 Also see 'git cl comments'.
3269 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003270 parser.add_option('--field',
3271 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003272 parser.add_option('-f', '--fast', action='store_true',
3273 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003274 parser.add_option(
3275 '-j', '--maxjobs', action='store', type=int,
3276 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003277
3278 auth.add_auth_options(parser)
3279 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003280 if args:
3281 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003282 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003285 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003287 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288 elif options.field == 'id':
3289 issueid = cl.GetIssue()
3290 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003291 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292 elif options.field == 'patch':
3293 patchset = cl.GetPatchset()
3294 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003295 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 elif options.field == 'url':
3297 url = cl.GetIssueURL()
3298 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003299 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003300 return 0
3301
3302 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3303 if not branches:
3304 print('No local branch found.')
3305 return 0
3306
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003307 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003308 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003309 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003310 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003311 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003312 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003313 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003314
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003315 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003316 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3317 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3318 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003319 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003320 c, status = output.next()
3321 branch_statuses[c.GetBranch()] = status
3322 status = branch_statuses.pop(branch)
3323 url = cl.GetIssueURL()
3324 if url and (not status or status == 'error'):
3325 # The issue probably doesn't exist anymore.
3326 url += ' (broken)'
3327
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003328 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003329 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003330 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003331 color = ''
3332 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003333 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003334 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003335 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003336 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003337
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003338 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003339 print()
3340 print('Current branch:',)
3341 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003342 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003343 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003344 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003345 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003346 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003347 print('Issue description:')
3348 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003349 return 0
3350
3351
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003352def colorize_CMDstatus_doc():
3353 """To be called once in main() to add colors to git cl status help."""
3354 colors = [i for i in dir(Fore) if i[0].isupper()]
3355
3356 def colorize_line(line):
3357 for color in colors:
3358 if color in line.upper():
3359 # Extract whitespaces first and the leading '-'.
3360 indent = len(line) - len(line.lstrip(' ')) + 1
3361 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3362 return line
3363
3364 lines = CMDstatus.__doc__.splitlines()
3365 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3366
3367
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003368@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003369def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003370 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371
3372 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003373 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003374 parser.add_option('-r', '--reverse', action='store_true',
3375 help='Lookup the branch(es) for the specified issues. If '
3376 'no issues are specified, all branches with mapped '
3377 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003378 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003379 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003380 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381
dnj@chromium.org406c4402015-03-03 17:22:28 +00003382 if options.reverse:
3383 branches = RunGit(['for-each-ref', 'refs/heads',
3384 '--format=%(refname:short)']).splitlines()
3385
3386 # Reverse issue lookup.
3387 issue_branch_map = {}
3388 for branch in branches:
3389 cl = Changelist(branchref=branch)
3390 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3391 if not args:
3392 args = sorted(issue_branch_map.iterkeys())
3393 for issue in args:
3394 if not issue:
3395 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003396 print('Branch for issue number %s: %s' % (
3397 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003398 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003399 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003400 if len(args) > 0:
3401 try:
3402 issue = int(args[0])
3403 except ValueError:
3404 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003405 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003406 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003407 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 return 0
3409
3410
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003411def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003412 """Shows or posts review comments for any changelist."""
3413 parser.add_option('-a', '--add-comment', dest='comment',
3414 help='comment to add to an issue')
3415 parser.add_option('-i', dest='issue',
3416 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003417 parser.add_option('-j', '--json-file',
3418 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003419 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003420 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003421 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003422
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003423 issue = None
3424 if options.issue:
3425 try:
3426 issue = int(options.issue)
3427 except ValueError:
3428 DieWithError('A review issue id is expected to be a number')
3429
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003430 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003431
3432 if options.comment:
3433 cl.AddComment(options.comment)
3434 return 0
3435
3436 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003437 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003438 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003439 summary.append({
3440 'date': message['date'],
3441 'lgtm': False,
3442 'message': message['text'],
3443 'not_lgtm': False,
3444 'sender': message['sender'],
3445 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003446 if message['disapproval']:
3447 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003448 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003449 elif message['approval']:
3450 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003451 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003452 elif message['sender'] == data['owner_email']:
3453 color = Fore.MAGENTA
3454 else:
3455 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003456 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003457 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003458 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003459 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003460 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003461 if options.json_file:
3462 with open(options.json_file, 'wb') as f:
3463 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003464 return 0
3465
3466
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003467@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003468def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003469 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003470 parser.add_option('-d', '--display', action='store_true',
3471 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003472 parser.add_option('-n', '--new-description',
3473 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003474
3475 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003476 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003477 options, args = parser.parse_args(args)
3478 _process_codereview_select_options(parser, options)
3479
3480 target_issue = None
3481 if len(args) > 0:
3482 issue_arg = ParseIssueNumberArgument(args[0])
3483 if not issue_arg.valid:
3484 parser.print_help()
3485 return 1
3486 target_issue = issue_arg.issue
3487
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003488 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003489
3490 cl = Changelist(
3491 auth_config=auth_config, issue=target_issue,
3492 codereview=options.forced_codereview)
3493
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003494 if not cl.GetIssue():
3495 DieWithError('This branch has no associated changelist.')
3496 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003497
smut@google.com34fb6b12015-07-13 20:03:26 +00003498 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003500 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003501
3502 if options.new_description:
3503 text = options.new_description
3504 if text == '-':
3505 text = '\n'.join(l.rstrip() for l in sys.stdin)
3506
3507 description.set_description(text)
3508 else:
3509 description.prompt()
3510
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003511 if cl.GetDescription() != description.description:
3512 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003513 return 0
3514
3515
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003516def CreateDescriptionFromLog(args):
3517 """Pulls out the commit log to use as a base for the CL description."""
3518 log_args = []
3519 if len(args) == 1 and not args[0].endswith('.'):
3520 log_args = [args[0] + '..']
3521 elif len(args) == 1 and args[0].endswith('...'):
3522 log_args = [args[0][:-1]]
3523 elif len(args) == 2:
3524 log_args = [args[0] + '..' + args[1]]
3525 else:
3526 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003527 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003528
3529
thestig@chromium.org44202a22014-03-11 19:22:18 +00003530def CMDlint(parser, args):
3531 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003532 parser.add_option('--filter', action='append', metavar='-x,+y',
3533 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003534 auth.add_auth_options(parser)
3535 options, args = parser.parse_args(args)
3536 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003537
3538 # Access to a protected member _XX of a client class
3539 # pylint: disable=W0212
3540 try:
3541 import cpplint
3542 import cpplint_chromium
3543 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003544 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003545 return 1
3546
3547 # Change the current working directory before calling lint so that it
3548 # shows the correct base.
3549 previous_cwd = os.getcwd()
3550 os.chdir(settings.GetRoot())
3551 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003552 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003553 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3554 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003555 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003557 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003558
3559 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003560 command = args + files
3561 if options.filter:
3562 command = ['--filter=' + ','.join(options.filter)] + command
3563 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003564
3565 white_regex = re.compile(settings.GetLintRegex())
3566 black_regex = re.compile(settings.GetLintIgnoreRegex())
3567 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3568 for filename in filenames:
3569 if white_regex.match(filename):
3570 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003572 else:
3573 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3574 extra_check_functions)
3575 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003576 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003577 finally:
3578 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003579 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003580 if cpplint._cpplint_state.error_count != 0:
3581 return 1
3582 return 0
3583
3584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003586 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003587 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003589 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003590 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003591 auth.add_auth_options(parser)
3592 options, args = parser.parse_args(args)
3593 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003594
sbc@chromium.org71437c02015-04-09 19:29:40 +00003595 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003596 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597 return 1
3598
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003599 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003600 if args:
3601 base_branch = args[0]
3602 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003603 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003604 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003605
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003606 cl.RunHook(
3607 committing=not options.upload,
3608 may_prompt=False,
3609 verbose=options.verbose,
3610 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003611 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612
3613
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003614def GenerateGerritChangeId(message):
3615 """Returns Ixxxxxx...xxx change id.
3616
3617 Works the same way as
3618 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3619 but can be called on demand on all platforms.
3620
3621 The basic idea is to generate git hash of a state of the tree, original commit
3622 message, author/committer info and timestamps.
3623 """
3624 lines = []
3625 tree_hash = RunGitSilent(['write-tree'])
3626 lines.append('tree %s' % tree_hash.strip())
3627 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3628 if code == 0:
3629 lines.append('parent %s' % parent.strip())
3630 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3631 lines.append('author %s' % author.strip())
3632 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3633 lines.append('committer %s' % committer.strip())
3634 lines.append('')
3635 # Note: Gerrit's commit-hook actually cleans message of some lines and
3636 # whitespace. This code is not doing this, but it clearly won't decrease
3637 # entropy.
3638 lines.append(message)
3639 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3640 stdin='\n'.join(lines))
3641 return 'I%s' % change_hash.strip()
3642
3643
wittman@chromium.org455dc922015-01-26 20:15:50 +00003644def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3645 """Computes the remote branch ref to use for the CL.
3646
3647 Args:
3648 remote (str): The git remote for the CL.
3649 remote_branch (str): The git remote branch for the CL.
3650 target_branch (str): The target branch specified by the user.
3651 pending_prefix (str): The pending prefix from the settings.
3652 """
3653 if not (remote and remote_branch):
3654 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003655
wittman@chromium.org455dc922015-01-26 20:15:50 +00003656 if target_branch:
3657 # Cannonicalize branch references to the equivalent local full symbolic
3658 # refs, which are then translated into the remote full symbolic refs
3659 # below.
3660 if '/' not in target_branch:
3661 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3662 else:
3663 prefix_replacements = (
3664 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3665 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3666 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3667 )
3668 match = None
3669 for regex, replacement in prefix_replacements:
3670 match = re.search(regex, target_branch)
3671 if match:
3672 remote_branch = target_branch.replace(match.group(0), replacement)
3673 break
3674 if not match:
3675 # This is a branch path but not one we recognize; use as-is.
3676 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003677 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3678 # Handle the refs that need to land in different refs.
3679 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003680
wittman@chromium.org455dc922015-01-26 20:15:50 +00003681 # Create the true path to the remote branch.
3682 # Does the following translation:
3683 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3684 # * refs/remotes/origin/master -> refs/heads/master
3685 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3686 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3687 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3688 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3689 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3690 'refs/heads/')
3691 elif remote_branch.startswith('refs/remotes/branch-heads'):
3692 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3693 # If a pending prefix exists then replace refs/ with it.
3694 if pending_prefix:
3695 remote_branch = remote_branch.replace('refs/', pending_prefix)
3696 return remote_branch
3697
3698
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003699def cleanup_list(l):
3700 """Fixes a list so that comma separated items are put as individual items.
3701
3702 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3703 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3704 """
3705 items = sum((i.split(',') for i in l), [])
3706 stripped_items = (i.strip() for i in items)
3707 return sorted(filter(None, stripped_items))
3708
3709
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003710@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003711def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003712 """Uploads the current changelist to codereview.
3713
3714 Can skip dependency patchset uploads for a branch by running:
3715 git config branch.branch_name.skip-deps-uploads True
3716 To unset run:
3717 git config --unset branch.branch_name.skip-deps-uploads
3718 Can also set the above globally by using the --global flag.
3719 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003720 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3721 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003722 parser.add_option('--bypass-watchlists', action='store_true',
3723 dest='bypass_watchlists',
3724 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003725 parser.add_option('-f', action='store_true', dest='force',
3726 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003727 parser.add_option('-m', dest='message', help='message for patchset')
tandriib80458a2016-06-23 12:20:07 -07003728 parser.add_option('--message-file', dest='message_file',
3729 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003730 parser.add_option('-t', dest='title',
3731 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003732 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003733 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003734 help='reviewer email addresses')
3735 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003736 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003737 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003738 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003739 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003740 parser.add_option('--emulate_svn_auto_props',
3741 '--emulate-svn-auto-props',
3742 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003743 dest="emulate_svn_auto_props",
3744 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003745 parser.add_option('-c', '--use-commit-queue', action='store_true',
3746 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003747 parser.add_option('--private', action='store_true',
3748 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003749 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003750 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003751 metavar='TARGET',
3752 help='Apply CL to remote ref TARGET. ' +
3753 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003754 parser.add_option('--squash', action='store_true',
3755 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003756 parser.add_option('--no-squash', action='store_true',
3757 help='Don\'t squash multiple commits into one ' +
3758 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003759 parser.add_option('--email', default=None,
3760 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003761 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3762 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003763 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3764 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003765 help='Send the patchset to do a CQ dry run right after '
3766 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003767 parser.add_option('--dependencies', action='store_true',
3768 help='Uploads CLs of all the local branches that depend on '
3769 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003770
rmistry@google.com2dd99862015-06-22 12:22:18 +00003771 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003772 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003773 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003774 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003775 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003776 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003777 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003778
sbc@chromium.org71437c02015-04-09 19:29:40 +00003779 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003780 return 1
3781
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003782 options.reviewers = cleanup_list(options.reviewers)
3783 options.cc = cleanup_list(options.cc)
3784
tandriib80458a2016-06-23 12:20:07 -07003785 if options.message_file:
3786 if options.message:
3787 parser.error('only one of --message and --message-file allowed.')
3788 options.message = gclient_utils.FileRead(options.message_file)
3789 options.message_file = None
3790
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003791 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3792 settings.GetIsGerrit()
3793
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003794 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003795 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003796
3797
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003798def IsSubmoduleMergeCommit(ref):
3799 # When submodules are added to the repo, we expect there to be a single
3800 # non-git-svn merge commit at remote HEAD with a signature comment.
3801 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003802 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003803 return RunGit(cmd) != ''
3804
3805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003807 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003808
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003809 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3810 upstream and closes the issue automatically and atomically.
3811
3812 Otherwise (in case of Rietveld):
3813 Squashes branch into a single commit.
3814 Updates changelog with metadata (e.g. pointer to review).
3815 Pushes/dcommits the code upstream.
3816 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003817 """
3818 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3819 help='bypass upload presubmit hook')
3820 parser.add_option('-m', dest='message',
3821 help="override review description")
3822 parser.add_option('-f', action='store_true', dest='force',
3823 help="force yes to questions (don't prompt)")
3824 parser.add_option('-c', dest='contributor',
3825 help="external contributor for patch (appended to " +
3826 "description and used as author for git). Should be " +
3827 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003828 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003829 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003831 auth_config = auth.extract_auth_config_from_options(options)
3832
3833 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003835 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3836 if cl.IsGerrit():
3837 if options.message:
3838 # This could be implemented, but it requires sending a new patch to
3839 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3840 # Besides, Gerrit has the ability to change the commit message on submit
3841 # automatically, thus there is no need to support this option (so far?).
3842 parser.error('-m MESSAGE option is not supported for Gerrit.')
3843 if options.contributor:
3844 parser.error(
3845 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3846 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3847 'the contributor\'s "name <email>". If you can\'t upload such a '
3848 'commit for review, contact your repository admin and request'
3849 '"Forge-Author" permission.')
3850 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3851 options.verbose)
3852
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003853 current = cl.GetBranch()
3854 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3855 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003856 print()
3857 print('Attempting to push branch %r into another local branch!' % current)
3858 print()
3859 print('Either reparent this branch on top of origin/master:')
3860 print(' git reparent-branch --root')
3861 print()
3862 print('OR run `git rebase-update` if you think the parent branch is ')
3863 print('already committed.')
3864 print()
3865 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003866 return 1
3867
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003868 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869 # Default to merging against our best guess of the upstream branch.
3870 args = [cl.GetUpstreamBranch()]
3871
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003872 if options.contributor:
3873 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003874 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003875 return 1
3876
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003877 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003878 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003879
sbc@chromium.org71437c02015-04-09 19:29:40 +00003880 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881 return 1
3882
3883 # This rev-list syntax means "show all commits not in my branch that
3884 # are in base_branch".
3885 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3886 base_branch]).splitlines()
3887 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003888 print('Base branch "%s" has %d commits '
3889 'not in this branch.' % (base_branch, len(upstream_commits)))
3890 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003891 return 1
3892
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003893 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003894 svn_head = None
3895 if cmd == 'dcommit' or base_has_submodules:
3896 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3897 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003900 # If the base_head is a submodule merge commit, the first parent of the
3901 # base_head should be a git-svn commit, which is what we're interested in.
3902 base_svn_head = base_branch
3903 if base_has_submodules:
3904 base_svn_head += '^1'
3905
3906 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003907 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print('This branch has %d additional commits not upstreamed yet.'
3909 % len(extra_commits.splitlines()))
3910 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3911 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 return 1
3913
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003914 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003915 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003916 author = None
3917 if options.contributor:
3918 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003919 hook_results = cl.RunHook(
3920 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003921 may_prompt=not options.force,
3922 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003923 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003924 if not hook_results.should_continue():
3925 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003927 # Check the tree status if the tree status URL is set.
3928 status = GetTreeStatus()
3929 if 'closed' == status:
3930 print('The tree is closed. Please wait for it to reopen. Use '
3931 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3932 return 1
3933 elif 'unknown' == status:
3934 print('Unable to determine tree status. Please verify manually and '
3935 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3936 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003938 change_desc = ChangeDescription(options.message)
3939 if not change_desc.description and cl.GetIssue():
3940 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003942 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003943 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003944 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003945 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003946 print('No description set.')
3947 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003948 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003950 # Keep a separate copy for the commit message, because the commit message
3951 # contains the link to the Rietveld issue, while the Rietveld message contains
3952 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003953 # Keep a separate copy for the commit message.
3954 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003955 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003956
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003957 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003958 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003959 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003960 # after it. Add a period on a new line to circumvent this. Also add a space
3961 # before the period to make sure that Gitiles continues to correctly resolve
3962 # the URL.
3963 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003965 commit_desc.append_footer('Patch from %s.' % options.contributor)
3966
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003967 print('Description:')
3968 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003969
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003970 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003972 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003974 # We want to squash all this branch's commits into one commit with the proper
3975 # description. We do this by doing a "reset --soft" to the base branch (which
3976 # keeps the working copy the same), then dcommitting that. If origin/master
3977 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3978 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003979 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003980 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3981 # Delete the branches if they exist.
3982 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3983 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3984 result = RunGitWithCode(showref_cmd)
3985 if result[0] == 0:
3986 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987
3988 # We might be in a directory that's present in this branch but not in the
3989 # trunk. Move up to the top of the tree so that git commands that expect a
3990 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003991 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992 if rel_base_path:
3993 os.chdir(rel_base_path)
3994
3995 # Stuff our change into the merge branch.
3996 # We wrap in a try...finally block so if anything goes wrong,
3997 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003998 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003999 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004000 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004001 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004002 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004003 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004004 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004006 RunGit(
4007 [
4008 'commit', '--author', options.contributor,
4009 '-m', commit_desc.description,
4010 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004011 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004012 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004013 if base_has_submodules:
4014 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4015 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4016 RunGit(['checkout', CHERRY_PICK_BRANCH])
4017 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004018 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004019 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004020 mirror = settings.GetGitMirror(remote)
4021 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004022 pending_prefix = settings.GetPendingRefPrefix()
4023 if not pending_prefix or branch.startswith(pending_prefix):
4024 # If not using refs/pending/heads/* at all, or target ref is already set
4025 # to pending, then push to the target ref directly.
4026 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004027 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004028 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004029 else:
4030 # Cherry-pick the change on top of pending ref and then push it.
4031 assert branch.startswith('refs/'), branch
4032 assert pending_prefix[-1] == '/', pending_prefix
4033 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004034 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004035 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004036 if retcode == 0:
4037 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038 else:
4039 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004040 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004041 'svn', 'dcommit',
4042 '-C%s' % options.similarity,
4043 '--no-rebase', '--rmdir',
4044 ]
4045 if settings.GetForceHttpsCommitUrl():
4046 # Allow forcing https commit URLs for some projects that don't allow
4047 # committing to http URLs (like Google Code).
4048 remote_url = cl.GetGitSvnRemoteUrl()
4049 if urlparse.urlparse(remote_url).scheme == 'http':
4050 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004051 cmd_args.append('--commit-url=%s' % remote_url)
4052 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004053 if 'Committed r' in output:
4054 revision = re.match(
4055 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4056 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057 finally:
4058 # And then swap back to the original branch and clean up.
4059 RunGit(['checkout', '-q', cl.GetBranch()])
4060 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004061 if base_has_submodules:
4062 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004064 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004065 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004066 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004067
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004068 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004069 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004070 try:
4071 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4072 # We set pushed_to_pending to False, since it made it all the way to the
4073 # real ref.
4074 pushed_to_pending = False
4075 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004076 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004079 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004081 if not to_pending:
4082 if viewvc_url and revision:
4083 change_desc.append_footer(
4084 'Committed: %s%s' % (viewvc_url, revision))
4085 elif revision:
4086 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004087 print('Closing issue '
4088 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004089 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004090 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004091 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004092 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004093 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004094 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004095 if options.bypass_hooks:
4096 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4097 else:
4098 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004099 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004100 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004101
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004102 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004103 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004104 print('The commit is in the pending queue (%s).' % pending_ref)
4105 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4106 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004107
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004108 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4109 if os.path.isfile(hook):
4110 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004111
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004112 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004113
4114
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004115def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004116 print()
4117 print('Waiting for commit to be landed on %s...' % real_ref)
4118 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004119 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4120 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004121 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004122
4123 loop = 0
4124 while True:
4125 sys.stdout.write('fetching (%d)... \r' % loop)
4126 sys.stdout.flush()
4127 loop += 1
4128
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004129 if mirror:
4130 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004131 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4132 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4133 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4134 for commit in commits.splitlines():
4135 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004137 return commit
4138
4139 current_rev = to_rev
4140
4141
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004142def PushToGitPending(remote, pending_ref, upstream_ref):
4143 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4144
4145 Returns:
4146 (retcode of last operation, output log of last operation).
4147 """
4148 assert pending_ref.startswith('refs/'), pending_ref
4149 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4150 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4151 code = 0
4152 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004153 max_attempts = 3
4154 attempts_left = max_attempts
4155 while attempts_left:
4156 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004157 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004158 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004159
4160 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004162 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004163 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004164 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004165 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004166 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004168 continue
4169
4170 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004171 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004172 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004173 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004174 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4176 'the following files have merge conflicts:' % pending_ref)
4177 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4178 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004179 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004180 return code, out
4181
4182 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004183 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004184 code, out = RunGitWithCode(
4185 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4186 if code == 0:
4187 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004189 return code, out
4190
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004192 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004193 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004194 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004195 print('Fatal push error. Make sure your .netrc credentials and git '
4196 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004197 return code, out
4198
vapiera7fbd5a2016-06-16 09:17:49 -07004199 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004200 return code, out
4201
4202
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004203def IsFatalPushFailure(push_stdout):
4204 """True if retrying push won't help."""
4205 return '(prohibited by Gerrit)' in push_stdout
4206
4207
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004208@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004210 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004211 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004212 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004213 # If it looks like previous commits were mirrored with git-svn.
4214 message = """This repository appears to be a git-svn mirror, but no
4215upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4216 else:
4217 message = """This doesn't appear to be an SVN repository.
4218If your project has a true, writeable git repository, you probably want to run
4219'git cl land' instead.
4220If your project has a git mirror of an upstream SVN master, you probably need
4221to run 'git svn init'.
4222
4223Using the wrong command might cause your commit to appear to succeed, and the
4224review to be closed, without actually landing upstream. If you choose to
4225proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004226 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004227 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004228 # TODO(tandrii): kill this post SVN migration with
4229 # https://codereview.chromium.org/2076683002
4230 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4231 'Please let us know of this project you are committing to:'
4232 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233 return SendUpstream(parser, args, 'dcommit')
4234
4235
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004236@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004237def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004238 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004239 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240 print('This appears to be an SVN repository.')
4241 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004242 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004243 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004244 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245
4246
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004247@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004248def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004249 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 parser.add_option('-b', dest='newbranch',
4251 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004252 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004253 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004254 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4255 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004256 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004257 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004258 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004259 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004260 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004261 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004262
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004263
4264 group = optparse.OptionGroup(
4265 parser,
4266 'Options for continuing work on the current issue uploaded from a '
4267 'different clone (e.g. different machine). Must be used independently '
4268 'from the other options. No issue number should be specified, and the '
4269 'branch must have an issue number associated with it')
4270 group.add_option('--reapply', action='store_true', dest='reapply',
4271 help='Reset the branch and reapply the issue.\n'
4272 'CAUTION: This will undo any local changes in this '
4273 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004274
4275 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004276 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004277 parser.add_option_group(group)
4278
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004279 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004280 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004282 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004283 auth_config = auth.extract_auth_config_from_options(options)
4284
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004285
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004286 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004287 if options.newbranch:
4288 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004289 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004290 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004291
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004292 cl = Changelist(auth_config=auth_config,
4293 codereview=options.forced_codereview)
4294 if not cl.GetIssue():
4295 parser.error('current branch must have an associated issue')
4296
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004297 upstream = cl.GetUpstreamBranch()
4298 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004299 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004300
4301 RunGit(['reset', '--hard', upstream])
4302 if options.pull:
4303 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004304
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004305 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4306 options.directory)
4307
4308 if len(args) != 1 or not args[0]:
4309 parser.error('Must specify issue number or url')
4310
4311 # We don't want uncommitted changes mixed up with the patch.
4312 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004313 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004314
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004315 if options.newbranch:
4316 if options.force:
4317 RunGit(['branch', '-D', options.newbranch],
4318 stderr=subprocess2.PIPE, error_ok=True)
4319 RunGit(['new-branch', options.newbranch])
4320
4321 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4322
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004323 if cl.IsGerrit():
4324 if options.reject:
4325 parser.error('--reject is not supported with Gerrit codereview.')
4326 if options.nocommit:
4327 parser.error('--nocommit is not supported with Gerrit codereview.')
4328 if options.directory:
4329 parser.error('--directory is not supported with Gerrit codereview.')
4330
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004331 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004332 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004333
4334
4335def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004336 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337 # Provide a wrapper for git svn rebase to help avoid accidental
4338 # git svn dcommit.
4339 # It's the only command that doesn't use parser at all since we just defer
4340 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004341
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004342 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004343
4344
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004345def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004346 """Fetches the tree status and returns either 'open', 'closed',
4347 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004348 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349 if url:
4350 status = urllib2.urlopen(url).read().lower()
4351 if status.find('closed') != -1 or status == '0':
4352 return 'closed'
4353 elif status.find('open') != -1 or status == '1':
4354 return 'open'
4355 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356 return 'unset'
4357
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004359def GetTreeStatusReason():
4360 """Fetches the tree status from a json url and returns the message
4361 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004362 url = settings.GetTreeStatusUrl()
4363 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004364 connection = urllib2.urlopen(json_url)
4365 status = json.loads(connection.read())
4366 connection.close()
4367 return status['message']
4368
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004369
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004370def GetBuilderMaster(bot_list):
4371 """For a given builder, fetch the master from AE if available."""
4372 map_url = 'https://builders-map.appspot.com/'
4373 try:
4374 master_map = json.load(urllib2.urlopen(map_url))
4375 except urllib2.URLError as e:
4376 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4377 (map_url, e))
4378 except ValueError as e:
4379 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4380 if not master_map:
4381 return None, 'Failed to build master map.'
4382
4383 result_master = ''
4384 for bot in bot_list:
4385 builder = bot.split(':', 1)[0]
4386 master_list = master_map.get(builder, [])
4387 if not master_list:
4388 return None, ('No matching master for builder %s.' % builder)
4389 elif len(master_list) > 1:
4390 return None, ('The builder name %s exists in multiple masters %s.' %
4391 (builder, master_list))
4392 else:
4393 cur_master = master_list[0]
4394 if not result_master:
4395 result_master = cur_master
4396 elif result_master != cur_master:
4397 return None, 'The builders do not belong to the same master.'
4398 return result_master, None
4399
4400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004402 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004403 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 status = GetTreeStatus()
4405 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004406 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407 return 2
4408
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print('The tree is %s' % status)
4410 print()
4411 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412 if status != 'open':
4413 return 1
4414 return 0
4415
4416
maruel@chromium.org15192402012-09-06 12:38:29 +00004417def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004418 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004419 group = optparse.OptionGroup(parser, "Try job options")
4420 group.add_option(
4421 "-b", "--bot", action="append",
4422 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4423 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004424 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004425 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004426 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004427 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004428 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004429 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004430 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004431 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004432 "-r", "--revision",
4433 help="Revision to use for the try job; default: the "
4434 "revision will be determined by the try server; see "
4435 "its waterfall for more info")
4436 group.add_option(
4437 "-c", "--clobber", action="store_true", default=False,
4438 help="Force a clobber before building; e.g. don't do an "
4439 "incremental build")
4440 group.add_option(
4441 "--project",
4442 help="Override which project to use. Projects are defined "
4443 "server-side to define what default bot set to use")
4444 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004445 "-p", "--property", dest="properties", action="append", default=[],
4446 help="Specify generic properties in the form -p key1=value1 -p "
4447 "key2=value2 etc (buildbucket only). The value will be treated as "
4448 "json if decodable, or as string otherwise.")
4449 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004450 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004451 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004452 "--use-rietveld", action="store_true", default=False,
4453 help="Use Rietveld to trigger try jobs.")
4454 group.add_option(
4455 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4456 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004457 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004458 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004459 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004460 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004461
machenbach@chromium.org45453142015-09-15 08:45:22 +00004462 if options.use_rietveld and options.properties:
4463 parser.error('Properties can only be specified with buildbucket')
4464
4465 # Make sure that all properties are prop=value pairs.
4466 bad_params = [x for x in options.properties if '=' not in x]
4467 if bad_params:
4468 parser.error('Got properties with missing "=": %s' % bad_params)
4469
maruel@chromium.org15192402012-09-06 12:38:29 +00004470 if args:
4471 parser.error('Unknown arguments: %s' % args)
4472
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004473 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004474 if not cl.GetIssue():
4475 parser.error('Need to upload first')
4476
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004477 if cl.IsGerrit():
4478 parser.error(
4479 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4480 'If your project has Commit Queue, dry run is a workaround:\n'
4481 ' git cl set-commit --dry-run')
4482 # Code below assumes Rietveld issue.
4483 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4484
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004485 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004486 if props.get('closed'):
4487 parser.error('Cannot send tryjobs for a closed CL')
4488
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004489 if props.get('private'):
4490 parser.error('Cannot use trybots with private issue')
4491
maruel@chromium.org15192402012-09-06 12:38:29 +00004492 if not options.name:
4493 options.name = cl.GetBranch()
4494
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004495 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004496 options.master, err_msg = GetBuilderMaster(options.bot)
4497 if err_msg:
4498 parser.error('Tryserver master cannot be found because: %s\n'
4499 'Please manually specify the tryserver master'
4500 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004501
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004502 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004503 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004504 if not options.bot:
4505 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004506
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004507 # Get try masters from PRESUBMIT.py files.
4508 masters = presubmit_support.DoGetTryMasters(
4509 change,
4510 change.LocalPaths(),
4511 settings.GetRoot(),
4512 None,
4513 None,
4514 options.verbose,
4515 sys.stdout)
4516 if masters:
4517 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004518
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004519 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4520 options.bot = presubmit_support.DoGetTrySlaves(
4521 change,
4522 change.LocalPaths(),
4523 settings.GetRoot(),
4524 None,
4525 None,
4526 options.verbose,
4527 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004528
4529 if not options.bot:
4530 # Get try masters from cq.cfg if any.
4531 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4532 # location.
4533 cq_cfg = os.path.join(change.RepositoryRoot(),
4534 'infra', 'config', 'cq.cfg')
4535 if os.path.exists(cq_cfg):
4536 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004537 cq_masters = commit_queue.get_master_builder_map(
4538 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004539 for master, builders in cq_masters.iteritems():
4540 for builder in builders:
4541 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004542 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004543 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004544 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004545 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004546 else:
4547 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004548
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004549 if not options.bot:
4550 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004551
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004552 builders_and_tests = {}
4553 # TODO(machenbach): The old style command-line options don't support
4554 # multiple try masters yet.
4555 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4556 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4557
4558 for bot in old_style:
4559 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004560 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004561 elif ',' in bot:
4562 parser.error('Specify one bot per --bot flag')
4563 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004564 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004565
4566 for bot, tests in new_style:
4567 builders_and_tests.setdefault(bot, []).extend(tests)
4568
4569 # Return a master map with one master to be backwards compatible. The
4570 # master name defaults to an empty string, which will cause the master
4571 # not to be set on rietveld (deprecated).
4572 return {options.master: builders_and_tests}
4573
4574 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004575
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004576 for builders in masters.itervalues():
4577 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004578 print('ERROR You are trying to send a job to a triggered bot. This type '
4579 'of bot requires an\ninitial job from a parent (usually a builder).'
4580 ' Instead send your job to the parent.\n'
4581 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004582 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004583
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004584 patchset = cl.GetMostRecentPatchset()
4585 if patchset and patchset != cl.GetPatchset():
4586 print(
4587 '\nWARNING Mismatch between local config and server. Did a previous '
4588 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4589 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004590 if options.luci:
4591 trigger_luci_job(cl, masters, options)
4592 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004593 try:
4594 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4595 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004596 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004597 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004598 except Exception as e:
4599 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004600 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4601 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004602 return 1
4603 else:
4604 try:
4605 cl.RpcServer().trigger_distributed_try_jobs(
4606 cl.GetIssue(), patchset, options.name, options.clobber,
4607 options.revision, masters)
4608 except urllib2.HTTPError as e:
4609 if e.code == 404:
4610 print('404 from rietveld; '
4611 'did you mean to use "git try" instead of "git cl try"?')
4612 return 1
4613 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004614
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004615 for (master, builders) in sorted(masters.iteritems()):
4616 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004617 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004618 length = max(len(builder) for builder in builders)
4619 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004620 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004621 return 0
4622
4623
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004624def CMDtry_results(parser, args):
4625 group = optparse.OptionGroup(parser, "Try job results options")
4626 group.add_option(
4627 "-p", "--patchset", type=int, help="patchset number if not current.")
4628 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004629 "--print-master", action='store_true', help="print master name as well.")
4630 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004631 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004632 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004633 group.add_option(
4634 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4635 help="Host of buildbucket. The default host is %default.")
4636 parser.add_option_group(group)
4637 auth.add_auth_options(parser)
4638 options, args = parser.parse_args(args)
4639 if args:
4640 parser.error('Unrecognized args: %s' % ' '.join(args))
4641
4642 auth_config = auth.extract_auth_config_from_options(options)
4643 cl = Changelist(auth_config=auth_config)
4644 if not cl.GetIssue():
4645 parser.error('Need to upload first')
4646
4647 if not options.patchset:
4648 options.patchset = cl.GetMostRecentPatchset()
4649 if options.patchset and options.patchset != cl.GetPatchset():
4650 print(
4651 '\nWARNING Mismatch between local config and server. Did a previous '
4652 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4653 'Continuing using\npatchset %s.\n' % options.patchset)
4654 try:
4655 jobs = fetch_try_jobs(auth_config, cl, options)
4656 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004657 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004658 return 1
4659 except Exception as e:
4660 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004661 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4662 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004663 return 1
4664 print_tryjobs(options, jobs)
4665 return 0
4666
4667
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004668@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004669def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004670 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004671 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004672 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004673 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004676 if args:
4677 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004678 branch = cl.GetBranch()
4679 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004680 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004681 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004682
4683 # Clear configured merge-base, if there is one.
4684 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004685 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004686 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687 return 0
4688
4689
thestig@chromium.org00858c82013-12-02 23:08:03 +00004690def CMDweb(parser, args):
4691 """Opens the current CL in the web browser."""
4692 _, args = parser.parse_args(args)
4693 if args:
4694 parser.error('Unrecognized args: %s' % ' '.join(args))
4695
4696 issue_url = Changelist().GetIssueURL()
4697 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004698 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004699 return 1
4700
4701 webbrowser.open(issue_url)
4702 return 0
4703
4704
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004705def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004706 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004707 parser.add_option('-d', '--dry-run', action='store_true',
4708 help='trigger in dry run mode')
4709 parser.add_option('-c', '--clear', action='store_true',
4710 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004711 auth.add_auth_options(parser)
4712 options, args = parser.parse_args(args)
4713 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004714 if args:
4715 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004716 if options.dry_run and options.clear:
4717 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4718
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004719 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004720 if options.clear:
4721 state = _CQState.CLEAR
4722 elif options.dry_run:
4723 state = _CQState.DRY_RUN
4724 else:
4725 state = _CQState.COMMIT
4726 if not cl.GetIssue():
4727 parser.error('Must upload the issue first')
4728 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004729 return 0
4730
4731
groby@chromium.org411034a2013-02-26 15:12:01 +00004732def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004733 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004734 auth.add_auth_options(parser)
4735 options, args = parser.parse_args(args)
4736 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004737 if args:
4738 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004739 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004740 # Ensure there actually is an issue to close.
4741 cl.GetDescription()
4742 cl.CloseIssue()
4743 return 0
4744
4745
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004746def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004747 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004748 auth.add_auth_options(parser)
4749 options, args = parser.parse_args(args)
4750 auth_config = auth.extract_auth_config_from_options(options)
4751 if args:
4752 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004753
4754 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004755 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004756 # Staged changes would be committed along with the patch from last
4757 # upload, hence counted toward the "last upload" side in the final
4758 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004759 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004760 return 1
4761
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004762 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004763 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004764 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004765 if not issue:
4766 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004767 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004768 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004769
4770 # Create a new branch based on the merge-base
4771 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004772 # Clear cached branch in cl object, to avoid overwriting original CL branch
4773 # properties.
4774 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004775 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004776 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004777 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004778 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004779 return rtn
4780
wychen@chromium.org06928532015-02-03 02:11:29 +00004781 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004782 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004783 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004784 finally:
4785 RunGit(['checkout', '-q', branch])
4786 RunGit(['branch', '-D', TMP_BRANCH])
4787
4788 return 0
4789
4790
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004791def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004792 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004793 parser.add_option(
4794 '--no-color',
4795 action='store_true',
4796 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004797 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004798 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004799 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004800
4801 author = RunGit(['config', 'user.email']).strip() or None
4802
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004803 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004804
4805 if args:
4806 if len(args) > 1:
4807 parser.error('Unknown args')
4808 base_branch = args[0]
4809 else:
4810 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004811 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004812
4813 change = cl.GetChange(base_branch, None)
4814 return owners_finder.OwnersFinder(
4815 [f.LocalPath() for f in
4816 cl.GetChange(base_branch, None).AffectedFiles()],
4817 change.RepositoryRoot(), author,
4818 fopen=file, os_path=os.path, glob=glob.glob,
4819 disable_color=options.no_color).run()
4820
4821
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004822def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004823 """Generates a diff command."""
4824 # Generate diff for the current branch's changes.
4825 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4826 upstream_commit, '--' ]
4827
4828 if args:
4829 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004830 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004831 diff_cmd.append(arg)
4832 else:
4833 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004834
4835 return diff_cmd
4836
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004837def MatchingFileType(file_name, extensions):
4838 """Returns true if the file name ends with one of the given extensions."""
4839 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004840
enne@chromium.org555cfe42014-01-29 18:21:39 +00004841@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004842def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004843 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004844 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004845 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004846 parser.add_option('--full', action='store_true',
4847 help='Reformat the full content of all touched files')
4848 parser.add_option('--dry-run', action='store_true',
4849 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004850 parser.add_option('--python', action='store_true',
4851 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004852 parser.add_option('--diff', action='store_true',
4853 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004854 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004855
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004856 # git diff generates paths against the root of the repository. Change
4857 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004858 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004859 if rel_base_path:
4860 os.chdir(rel_base_path)
4861
digit@chromium.org29e47272013-05-17 17:01:46 +00004862 # Grab the merge-base commit, i.e. the upstream commit of the current
4863 # branch when it was created or the last time it was rebased. This is
4864 # to cover the case where the user may have called "git fetch origin",
4865 # moving the origin branch to a newer commit, but hasn't rebased yet.
4866 upstream_commit = None
4867 cl = Changelist()
4868 upstream_branch = cl.GetUpstreamBranch()
4869 if upstream_branch:
4870 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4871 upstream_commit = upstream_commit.strip()
4872
4873 if not upstream_commit:
4874 DieWithError('Could not find base commit for this branch. '
4875 'Are you in detached state?')
4876
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004877 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4878 diff_output = RunGit(changed_files_cmd)
4879 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004880 # Filter out files deleted by this CL
4881 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004882
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004883 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4884 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4885 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004886 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004887
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004888 top_dir = os.path.normpath(
4889 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4890
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004891 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4892 # formatted. This is used to block during the presubmit.
4893 return_value = 0
4894
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004895 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004896 # Locate the clang-format binary in the checkout
4897 try:
4898 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004899 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004900 DieWithError(e)
4901
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004902 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004903 cmd = [clang_format_tool]
4904 if not opts.dry_run and not opts.diff:
4905 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004906 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004907 if opts.diff:
4908 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004909 else:
4910 env = os.environ.copy()
4911 env['PATH'] = str(os.path.dirname(clang_format_tool))
4912 try:
4913 script = clang_format.FindClangFormatScriptInChromiumTree(
4914 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004915 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004916 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004917
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004918 cmd = [sys.executable, script, '-p0']
4919 if not opts.dry_run and not opts.diff:
4920 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004921
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004922 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4923 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004924
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004925 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4926 if opts.diff:
4927 sys.stdout.write(stdout)
4928 if opts.dry_run and len(stdout) > 0:
4929 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004930
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004931 # Similar code to above, but using yapf on .py files rather than clang-format
4932 # on C/C++ files
4933 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004934 yapf_tool = gclient_utils.FindExecutable('yapf')
4935 if yapf_tool is None:
4936 DieWithError('yapf not found in PATH')
4937
4938 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004939 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004940 cmd = [yapf_tool]
4941 if not opts.dry_run and not opts.diff:
4942 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004943 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004944 if opts.diff:
4945 sys.stdout.write(stdout)
4946 else:
4947 # TODO(sbc): yapf --lines mode still has some issues.
4948 # https://github.com/google/yapf/issues/154
4949 DieWithError('--python currently only works with --full')
4950
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004951 # Dart's formatter does not have the nice property of only operating on
4952 # modified chunks, so hard code full.
4953 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004954 try:
4955 command = [dart_format.FindDartFmtToolInChromiumTree()]
4956 if not opts.dry_run and not opts.diff:
4957 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004958 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004959
ppi@chromium.org6593d932016-03-03 15:41:15 +00004960 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004961 if opts.dry_run and stdout:
4962 return_value = 2
4963 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004964 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4965 'found in this checkout. Files in other languages are still '
4966 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004967
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004968 # Format GN build files. Always run on full build files for canonical form.
4969 if gn_diff_files:
4970 cmd = ['gn', 'format']
4971 if not opts.dry_run and not opts.diff:
4972 cmd.append('--in-place')
4973 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004974 stdout = RunCommand(cmd + [gn_diff_file],
4975 shell=sys.platform == 'win32',
4976 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004977 if opts.diff:
4978 sys.stdout.write(stdout)
4979
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004980 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004981
4982
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004983@subcommand.usage('<codereview url or issue id>')
4984def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004985 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004986 _, args = parser.parse_args(args)
4987
4988 if len(args) != 1:
4989 parser.print_help()
4990 return 1
4991
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004992 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004993 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004994 parser.print_help()
4995 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004996 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004997
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004998 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004999 output = RunGit(['config', '--local', '--get-regexp',
5000 r'branch\..*\.%s' % issueprefix],
5001 error_ok=True)
5002 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005003 if issue == target_issue:
5004 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005005
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005006 branches = []
5007 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005008 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005009 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005010 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005011 return 1
5012 if len(branches) == 1:
5013 RunGit(['checkout', branches[0]])
5014 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005015 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005016 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005017 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005018 which = raw_input('Choose by index: ')
5019 try:
5020 RunGit(['checkout', branches[int(which)]])
5021 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005022 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005023 return 1
5024
5025 return 0
5026
5027
maruel@chromium.org29404b52014-09-08 22:58:00 +00005028def CMDlol(parser, args):
5029 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005030 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005031 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5032 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5033 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005034 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005035 return 0
5036
5037
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005038class OptionParser(optparse.OptionParser):
5039 """Creates the option parse and add --verbose support."""
5040 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005041 optparse.OptionParser.__init__(
5042 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005043 self.add_option(
5044 '-v', '--verbose', action='count', default=0,
5045 help='Use 2 times for more debugging info')
5046
5047 def parse_args(self, args=None, values=None):
5048 options, args = optparse.OptionParser.parse_args(self, args, values)
5049 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5050 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5051 return options, args
5052
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005054def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005055 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005056 print('\nYour python version %s is unsupported, please upgrade.\n' %
5057 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005058 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005059
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005060 # Reload settings.
5061 global settings
5062 settings = Settings()
5063
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005064 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005065 dispatcher = subcommand.CommandDispatcher(__name__)
5066 try:
5067 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005068 except auth.AuthenticationError as e:
5069 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005070 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005071 if e.code != 500:
5072 raise
5073 DieWithError(
5074 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5075 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005076 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005077
5078
5079if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005080 # These affect sys.stdout so do it outside of main() to simplify mocks in
5081 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005082 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005083 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005084 try:
5085 sys.exit(main(sys.argv[1:]))
5086 except KeyboardInterrupt:
5087 sys.stderr.write('interrupted\n')
5088 sys.exit(1)