blob: ab79f6235d82a3c0a0549fd4b8a3ceb75097194e [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'
martiniss6eda05f2016-06-30 10:18:35 -07001598 if not rietveld_server:
1599 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001600
1601 self._rietveld_server = rietveld_server
1602 self._auth_config = auth_config
1603 self._props = None
1604 self._rpc_server = None
1605
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001606 def GetCodereviewServer(self):
1607 if not self._rietveld_server:
1608 # If we're on a branch then get the server potentially associated
1609 # with that branch.
1610 if self.GetIssue():
1611 rietveld_server_setting = self.GetCodereviewServerSetting()
1612 if rietveld_server_setting:
1613 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1614 ['config', rietveld_server_setting], error_ok=True).strip())
1615 if not self._rietveld_server:
1616 self._rietveld_server = settings.GetDefaultServerUrl()
1617 return self._rietveld_server
1618
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001619 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 """Best effort check that user is authenticated with Rietveld server."""
1621 if self._auth_config.use_oauth2:
1622 authenticator = auth.get_authenticator_for_host(
1623 self.GetCodereviewServer(), self._auth_config)
1624 if not authenticator.has_cached_credentials():
1625 raise auth.LoginRequiredError(self.GetCodereviewServer())
1626
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001627 def FetchDescription(self):
1628 issue = self.GetIssue()
1629 assert issue
1630 try:
1631 return self.RpcServer().get_description(issue).strip()
1632 except urllib2.HTTPError as e:
1633 if e.code == 404:
1634 DieWithError(
1635 ('\nWhile fetching the description for issue %d, received a '
1636 '404 (not found)\n'
1637 'error. It is likely that you deleted this '
1638 'issue on the server. If this is the\n'
1639 'case, please run\n\n'
1640 ' git cl issue 0\n\n'
1641 'to clear the association with the deleted issue. Then run '
1642 'this command again.') % issue)
1643 else:
1644 DieWithError(
1645 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1646 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001647 print('Warning: Failed to retrieve CL description due to network '
1648 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 return ''
1650
1651 def GetMostRecentPatchset(self):
1652 return self.GetIssueProperties()['patchsets'][-1]
1653
1654 def GetPatchSetDiff(self, issue, patchset):
1655 return self.RpcServer().get(
1656 '/download/issue%s_%s.diff' % (issue, patchset))
1657
1658 def GetIssueProperties(self):
1659 if self._props is None:
1660 issue = self.GetIssue()
1661 if not issue:
1662 self._props = {}
1663 else:
1664 self._props = self.RpcServer().get_issue_properties(issue, True)
1665 return self._props
1666
1667 def GetApprovingReviewers(self):
1668 return get_approving_reviewers(self.GetIssueProperties())
1669
1670 def AddComment(self, message):
1671 return self.RpcServer().add_comment(self.GetIssue(), message)
1672
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001673 def GetStatus(self):
1674 """Apply a rough heuristic to give a simple summary of an issue's review
1675 or CQ status, assuming adherence to a common workflow.
1676
1677 Returns None if no issue for this branch, or one of the following keywords:
1678 * 'error' - error from review tool (including deleted issues)
1679 * 'unsent' - not sent for review
1680 * 'waiting' - waiting for review
1681 * 'reply' - waiting for owner to reply to review
1682 * 'lgtm' - LGTM from at least one approved reviewer
1683 * 'commit' - in the commit queue
1684 * 'closed' - closed
1685 """
1686 if not self.GetIssue():
1687 return None
1688
1689 try:
1690 props = self.GetIssueProperties()
1691 except urllib2.HTTPError:
1692 return 'error'
1693
1694 if props.get('closed'):
1695 # Issue is closed.
1696 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001697 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001698 # Issue is in the commit queue.
1699 return 'commit'
1700
1701 try:
1702 reviewers = self.GetApprovingReviewers()
1703 except urllib2.HTTPError:
1704 return 'error'
1705
1706 if reviewers:
1707 # Was LGTM'ed.
1708 return 'lgtm'
1709
1710 messages = props.get('messages') or []
1711
tandrii9d2c7a32016-06-22 03:42:45 -07001712 # Skip CQ messages that don't require owner's action.
1713 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1714 if 'Dry run:' in messages[-1]['text']:
1715 messages.pop()
1716 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1717 # This message always follows prior messages from CQ,
1718 # so skip this too.
1719 messages.pop()
1720 else:
1721 # This is probably a CQ messages warranting user attention.
1722 break
1723
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001724 if not messages:
1725 # No message was sent.
1726 return 'unsent'
1727 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001728 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001729 return 'reply'
1730 return 'waiting'
1731
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001733 return self.RpcServer().update_description(
1734 self.GetIssue(), self.description)
1735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001737 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001739 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001740 return self.SetFlags({flag: value})
1741
1742 def SetFlags(self, flags):
1743 """Sets flags on this CL/patchset in Rietveld.
1744
1745 The latest patchset in Rietveld must be the same as latest known locally.
1746 """
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001747 if not self.GetPatchset():
1748 DieWithError('The patchset needs to match. Send another patchset.')
1749 try:
tandrii4b233bd2016-07-06 03:50:29 -07001750 return self.RpcServer().set_flags(
1751 self.GetIssue(), self.GetPatchset(), flags)
vapierfd77ac72016-06-16 08:33:57 -07001752 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001753 if e.code == 404:
1754 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1755 if e.code == 403:
1756 DieWithError(
1757 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1758 'match?') % (self.GetIssue(), self.GetPatchset()))
1759 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001761 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762 """Returns an upload.RpcServer() to access this review's rietveld instance.
1763 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001764 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001765 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001767 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001768 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001770 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001771 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001772 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001774 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775 """Return the git setting that stores this change's most recent patchset."""
1776 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1777
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001778 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001780 branch = self.GetBranch()
1781 if branch:
1782 return 'branch.%s.rietveldserver' % branch
1783 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001785 def _PostUnsetIssueProperties(self):
1786 """Which branch-specific properties to erase when unsetting issue."""
1787 return ['rietveldserver']
1788
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 def GetRieveldObjForPresubmit(self):
1790 return self.RpcServer()
1791
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001792 def SetCQState(self, new_state):
1793 props = self.GetIssueProperties()
1794 if props.get('private'):
1795 DieWithError('Cannot set-commit on private issue')
1796
1797 if new_state == _CQState.COMMIT:
1798 self.SetFlag('commit', '1')
1799 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001800 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001801 else:
tandrii4b233bd2016-07-06 03:50:29 -07001802 assert new_state == _CQState.DRY_RUN
1803 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001804
1805
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001806 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1807 directory):
1808 # TODO(maruel): Use apply_issue.py
1809
1810 # PatchIssue should never be called with a dirty tree. It is up to the
1811 # caller to check this, but just in case we assert here since the
1812 # consequences of the caller not checking this could be dire.
1813 assert(not git_common.is_dirty_git_tree('apply'))
1814 assert(parsed_issue_arg.valid)
1815 self._changelist.issue = parsed_issue_arg.issue
1816 if parsed_issue_arg.hostname:
1817 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1818
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001819 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1820 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001821 assert parsed_issue_arg.patchset
1822 patchset = parsed_issue_arg.patchset
1823 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1824 else:
1825 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1826 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1827
1828 # Switch up to the top-level directory, if necessary, in preparation for
1829 # applying the patch.
1830 top = settings.GetRelativeRoot()
1831 if top:
1832 os.chdir(top)
1833
1834 # Git patches have a/ at the beginning of source paths. We strip that out
1835 # with a sed script rather than the -p flag to patch so we can feed either
1836 # Git or svn-style patches into the same apply command.
1837 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1838 try:
1839 patch_data = subprocess2.check_output(
1840 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1841 except subprocess2.CalledProcessError:
1842 DieWithError('Git patch mungling failed.')
1843 logging.info(patch_data)
1844
1845 # We use "git apply" to apply the patch instead of "patch" so that we can
1846 # pick up file adds.
1847 # The --index flag means: also insert into the index (so we catch adds).
1848 cmd = ['git', 'apply', '--index', '-p0']
1849 if directory:
1850 cmd.extend(('--directory', directory))
1851 if reject:
1852 cmd.append('--reject')
1853 elif IsGitVersionAtLeast('1.7.12'):
1854 cmd.append('--3way')
1855 try:
1856 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1857 stdin=patch_data, stdout=subprocess2.VOID)
1858 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001859 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001860 return 1
1861
1862 # If we had an issue, commit the current state and register the issue.
1863 if not nocommit:
1864 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1865 'patch from issue %(i)s at patchset '
1866 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1867 % {'i': self.GetIssue(), 'p': patchset})])
1868 self.SetIssue(self.GetIssue())
1869 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001870 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001871 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001872 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001873 return 0
1874
1875 @staticmethod
1876 def ParseIssueURL(parsed_url):
1877 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1878 return None
1879 # Typical url: https://domain/<issue_number>[/[other]]
1880 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1881 if match:
1882 return _RietveldParsedIssueNumberArgument(
1883 issue=int(match.group(1)),
1884 hostname=parsed_url.netloc)
1885 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1886 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1887 if match:
1888 return _RietveldParsedIssueNumberArgument(
1889 issue=int(match.group(1)),
1890 patchset=int(match.group(2)),
1891 hostname=parsed_url.netloc,
1892 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1893 return None
1894
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001895 def CMDUploadChange(self, options, args, change):
1896 """Upload the patch to Rietveld."""
1897 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1898 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001899 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1900 if options.emulate_svn_auto_props:
1901 upload_args.append('--emulate_svn_auto_props')
1902
1903 change_desc = None
1904
1905 if options.email is not None:
1906 upload_args.extend(['--email', options.email])
1907
1908 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001909 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001910 upload_args.extend(['--title', options.title])
1911 if options.message:
1912 upload_args.extend(['--message', options.message])
1913 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001914 print('This branch is associated with issue %s. '
1915 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001916 else:
nodirca166002016-06-27 10:59:51 -07001917 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001918 upload_args.extend(['--title', options.title])
1919 message = (options.title or options.message or
1920 CreateDescriptionFromLog(args))
1921 change_desc = ChangeDescription(message)
1922 if options.reviewers or options.tbr_owners:
1923 change_desc.update_reviewers(options.reviewers,
1924 options.tbr_owners,
1925 change)
1926 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001927 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001928
1929 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001930 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001931 return 1
1932
1933 upload_args.extend(['--message', change_desc.description])
1934 if change_desc.get_reviewers():
1935 upload_args.append('--reviewers=%s' % ','.join(
1936 change_desc.get_reviewers()))
1937 if options.send_mail:
1938 if not change_desc.get_reviewers():
1939 DieWithError("Must specify reviewers to send email.")
1940 upload_args.append('--send_mail')
1941
1942 # We check this before applying rietveld.private assuming that in
1943 # rietveld.cc only addresses which we can send private CLs to are listed
1944 # if rietveld.private is set, and so we should ignore rietveld.cc only
1945 # when --private is specified explicitly on the command line.
1946 if options.private:
1947 logging.warn('rietveld.cc is ignored since private flag is specified. '
1948 'You need to review and add them manually if necessary.')
1949 cc = self.GetCCListWithoutDefault()
1950 else:
1951 cc = self.GetCCList()
1952 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1953 if cc:
1954 upload_args.extend(['--cc', cc])
1955
1956 if options.private or settings.GetDefaultPrivateFlag() == "True":
1957 upload_args.append('--private')
1958
1959 upload_args.extend(['--git_similarity', str(options.similarity)])
1960 if not options.find_copies:
1961 upload_args.extend(['--git_no_find_copies'])
1962
1963 # Include the upstream repo's URL in the change -- this is useful for
1964 # projects that have their source spread across multiple repos.
1965 remote_url = self.GetGitBaseUrlFromConfig()
1966 if not remote_url:
1967 if settings.GetIsGitSvn():
1968 remote_url = self.GetGitSvnRemoteUrl()
1969 else:
1970 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1971 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1972 self.GetUpstreamBranch().split('/')[-1])
1973 if remote_url:
1974 upload_args.extend(['--base_url', remote_url])
1975 remote, remote_branch = self.GetRemoteBranch()
1976 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1977 settings.GetPendingRefPrefix())
1978 if target_ref:
1979 upload_args.extend(['--target_ref', target_ref])
1980
1981 # Look for dependent patchsets. See crbug.com/480453 for more details.
1982 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1983 upstream_branch = ShortBranchName(upstream_branch)
1984 if remote is '.':
1985 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001986 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001987 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001988 print()
1989 print('Skipping dependency patchset upload because git config '
1990 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1991 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001992 else:
1993 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001994 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 auth_config=auth_config)
1996 branch_cl_issue_url = branch_cl.GetIssueURL()
1997 branch_cl_issue = branch_cl.GetIssue()
1998 branch_cl_patchset = branch_cl.GetPatchset()
1999 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2000 upload_args.extend(
2001 ['--depends_on_patchset', '%s:%s' % (
2002 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002003 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002004 '\n'
2005 'The current branch (%s) is tracking a local branch (%s) with '
2006 'an associated CL.\n'
2007 'Adding %s/#ps%s as a dependency patchset.\n'
2008 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2009 branch_cl_patchset))
2010
2011 project = settings.GetProject()
2012 if project:
2013 upload_args.extend(['--project', project])
2014
2015 if options.cq_dry_run:
2016 upload_args.extend(['--cq_dry_run'])
2017
2018 try:
2019 upload_args = ['upload'] + upload_args + args
2020 logging.info('upload.RealMain(%s)', upload_args)
2021 issue, patchset = upload.RealMain(upload_args)
2022 issue = int(issue)
2023 patchset = int(patchset)
2024 except KeyboardInterrupt:
2025 sys.exit(1)
2026 except:
2027 # If we got an exception after the user typed a description for their
2028 # change, back up the description before re-raising.
2029 if change_desc:
2030 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2031 print('\nGot exception while uploading -- saving description to %s\n' %
2032 backup_path)
2033 backup_file = open(backup_path, 'w')
2034 backup_file.write(change_desc.description)
2035 backup_file.close()
2036 raise
2037
2038 if not self.GetIssue():
2039 self.SetIssue(issue)
2040 self.SetPatchset(patchset)
2041
2042 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002043 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002044 return 0
2045
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002046
2047class _GerritChangelistImpl(_ChangelistCodereviewBase):
2048 def __init__(self, changelist, auth_config=None):
2049 # auth_config is Rietveld thing, kept here to preserve interface only.
2050 super(_GerritChangelistImpl, self).__init__(changelist)
2051 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002052 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002053 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002054 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002055
2056 def _GetGerritHost(self):
2057 # Lazy load of configs.
2058 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002059 if self._gerrit_host and '.' not in self._gerrit_host:
2060 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2061 # This happens for internal stuff http://crbug.com/614312.
2062 parsed = urlparse.urlparse(self.GetRemoteUrl())
2063 if parsed.scheme == 'sso':
2064 print('WARNING: using non https URLs for remote is likely broken\n'
2065 ' Your current remote is: %s' % self.GetRemoteUrl())
2066 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2067 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002068 return self._gerrit_host
2069
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002070 def _GetGitHost(self):
2071 """Returns git host to be used when uploading change to Gerrit."""
2072 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2073
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002074 def GetCodereviewServer(self):
2075 if not self._gerrit_server:
2076 # If we're on a branch then get the server potentially associated
2077 # with that branch.
2078 if self.GetIssue():
2079 gerrit_server_setting = self.GetCodereviewServerSetting()
2080 if gerrit_server_setting:
2081 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2082 error_ok=True).strip()
2083 if self._gerrit_server:
2084 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2085 if not self._gerrit_server:
2086 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2087 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002088 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089 parts[0] = parts[0] + '-review'
2090 self._gerrit_host = '.'.join(parts)
2091 self._gerrit_server = 'https://%s' % self._gerrit_host
2092 return self._gerrit_server
2093
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002094 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002095 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002096 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002097
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002098 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002099 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002100 if settings.GetGerritSkipEnsureAuthenticated():
2101 # For projects with unusual authentication schemes.
2102 # See http://crbug.com/603378.
2103 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002104 # Lazy-loader to identify Gerrit and Git hosts.
2105 if gerrit_util.GceAuthenticator.is_gce():
2106 return
2107 self.GetCodereviewServer()
2108 git_host = self._GetGitHost()
2109 assert self._gerrit_server and self._gerrit_host
2110 cookie_auth = gerrit_util.CookiesAuthenticator()
2111
2112 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2113 git_auth = cookie_auth.get_auth_header(git_host)
2114 if gerrit_auth and git_auth:
2115 if gerrit_auth == git_auth:
2116 return
2117 print((
2118 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2119 ' Check your %s or %s file for credentials of hosts:\n'
2120 ' %s\n'
2121 ' %s\n'
2122 ' %s') %
2123 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2124 git_host, self._gerrit_host,
2125 cookie_auth.get_new_password_message(git_host)))
2126 if not force:
2127 ask_for_data('If you know what you are doing, press Enter to continue, '
2128 'Ctrl+C to abort.')
2129 return
2130 else:
2131 missing = (
2132 [] if gerrit_auth else [self._gerrit_host] +
2133 [] if git_auth else [git_host])
2134 DieWithError('Credentials for the following hosts are required:\n'
2135 ' %s\n'
2136 'These are read from %s (or legacy %s)\n'
2137 '%s' % (
2138 '\n '.join(missing),
2139 cookie_auth.get_gitcookies_path(),
2140 cookie_auth.get_netrc_path(),
2141 cookie_auth.get_new_password_message(git_host)))
2142
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002143
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002144 def PatchsetSetting(self):
2145 """Return the git setting that stores this change's most recent patchset."""
2146 return 'branch.%s.gerritpatchset' % self.GetBranch()
2147
2148 def GetCodereviewServerSetting(self):
2149 """Returns the git setting that stores this change's Gerrit server."""
2150 branch = self.GetBranch()
2151 if branch:
2152 return 'branch.%s.gerritserver' % branch
2153 return None
2154
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002155 def _PostUnsetIssueProperties(self):
2156 """Which branch-specific properties to erase when unsetting issue."""
2157 return [
2158 'gerritserver',
2159 'gerritsquashhash',
2160 ]
2161
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002162 def GetRieveldObjForPresubmit(self):
2163 class ThisIsNotRietveldIssue(object):
2164 def __nonzero__(self):
2165 # This is a hack to make presubmit_support think that rietveld is not
2166 # defined, yet still ensure that calls directly result in a decent
2167 # exception message below.
2168 return False
2169
2170 def __getattr__(self, attr):
2171 print(
2172 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2173 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2174 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2175 'or use Rietveld for codereview.\n'
2176 'See also http://crbug.com/579160.' % attr)
2177 raise NotImplementedError()
2178 return ThisIsNotRietveldIssue()
2179
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002180 def GetGerritObjForPresubmit(self):
2181 return presubmit_support.GerritAccessor(self._GetGerritHost())
2182
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002183 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002184 """Apply a rough heuristic to give a simple summary of an issue's review
2185 or CQ status, assuming adherence to a common workflow.
2186
2187 Returns None if no issue for this branch, or one of the following keywords:
2188 * 'error' - error from review tool (including deleted issues)
2189 * 'unsent' - no reviewers added
2190 * 'waiting' - waiting for review
2191 * 'reply' - waiting for owner to reply to review
2192 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2193 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2194 * 'commit' - in the commit queue
2195 * 'closed' - abandoned
2196 """
2197 if not self.GetIssue():
2198 return None
2199
2200 try:
2201 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2202 except httplib.HTTPException:
2203 return 'error'
2204
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002205 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002206 return 'closed'
2207
2208 cq_label = data['labels'].get('Commit-Queue', {})
2209 if cq_label:
2210 # Vote value is a stringified integer, which we expect from 0 to 2.
2211 vote_value = cq_label.get('value', '0')
2212 vote_text = cq_label.get('values', {}).get(vote_value, '')
2213 if vote_text.lower() == 'commit':
2214 return 'commit'
2215
2216 lgtm_label = data['labels'].get('Code-Review', {})
2217 if lgtm_label:
2218 if 'rejected' in lgtm_label:
2219 return 'not lgtm'
2220 if 'approved' in lgtm_label:
2221 return 'lgtm'
2222
2223 if not data.get('reviewers', {}).get('REVIEWER', []):
2224 return 'unsent'
2225
2226 messages = data.get('messages', [])
2227 if messages:
2228 owner = data['owner'].get('_account_id')
2229 last_message_author = messages[-1].get('author', {}).get('_account_id')
2230 if owner != last_message_author:
2231 # Some reply from non-owner.
2232 return 'reply'
2233
2234 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002235
2236 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002237 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238 return data['revisions'][data['current_revision']]['_number']
2239
2240 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002241 data = self._GetChangeDetail(['CURRENT_REVISION'])
2242 current_rev = data['current_revision']
2243 url = data['revisions'][current_rev]['fetch']['http']['url']
2244 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002245
2246 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002247 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2248 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002249
2250 def CloseIssue(self):
2251 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2252
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002253 def GetApprovingReviewers(self):
2254 """Returns a list of reviewers approving the change.
2255
2256 Note: not necessarily committers.
2257 """
2258 raise NotImplementedError()
2259
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002260 def SubmitIssue(self, wait_for_merge=True):
2261 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2262 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002263
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002264 def _GetChangeDetail(self, options=None, issue=None):
2265 options = options or []
2266 issue = issue or self.GetIssue()
2267 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002268 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2269 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002270
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002271 def CMDLand(self, force, bypass_hooks, verbose):
2272 if git_common.is_dirty_git_tree('land'):
2273 return 1
tandriid60367b2016-06-22 05:25:12 -07002274 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2275 if u'Commit-Queue' in detail.get('labels', {}):
2276 if not force:
2277 ask_for_data('\nIt seems this repository has a Commit Queue, '
2278 'which can test and land changes for you. '
2279 'Are you sure you wish to bypass it?\n'
2280 'Press Enter to continue, Ctrl+C to abort.')
2281
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002282 differs = True
2283 last_upload = RunGit(['config',
2284 'branch.%s.gerritsquashhash' % self.GetBranch()],
2285 error_ok=True).strip()
2286 # Note: git diff outputs nothing if there is no diff.
2287 if not last_upload or RunGit(['diff', last_upload]).strip():
2288 print('WARNING: some changes from local branch haven\'t been uploaded')
2289 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002290 if detail['current_revision'] == last_upload:
2291 differs = False
2292 else:
2293 print('WARNING: local branch contents differ from latest uploaded '
2294 'patchset')
2295 if differs:
2296 if not force:
2297 ask_for_data(
2298 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2299 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2300 elif not bypass_hooks:
2301 hook_results = self.RunHook(
2302 committing=True,
2303 may_prompt=not force,
2304 verbose=verbose,
2305 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2306 if not hook_results.should_continue():
2307 return 1
2308
2309 self.SubmitIssue(wait_for_merge=True)
2310 print('Issue %s has been submitted.' % self.GetIssueURL())
2311 return 0
2312
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002313 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2314 directory):
2315 assert not reject
2316 assert not nocommit
2317 assert not directory
2318 assert parsed_issue_arg.valid
2319
2320 self._changelist.issue = parsed_issue_arg.issue
2321
2322 if parsed_issue_arg.hostname:
2323 self._gerrit_host = parsed_issue_arg.hostname
2324 self._gerrit_server = 'https://%s' % self._gerrit_host
2325
2326 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2327
2328 if not parsed_issue_arg.patchset:
2329 # Use current revision by default.
2330 revision_info = detail['revisions'][detail['current_revision']]
2331 patchset = int(revision_info['_number'])
2332 else:
2333 patchset = parsed_issue_arg.patchset
2334 for revision_info in detail['revisions'].itervalues():
2335 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2336 break
2337 else:
2338 DieWithError('Couldn\'t find patchset %i in issue %i' %
2339 (parsed_issue_arg.patchset, self.GetIssue()))
2340
2341 fetch_info = revision_info['fetch']['http']
2342 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2343 RunGit(['cherry-pick', 'FETCH_HEAD'])
2344 self.SetIssue(self.GetIssue())
2345 self.SetPatchset(patchset)
2346 print('Committed patch for issue %i pathset %i locally' %
2347 (self.GetIssue(), self.GetPatchset()))
2348 return 0
2349
2350 @staticmethod
2351 def ParseIssueURL(parsed_url):
2352 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2353 return None
2354 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2355 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2356 # Short urls like https://domain/<issue_number> can be used, but don't allow
2357 # specifying the patchset (you'd 404), but we allow that here.
2358 if parsed_url.path == '/':
2359 part = parsed_url.fragment
2360 else:
2361 part = parsed_url.path
2362 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2363 if match:
2364 return _ParsedIssueNumberArgument(
2365 issue=int(match.group(2)),
2366 patchset=int(match.group(4)) if match.group(4) else None,
2367 hostname=parsed_url.netloc)
2368 return None
2369
tandrii16e0b4e2016-06-07 10:34:28 -07002370 def _GerritCommitMsgHookCheck(self, offer_removal):
2371 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2372 if not os.path.exists(hook):
2373 return
2374 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2375 # custom developer made one.
2376 data = gclient_utils.FileRead(hook)
2377 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2378 return
2379 print('Warning: you have Gerrit commit-msg hook installed.\n'
2380 'It is not neccessary for uploading with git cl in squash mode, '
2381 'and may interfere with it in subtle ways.\n'
2382 'We recommend you remove the commit-msg hook.')
2383 if offer_removal:
2384 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2385 if reply.lower().startswith('y'):
2386 gclient_utils.rm_file_or_tree(hook)
2387 print('Gerrit commit-msg hook removed.')
2388 else:
2389 print('OK, will keep Gerrit commit-msg hook in place.')
2390
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002391 def CMDUploadChange(self, options, args, change):
2392 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002393 if options.squash and options.no_squash:
2394 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002395
2396 if not options.squash and not options.no_squash:
2397 # Load default for user, repo, squash=true, in this order.
2398 options.squash = settings.GetSquashGerritUploads()
2399 elif options.no_squash:
2400 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002401
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002402 # We assume the remote called "origin" is the one we want.
2403 # It is probably not worthwhile to support different workflows.
2404 gerrit_remote = 'origin'
2405
2406 remote, remote_branch = self.GetRemoteBranch()
2407 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2408 pending_prefix='')
2409
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002410 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002411 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002412 if self.GetIssue():
2413 # Try to get the message from a previous upload.
2414 message = self.GetDescription()
2415 if not message:
2416 DieWithError(
2417 'failed to fetch description from current Gerrit issue %d\n'
2418 '%s' % (self.GetIssue(), self.GetIssueURL()))
2419 change_id = self._GetChangeDetail()['change_id']
2420 while True:
2421 footer_change_ids = git_footers.get_footer_change_id(message)
2422 if footer_change_ids == [change_id]:
2423 break
2424 if not footer_change_ids:
2425 message = git_footers.add_footer_change_id(message, change_id)
2426 print('WARNING: appended missing Change-Id to issue description')
2427 continue
2428 # There is already a valid footer but with different or several ids.
2429 # Doing this automatically is non-trivial as we don't want to lose
2430 # existing other footers, yet we want to append just 1 desired
2431 # Change-Id. Thus, just create a new footer, but let user verify the
2432 # new description.
2433 message = '%s\n\nChange-Id: %s' % (message, change_id)
2434 print(
2435 'WARNING: issue %s has Change-Id footer(s):\n'
2436 ' %s\n'
2437 'but issue has Change-Id %s, according to Gerrit.\n'
2438 'Please, check the proposed correction to the description, '
2439 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2440 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2441 change_id))
2442 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2443 if not options.force:
2444 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002445 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002446 message = change_desc.description
2447 if not message:
2448 DieWithError("Description is empty. Aborting...")
2449 # Continue the while loop.
2450 # Sanity check of this code - we should end up with proper message
2451 # footer.
2452 assert [change_id] == git_footers.get_footer_change_id(message)
2453 change_desc = ChangeDescription(message)
2454 else:
2455 change_desc = ChangeDescription(
2456 options.message or CreateDescriptionFromLog(args))
2457 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002458 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002459 if not change_desc.description:
2460 DieWithError("Description is empty. Aborting...")
2461 message = change_desc.description
2462 change_ids = git_footers.get_footer_change_id(message)
2463 if len(change_ids) > 1:
2464 DieWithError('too many Change-Id footers, at most 1 allowed.')
2465 if not change_ids:
2466 # Generate the Change-Id automatically.
2467 message = git_footers.add_footer_change_id(
2468 message, GenerateGerritChangeId(message))
2469 change_desc.set_description(message)
2470 change_ids = git_footers.get_footer_change_id(message)
2471 assert len(change_ids) == 1
2472 change_id = change_ids[0]
2473
2474 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2475 if remote is '.':
2476 # If our upstream branch is local, we base our squashed commit on its
2477 # squashed version.
2478 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2479 # Check the squashed hash of the parent.
2480 parent = RunGit(['config',
2481 'branch.%s.gerritsquashhash' % upstream_branch_name],
2482 error_ok=True).strip()
2483 # Verify that the upstream branch has been uploaded too, otherwise
2484 # Gerrit will create additional CLs when uploading.
2485 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2486 RunGitSilent(['rev-parse', parent + ':'])):
2487 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2488 DieWithError(
2489 'Upload upstream branch %s first.\n'
2490 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2491 'version of depot_tools. If so, then re-upload it with:\n'
2492 ' git cl upload --squash\n' % upstream_branch_name)
2493 else:
2494 parent = self.GetCommonAncestorWithUpstream()
2495
2496 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2497 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2498 '-m', message]).strip()
2499 else:
2500 change_desc = ChangeDescription(
2501 options.message or CreateDescriptionFromLog(args))
2502 if not change_desc.description:
2503 DieWithError("Description is empty. Aborting...")
2504
2505 if not git_footers.get_footer_change_id(change_desc.description):
2506 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002507 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2508 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002509 ref_to_push = 'HEAD'
2510 parent = '%s/%s' % (gerrit_remote, branch)
2511 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2512
2513 assert change_desc
2514 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2515 ref_to_push)]).splitlines()
2516 if len(commits) > 1:
2517 print('WARNING: This will upload %d commits. Run the following command '
2518 'to see which commits will be uploaded: ' % len(commits))
2519 print('git log %s..%s' % (parent, ref_to_push))
2520 print('You can also use `git squash-branch` to squash these into a '
2521 'single commit.')
2522 ask_for_data('About to upload; enter to confirm.')
2523
2524 if options.reviewers or options.tbr_owners:
2525 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2526 change)
2527
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002528 # Extra options that can be specified at push time. Doc:
2529 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2530 refspec_opts = []
2531 if options.title:
2532 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2533 # reverse on its side.
2534 if '_' in options.title:
2535 print('WARNING: underscores in title will be converted to spaces.')
2536 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2537
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002538 if options.send_mail:
2539 if not change_desc.get_reviewers():
2540 DieWithError('Must specify reviewers to send email.')
2541 refspec_opts.append('notify=ALL')
2542 else:
2543 refspec_opts.append('notify=NONE')
2544
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002545 cc = self.GetCCList().split(',')
2546 if options.cc:
2547 cc.extend(options.cc)
2548 cc = filter(None, cc)
2549 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002550 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002551
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002552 if change_desc.get_reviewers():
2553 refspec_opts.extend('r=' + email.strip()
2554 for email in change_desc.get_reviewers())
2555
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002556 refspec_suffix = ''
2557 if refspec_opts:
2558 refspec_suffix = '%' + ','.join(refspec_opts)
2559 assert ' ' not in refspec_suffix, (
2560 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002561 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002562
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002563 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002564 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002565 print_stdout=True,
2566 # Flush after every line: useful for seeing progress when running as
2567 # recipe.
2568 filter_fn=lambda _: sys.stdout.flush())
2569
2570 if options.squash:
2571 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2572 change_numbers = [m.group(1)
2573 for m in map(regex.match, push_stdout.splitlines())
2574 if m]
2575 if len(change_numbers) != 1:
2576 DieWithError(
2577 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2578 'Change-Id: %s') % (len(change_numbers), change_id))
2579 self.SetIssue(change_numbers[0])
2580 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2581 ref_to_push])
2582 return 0
2583
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002584 def _AddChangeIdToCommitMessage(self, options, args):
2585 """Re-commits using the current message, assumes the commit hook is in
2586 place.
2587 """
2588 log_desc = options.message or CreateDescriptionFromLog(args)
2589 git_command = ['commit', '--amend', '-m', log_desc]
2590 RunGit(git_command)
2591 new_log_desc = CreateDescriptionFromLog(args)
2592 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002593 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002594 return new_log_desc
2595 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002596 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002597
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002598 def SetCQState(self, new_state):
2599 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2600 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2601 # self-discovery of label config for this CL using REST API.
2602 vote_map = {
2603 _CQState.NONE: 0,
2604 _CQState.DRY_RUN: 1,
2605 _CQState.COMMIT : 2,
2606 }
2607 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2608 labels={'Commit-Queue': vote_map[new_state]})
2609
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002610
2611_CODEREVIEW_IMPLEMENTATIONS = {
2612 'rietveld': _RietveldChangelistImpl,
2613 'gerrit': _GerritChangelistImpl,
2614}
2615
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002616
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002617def _add_codereview_select_options(parser):
2618 """Appends --gerrit and --rietveld options to force specific codereview."""
2619 parser.codereview_group = optparse.OptionGroup(
2620 parser, 'EXPERIMENTAL! Codereview override options')
2621 parser.add_option_group(parser.codereview_group)
2622 parser.codereview_group.add_option(
2623 '--gerrit', action='store_true',
2624 help='Force the use of Gerrit for codereview')
2625 parser.codereview_group.add_option(
2626 '--rietveld', action='store_true',
2627 help='Force the use of Rietveld for codereview')
2628
2629
2630def _process_codereview_select_options(parser, options):
2631 if options.gerrit and options.rietveld:
2632 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2633 options.forced_codereview = None
2634 if options.gerrit:
2635 options.forced_codereview = 'gerrit'
2636 elif options.rietveld:
2637 options.forced_codereview = 'rietveld'
2638
2639
tandriif9aefb72016-07-01 09:06:51 -07002640def _get_bug_line_values(default_project, bugs):
2641 """Given default_project and comma separated list of bugs, yields bug line
2642 values.
2643
2644 Each bug can be either:
2645 * a number, which is combined with default_project
2646 * string, which is left as is.
2647
2648 This function may produce more than one line, because bugdroid expects one
2649 project per line.
2650
2651 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2652 ['v8:123', 'chromium:789']
2653 """
2654 default_bugs = []
2655 others = []
2656 for bug in bugs.split(','):
2657 bug = bug.strip()
2658 if bug:
2659 try:
2660 default_bugs.append(int(bug))
2661 except ValueError:
2662 others.append(bug)
2663
2664 if default_bugs:
2665 default_bugs = ','.join(map(str, default_bugs))
2666 if default_project:
2667 yield '%s:%s' % (default_project, default_bugs)
2668 else:
2669 yield default_bugs
2670 for other in sorted(others):
2671 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2672 yield other
2673
2674
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002675class ChangeDescription(object):
2676 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002677 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002678 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002679
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002680 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002681 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002682
agable@chromium.org42c20792013-09-12 17:34:49 +00002683 @property # www.logilab.org/ticket/89786
2684 def description(self): # pylint: disable=E0202
2685 return '\n'.join(self._description_lines)
2686
2687 def set_description(self, desc):
2688 if isinstance(desc, basestring):
2689 lines = desc.splitlines()
2690 else:
2691 lines = [line.rstrip() for line in desc]
2692 while lines and not lines[0]:
2693 lines.pop(0)
2694 while lines and not lines[-1]:
2695 lines.pop(-1)
2696 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697
piman@chromium.org336f9122014-09-04 02:16:55 +00002698 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002699 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002700 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002701 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002702 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002703 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002704
agable@chromium.org42c20792013-09-12 17:34:49 +00002705 # Get the set of R= and TBR= lines and remove them from the desciption.
2706 regexp = re.compile(self.R_LINE)
2707 matches = [regexp.match(line) for line in self._description_lines]
2708 new_desc = [l for i, l in enumerate(self._description_lines)
2709 if not matches[i]]
2710 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002711
agable@chromium.org42c20792013-09-12 17:34:49 +00002712 # Construct new unified R= and TBR= lines.
2713 r_names = []
2714 tbr_names = []
2715 for match in matches:
2716 if not match:
2717 continue
2718 people = cleanup_list([match.group(2).strip()])
2719 if match.group(1) == 'TBR':
2720 tbr_names.extend(people)
2721 else:
2722 r_names.extend(people)
2723 for name in r_names:
2724 if name not in reviewers:
2725 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002726 if add_owners_tbr:
2727 owners_db = owners.Database(change.RepositoryRoot(),
2728 fopen=file, os_path=os.path, glob=glob.glob)
2729 all_reviewers = set(tbr_names + reviewers)
2730 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2731 all_reviewers)
2732 tbr_names.extend(owners_db.reviewers_for(missing_files,
2733 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002734 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2735 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2736
2737 # Put the new lines in the description where the old first R= line was.
2738 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2739 if 0 <= line_loc < len(self._description_lines):
2740 if new_tbr_line:
2741 self._description_lines.insert(line_loc, new_tbr_line)
2742 if new_r_line:
2743 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002744 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002745 if new_r_line:
2746 self.append_footer(new_r_line)
2747 if new_tbr_line:
2748 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002749
tandriif9aefb72016-07-01 09:06:51 -07002750 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002751 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002752 self.set_description([
2753 '# Enter a description of the change.',
2754 '# This will be displayed on the codereview site.',
2755 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002756 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002757 '--------------------',
2758 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002759
agable@chromium.org42c20792013-09-12 17:34:49 +00002760 regexp = re.compile(self.BUG_LINE)
2761 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002762 prefix = settings.GetBugPrefix()
2763 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2764 for value in values:
2765 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2766 self.append_footer('BUG=%s' % value)
2767
agable@chromium.org42c20792013-09-12 17:34:49 +00002768 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002769 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002770 if not content:
2771 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002772 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002773
2774 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002775 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2776 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002777 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002778 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002779
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002780 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002781 """Adds a footer line to the description.
2782
2783 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2784 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2785 that Gerrit footers are always at the end.
2786 """
2787 parsed_footer_line = git_footers.parse_footer(line)
2788 if parsed_footer_line:
2789 # Line is a gerrit footer in the form: Footer-Key: any value.
2790 # Thus, must be appended observing Gerrit footer rules.
2791 self.set_description(
2792 git_footers.add_footer(self.description,
2793 key=parsed_footer_line[0],
2794 value=parsed_footer_line[1]))
2795 return
2796
2797 if not self._description_lines:
2798 self._description_lines.append(line)
2799 return
2800
2801 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2802 if gerrit_footers:
2803 # git_footers.split_footers ensures that there is an empty line before
2804 # actual (gerrit) footers, if any. We have to keep it that way.
2805 assert top_lines and top_lines[-1] == ''
2806 top_lines, separator = top_lines[:-1], top_lines[-1:]
2807 else:
2808 separator = [] # No need for separator if there are no gerrit_footers.
2809
2810 prev_line = top_lines[-1] if top_lines else ''
2811 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2812 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2813 top_lines.append('')
2814 top_lines.append(line)
2815 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002816
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002817 def get_reviewers(self):
2818 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002819 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2820 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002821 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002822
2823
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002824def get_approving_reviewers(props):
2825 """Retrieves the reviewers that approved a CL from the issue properties with
2826 messages.
2827
2828 Note that the list may contain reviewers that are not committer, thus are not
2829 considered by the CQ.
2830 """
2831 return sorted(
2832 set(
2833 message['sender']
2834 for message in props['messages']
2835 if message['approval'] and message['sender'] in props['reviewers']
2836 )
2837 )
2838
2839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840def FindCodereviewSettingsFile(filename='codereview.settings'):
2841 """Finds the given file starting in the cwd and going up.
2842
2843 Only looks up to the top of the repository unless an
2844 'inherit-review-settings-ok' file exists in the root of the repository.
2845 """
2846 inherit_ok_file = 'inherit-review-settings-ok'
2847 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002848 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2850 root = '/'
2851 while True:
2852 if filename in os.listdir(cwd):
2853 if os.path.isfile(os.path.join(cwd, filename)):
2854 return open(os.path.join(cwd, filename))
2855 if cwd == root:
2856 break
2857 cwd = os.path.dirname(cwd)
2858
2859
2860def LoadCodereviewSettingsFromFile(fileobj):
2861 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002862 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 def SetProperty(name, setting, unset_error_ok=False):
2865 fullname = 'rietveld.' + name
2866 if setting in keyvals:
2867 RunGit(['config', fullname, keyvals[setting]])
2868 else:
2869 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2870
2871 SetProperty('server', 'CODE_REVIEW_SERVER')
2872 # Only server setting is required. Other settings can be absent.
2873 # In that case, we ignore errors raised during option deletion attempt.
2874 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002875 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002876 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2877 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002878 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002879 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002880 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2881 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002882 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002883 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002884 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002885 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2886 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002887
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002888 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002889 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002890
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002891 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002892 RunGit(['config', 'gerrit.squash-uploads',
2893 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002894
tandrii@chromium.org28253532016-04-14 13:46:56 +00002895 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002896 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002897 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002899 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2900 #should be of the form
2901 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2902 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2903 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2904 keyvals['ORIGIN_URL_CONFIG']])
2905
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002906
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002907def urlretrieve(source, destination):
2908 """urllib is broken for SSL connections via a proxy therefore we
2909 can't use urllib.urlretrieve()."""
2910 with open(destination, 'w') as f:
2911 f.write(urllib2.urlopen(source).read())
2912
2913
ukai@chromium.org712d6102013-11-27 00:52:58 +00002914def hasSheBang(fname):
2915 """Checks fname is a #! script."""
2916 with open(fname) as f:
2917 return f.read(2).startswith('#!')
2918
2919
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002920# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2921def DownloadHooks(*args, **kwargs):
2922 pass
2923
2924
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002925def DownloadGerritHook(force):
2926 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002927
2928 Args:
2929 force: True to update hooks. False to install hooks if not present.
2930 """
2931 if not settings.GetIsGerrit():
2932 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002933 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002934 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2935 if not os.access(dst, os.X_OK):
2936 if os.path.exists(dst):
2937 if not force:
2938 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002939 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002940 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002941 if not hasSheBang(dst):
2942 DieWithError('Not a script: %s\n'
2943 'You need to download from\n%s\n'
2944 'into .git/hooks/commit-msg and '
2945 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002946 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2947 except Exception:
2948 if os.path.exists(dst):
2949 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002950 DieWithError('\nFailed to download hooks.\n'
2951 'You need to download from\n%s\n'
2952 'into .git/hooks/commit-msg and '
2953 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002954
2955
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002956
2957def GetRietveldCodereviewSettingsInteractively():
2958 """Prompt the user for settings."""
2959 server = settings.GetDefaultServerUrl(error_ok=True)
2960 prompt = 'Rietveld server (host[:port])'
2961 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2962 newserver = ask_for_data(prompt + ':')
2963 if not server and not newserver:
2964 newserver = DEFAULT_SERVER
2965 if newserver:
2966 newserver = gclient_utils.UpgradeToHttps(newserver)
2967 if newserver != server:
2968 RunGit(['config', 'rietveld.server', newserver])
2969
2970 def SetProperty(initial, caption, name, is_url):
2971 prompt = caption
2972 if initial:
2973 prompt += ' ("x" to clear) [%s]' % initial
2974 new_val = ask_for_data(prompt + ':')
2975 if new_val == 'x':
2976 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2977 elif new_val:
2978 if is_url:
2979 new_val = gclient_utils.UpgradeToHttps(new_val)
2980 if new_val != initial:
2981 RunGit(['config', 'rietveld.' + name, new_val])
2982
2983 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2984 SetProperty(settings.GetDefaultPrivateFlag(),
2985 'Private flag (rietveld only)', 'private', False)
2986 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2987 'tree-status-url', False)
2988 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2989 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2990 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2991 'run-post-upload-hook', False)
2992
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002993@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002994def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002995 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002996
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002997 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002998 'For Gerrit, see http://crbug.com/603116.')
2999 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003000 parser.add_option('--activate-update', action='store_true',
3001 help='activate auto-updating [rietveld] section in '
3002 '.git/config')
3003 parser.add_option('--deactivate-update', action='store_true',
3004 help='deactivate auto-updating [rietveld] section in '
3005 '.git/config')
3006 options, args = parser.parse_args(args)
3007
3008 if options.deactivate_update:
3009 RunGit(['config', 'rietveld.autoupdate', 'false'])
3010 return
3011
3012 if options.activate_update:
3013 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3014 return
3015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003016 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003017 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003018 return 0
3019
3020 url = args[0]
3021 if not url.endswith('codereview.settings'):
3022 url = os.path.join(url, 'codereview.settings')
3023
3024 # Load code review settings and download hooks (if available).
3025 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3026 return 0
3027
3028
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003029def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003030 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003031 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3032 branch = ShortBranchName(branchref)
3033 _, args = parser.parse_args(args)
3034 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003035 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003036 return RunGit(['config', 'branch.%s.base-url' % branch],
3037 error_ok=False).strip()
3038 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003039 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003040 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3041 error_ok=False).strip()
3042
3043
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003044def color_for_status(status):
3045 """Maps a Changelist status to color, for CMDstatus and other tools."""
3046 return {
3047 'unsent': Fore.RED,
3048 'waiting': Fore.BLUE,
3049 'reply': Fore.YELLOW,
3050 'lgtm': Fore.GREEN,
3051 'commit': Fore.MAGENTA,
3052 'closed': Fore.CYAN,
3053 'error': Fore.WHITE,
3054 }.get(status, Fore.WHITE)
3055
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003056
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003057def get_cl_statuses(changes, fine_grained, max_processes=None):
3058 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003059
3060 If fine_grained is true, this will fetch CL statuses from the server.
3061 Otherwise, simply indicate if there's a matching url for the given branches.
3062
3063 If max_processes is specified, it is used as the maximum number of processes
3064 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3065 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003066
3067 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003068 """
3069 # Silence upload.py otherwise it becomes unwieldly.
3070 upload.verbosity = 0
3071
3072 if fine_grained:
3073 # Process one branch synchronously to work through authentication, then
3074 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003075 if changes:
3076 fetch = lambda cl: (cl, cl.GetStatus())
3077 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003078
kmarshall3bff56b2016-06-06 18:31:47 -07003079 if not changes:
3080 # Exit early if there was only one branch to fetch.
3081 return
3082
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003083 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003084 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003085 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003086 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003087 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003088
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003089 fetched_cls = set()
3090 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003091 while True:
3092 try:
3093 row = it.next(timeout=5)
3094 except multiprocessing.TimeoutError:
3095 break
3096
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003097 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003098 yield row
3099
3100 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003101 for cl in set(changes_to_fetch) - fetched_cls:
3102 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003103
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003104 else:
3105 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003106 for cl in changes:
3107 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003108
rmistry@google.com2dd99862015-06-22 12:22:18 +00003109
3110def upload_branch_deps(cl, args):
3111 """Uploads CLs of local branches that are dependents of the current branch.
3112
3113 If the local branch dependency tree looks like:
3114 test1 -> test2.1 -> test3.1
3115 -> test3.2
3116 -> test2.2 -> test3.3
3117
3118 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3119 run on the dependent branches in this order:
3120 test2.1, test3.1, test3.2, test2.2, test3.3
3121
3122 Note: This function does not rebase your local dependent branches. Use it when
3123 you make a change to the parent branch that will not conflict with its
3124 dependent branches, and you would like their dependencies updated in
3125 Rietveld.
3126 """
3127 if git_common.is_dirty_git_tree('upload-branch-deps'):
3128 return 1
3129
3130 root_branch = cl.GetBranch()
3131 if root_branch is None:
3132 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3133 'Get on a branch!')
3134 if not cl.GetIssue() or not cl.GetPatchset():
3135 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3136 'patchset dependencies without an uploaded CL.')
3137
3138 branches = RunGit(['for-each-ref',
3139 '--format=%(refname:short) %(upstream:short)',
3140 'refs/heads'])
3141 if not branches:
3142 print('No local branches found.')
3143 return 0
3144
3145 # Create a dictionary of all local branches to the branches that are dependent
3146 # on it.
3147 tracked_to_dependents = collections.defaultdict(list)
3148 for b in branches.splitlines():
3149 tokens = b.split()
3150 if len(tokens) == 2:
3151 branch_name, tracked = tokens
3152 tracked_to_dependents[tracked].append(branch_name)
3153
vapiera7fbd5a2016-06-16 09:17:49 -07003154 print()
3155 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003156 dependents = []
3157 def traverse_dependents_preorder(branch, padding=''):
3158 dependents_to_process = tracked_to_dependents.get(branch, [])
3159 padding += ' '
3160 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003161 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003162 dependents.append(dependent)
3163 traverse_dependents_preorder(dependent, padding)
3164 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003165 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003166
3167 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003168 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003169 return 0
3170
vapiera7fbd5a2016-06-16 09:17:49 -07003171 print('This command will checkout all dependent branches and run '
3172 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003173 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3174
andybons@chromium.org962f9462016-02-03 20:00:42 +00003175 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003176 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003177 args.extend(['-t', 'Updated patchset dependency'])
3178
rmistry@google.com2dd99862015-06-22 12:22:18 +00003179 # Record all dependents that failed to upload.
3180 failures = {}
3181 # Go through all dependents, checkout the branch and upload.
3182 try:
3183 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003184 print()
3185 print('--------------------------------------')
3186 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003187 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003188 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003189 try:
3190 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003191 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003192 failures[dependent_branch] = 1
3193 except: # pylint: disable=W0702
3194 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003195 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003196 finally:
3197 # Swap back to the original root branch.
3198 RunGit(['checkout', '-q', root_branch])
3199
vapiera7fbd5a2016-06-16 09:17:49 -07003200 print()
3201 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003202 for dependent_branch in dependents:
3203 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003204 print(' %s : %s' % (dependent_branch, upload_status))
3205 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003206
3207 return 0
3208
3209
kmarshall3bff56b2016-06-06 18:31:47 -07003210def CMDarchive(parser, args):
3211 """Archives and deletes branches associated with closed changelists."""
3212 parser.add_option(
3213 '-j', '--maxjobs', action='store', type=int,
3214 help='The maximum number of jobs to use when retrieving review status')
3215 parser.add_option(
3216 '-f', '--force', action='store_true',
3217 help='Bypasses the confirmation prompt.')
3218
3219 auth.add_auth_options(parser)
3220 options, args = parser.parse_args(args)
3221 if args:
3222 parser.error('Unsupported args: %s' % ' '.join(args))
3223 auth_config = auth.extract_auth_config_from_options(options)
3224
3225 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3226 if not branches:
3227 return 0
3228
vapiera7fbd5a2016-06-16 09:17:49 -07003229 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003230 changes = [Changelist(branchref=b, auth_config=auth_config)
3231 for b in branches.splitlines()]
3232 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3233 statuses = get_cl_statuses(changes,
3234 fine_grained=True,
3235 max_processes=options.maxjobs)
3236 proposal = [(cl.GetBranch(),
3237 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3238 for cl, status in statuses
3239 if status == 'closed']
3240 proposal.sort()
3241
3242 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003243 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003244 return 0
3245
3246 current_branch = GetCurrentBranch()
3247
vapiera7fbd5a2016-06-16 09:17:49 -07003248 print('\nBranches with closed issues that will be archived:\n')
3249 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003250 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003251 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003252
3253 if any(branch == current_branch for branch, _ in proposal):
3254 print('You are currently on a branch \'%s\' which is associated with a '
3255 'closed codereview issue, so archive cannot proceed. Please '
3256 'checkout another branch and run this command again.' %
3257 current_branch)
3258 return 1
3259
3260 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003261 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3262 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003263 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003264 return 1
3265
3266 for branch, tagname in proposal:
3267 RunGit(['tag', tagname, branch])
3268 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003269 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003270
3271 return 0
3272
3273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003274def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003275 """Show status of changelists.
3276
3277 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003278 - Red not sent for review or broken
3279 - Blue waiting for review
3280 - Yellow waiting for you to reply to review
3281 - Green LGTM'ed
3282 - Magenta in the commit queue
3283 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003284
3285 Also see 'git cl comments'.
3286 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003287 parser.add_option('--field',
3288 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003289 parser.add_option('-f', '--fast', action='store_true',
3290 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003291 parser.add_option(
3292 '-j', '--maxjobs', action='store', type=int,
3293 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003294
3295 auth.add_auth_options(parser)
3296 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003297 if args:
3298 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003299 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003300
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003302 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003304 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 elif options.field == 'id':
3306 issueid = cl.GetIssue()
3307 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003308 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003309 elif options.field == 'patch':
3310 patchset = cl.GetPatchset()
3311 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003312 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003313 elif options.field == 'url':
3314 url = cl.GetIssueURL()
3315 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003316 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003317 return 0
3318
3319 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3320 if not branches:
3321 print('No local branch found.')
3322 return 0
3323
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003324 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003325 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003326 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003327 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003328 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003329 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003330 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003331
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003332 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003333 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3334 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3335 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003336 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003337 c, status = output.next()
3338 branch_statuses[c.GetBranch()] = status
3339 status = branch_statuses.pop(branch)
3340 url = cl.GetIssueURL()
3341 if url and (not status or status == 'error'):
3342 # The issue probably doesn't exist anymore.
3343 url += ' (broken)'
3344
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003345 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003346 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003347 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003348 color = ''
3349 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003350 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003351 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003352 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003353 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003354
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003355 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003356 print()
3357 print('Current branch:',)
3358 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003359 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003360 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003361 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003362 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003363 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003364 print('Issue description:')
3365 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003366 return 0
3367
3368
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003369def colorize_CMDstatus_doc():
3370 """To be called once in main() to add colors to git cl status help."""
3371 colors = [i for i in dir(Fore) if i[0].isupper()]
3372
3373 def colorize_line(line):
3374 for color in colors:
3375 if color in line.upper():
3376 # Extract whitespaces first and the leading '-'.
3377 indent = len(line) - len(line.lstrip(' ')) + 1
3378 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3379 return line
3380
3381 lines = CMDstatus.__doc__.splitlines()
3382 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3383
3384
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003385@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003386def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003387 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003388
3389 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003390 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003391 parser.add_option('-r', '--reverse', action='store_true',
3392 help='Lookup the branch(es) for the specified issues. If '
3393 'no issues are specified, all branches with mapped '
3394 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003395 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003396 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003397 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398
dnj@chromium.org406c4402015-03-03 17:22:28 +00003399 if options.reverse:
3400 branches = RunGit(['for-each-ref', 'refs/heads',
3401 '--format=%(refname:short)']).splitlines()
3402
3403 # Reverse issue lookup.
3404 issue_branch_map = {}
3405 for branch in branches:
3406 cl = Changelist(branchref=branch)
3407 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3408 if not args:
3409 args = sorted(issue_branch_map.iterkeys())
3410 for issue in args:
3411 if not issue:
3412 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003413 print('Branch for issue number %s: %s' % (
3414 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003415 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003416 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003417 if len(args) > 0:
3418 try:
3419 issue = int(args[0])
3420 except ValueError:
3421 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003422 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003423 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003424 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425 return 0
3426
3427
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003428def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003429 """Shows or posts review comments for any changelist."""
3430 parser.add_option('-a', '--add-comment', dest='comment',
3431 help='comment to add to an issue')
3432 parser.add_option('-i', dest='issue',
3433 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003434 parser.add_option('-j', '--json-file',
3435 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003436 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003437 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003438 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003439
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003440 issue = None
3441 if options.issue:
3442 try:
3443 issue = int(options.issue)
3444 except ValueError:
3445 DieWithError('A review issue id is expected to be a number')
3446
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003447 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003448
3449 if options.comment:
3450 cl.AddComment(options.comment)
3451 return 0
3452
3453 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003454 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003455 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003456 summary.append({
3457 'date': message['date'],
3458 'lgtm': False,
3459 'message': message['text'],
3460 'not_lgtm': False,
3461 'sender': message['sender'],
3462 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003463 if message['disapproval']:
3464 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003465 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003466 elif message['approval']:
3467 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003468 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003469 elif message['sender'] == data['owner_email']:
3470 color = Fore.MAGENTA
3471 else:
3472 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003474 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003475 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003476 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003477 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003478 if options.json_file:
3479 with open(options.json_file, 'wb') as f:
3480 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003481 return 0
3482
3483
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003484@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003485def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003486 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003487 parser.add_option('-d', '--display', action='store_true',
3488 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003489 parser.add_option('-n', '--new-description',
3490 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003491
3492 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003493 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003494 options, args = parser.parse_args(args)
3495 _process_codereview_select_options(parser, options)
3496
3497 target_issue = None
3498 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003499 target_issue = ParseIssueNumberArgument(args[0])
3500 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003501 parser.print_help()
3502 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003503
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003504 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003505
martiniss6eda05f2016-06-30 10:18:35 -07003506 kwargs = {
3507 'auth_config': auth_config,
3508 'codereview': options.forced_codereview,
3509 }
3510 if target_issue:
3511 kwargs['issue'] = target_issue.issue
3512 if options.forced_codereview == 'rietveld':
3513 kwargs['rietveld_server'] = target_issue.hostname
3514
3515 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003516
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003517 if not cl.GetIssue():
3518 DieWithError('This branch has no associated changelist.')
3519 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003520
smut@google.com34fb6b12015-07-13 20:03:26 +00003521 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003522 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003523 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003524
3525 if options.new_description:
3526 text = options.new_description
3527 if text == '-':
3528 text = '\n'.join(l.rstrip() for l in sys.stdin)
3529
3530 description.set_description(text)
3531 else:
3532 description.prompt()
3533
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003534 if cl.GetDescription() != description.description:
3535 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003536 return 0
3537
3538
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539def CreateDescriptionFromLog(args):
3540 """Pulls out the commit log to use as a base for the CL description."""
3541 log_args = []
3542 if len(args) == 1 and not args[0].endswith('.'):
3543 log_args = [args[0] + '..']
3544 elif len(args) == 1 and args[0].endswith('...'):
3545 log_args = [args[0][:-1]]
3546 elif len(args) == 2:
3547 log_args = [args[0] + '..' + args[1]]
3548 else:
3549 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003550 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003551
3552
thestig@chromium.org44202a22014-03-11 19:22:18 +00003553def CMDlint(parser, args):
3554 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003555 parser.add_option('--filter', action='append', metavar='-x,+y',
3556 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003557 auth.add_auth_options(parser)
3558 options, args = parser.parse_args(args)
3559 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003560
3561 # Access to a protected member _XX of a client class
3562 # pylint: disable=W0212
3563 try:
3564 import cpplint
3565 import cpplint_chromium
3566 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003568 return 1
3569
3570 # Change the current working directory before calling lint so that it
3571 # shows the correct base.
3572 previous_cwd = os.getcwd()
3573 os.chdir(settings.GetRoot())
3574 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003575 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003576 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3577 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003578 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003579 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003580 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003581
3582 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003583 command = args + files
3584 if options.filter:
3585 command = ['--filter=' + ','.join(options.filter)] + command
3586 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003587
3588 white_regex = re.compile(settings.GetLintRegex())
3589 black_regex = re.compile(settings.GetLintIgnoreRegex())
3590 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3591 for filename in filenames:
3592 if white_regex.match(filename):
3593 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003595 else:
3596 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3597 extra_check_functions)
3598 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003599 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003600 finally:
3601 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003602 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003603 if cpplint._cpplint_state.error_count != 0:
3604 return 1
3605 return 0
3606
3607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003608def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003609 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003610 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003611 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003612 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003613 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003614 auth.add_auth_options(parser)
3615 options, args = parser.parse_args(args)
3616 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617
sbc@chromium.org71437c02015-04-09 19:29:40 +00003618 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003619 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620 return 1
3621
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003622 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623 if args:
3624 base_branch = args[0]
3625 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003626 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003627 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003628
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003629 cl.RunHook(
3630 committing=not options.upload,
3631 may_prompt=False,
3632 verbose=options.verbose,
3633 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003634 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003635
3636
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003637def GenerateGerritChangeId(message):
3638 """Returns Ixxxxxx...xxx change id.
3639
3640 Works the same way as
3641 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3642 but can be called on demand on all platforms.
3643
3644 The basic idea is to generate git hash of a state of the tree, original commit
3645 message, author/committer info and timestamps.
3646 """
3647 lines = []
3648 tree_hash = RunGitSilent(['write-tree'])
3649 lines.append('tree %s' % tree_hash.strip())
3650 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3651 if code == 0:
3652 lines.append('parent %s' % parent.strip())
3653 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3654 lines.append('author %s' % author.strip())
3655 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3656 lines.append('committer %s' % committer.strip())
3657 lines.append('')
3658 # Note: Gerrit's commit-hook actually cleans message of some lines and
3659 # whitespace. This code is not doing this, but it clearly won't decrease
3660 # entropy.
3661 lines.append(message)
3662 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3663 stdin='\n'.join(lines))
3664 return 'I%s' % change_hash.strip()
3665
3666
wittman@chromium.org455dc922015-01-26 20:15:50 +00003667def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3668 """Computes the remote branch ref to use for the CL.
3669
3670 Args:
3671 remote (str): The git remote for the CL.
3672 remote_branch (str): The git remote branch for the CL.
3673 target_branch (str): The target branch specified by the user.
3674 pending_prefix (str): The pending prefix from the settings.
3675 """
3676 if not (remote and remote_branch):
3677 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003678
wittman@chromium.org455dc922015-01-26 20:15:50 +00003679 if target_branch:
3680 # Cannonicalize branch references to the equivalent local full symbolic
3681 # refs, which are then translated into the remote full symbolic refs
3682 # below.
3683 if '/' not in target_branch:
3684 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3685 else:
3686 prefix_replacements = (
3687 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3688 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3689 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3690 )
3691 match = None
3692 for regex, replacement in prefix_replacements:
3693 match = re.search(regex, target_branch)
3694 if match:
3695 remote_branch = target_branch.replace(match.group(0), replacement)
3696 break
3697 if not match:
3698 # This is a branch path but not one we recognize; use as-is.
3699 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003700 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3701 # Handle the refs that need to land in different refs.
3702 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003703
wittman@chromium.org455dc922015-01-26 20:15:50 +00003704 # Create the true path to the remote branch.
3705 # Does the following translation:
3706 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3707 # * refs/remotes/origin/master -> refs/heads/master
3708 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3709 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3710 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3711 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3712 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3713 'refs/heads/')
3714 elif remote_branch.startswith('refs/remotes/branch-heads'):
3715 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3716 # If a pending prefix exists then replace refs/ with it.
3717 if pending_prefix:
3718 remote_branch = remote_branch.replace('refs/', pending_prefix)
3719 return remote_branch
3720
3721
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003722def cleanup_list(l):
3723 """Fixes a list so that comma separated items are put as individual items.
3724
3725 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3726 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3727 """
3728 items = sum((i.split(',') for i in l), [])
3729 stripped_items = (i.strip() for i in items)
3730 return sorted(filter(None, stripped_items))
3731
3732
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003733@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003734def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003735 """Uploads the current changelist to codereview.
3736
3737 Can skip dependency patchset uploads for a branch by running:
3738 git config branch.branch_name.skip-deps-uploads True
3739 To unset run:
3740 git config --unset branch.branch_name.skip-deps-uploads
3741 Can also set the above globally by using the --global flag.
3742 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003743 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3744 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003745 parser.add_option('--bypass-watchlists', action='store_true',
3746 dest='bypass_watchlists',
3747 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003748 parser.add_option('-f', action='store_true', dest='force',
3749 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003750 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003751 parser.add_option('-b', '--bug',
3752 help='pre-populate the bug number(s) for this issue. '
3753 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003754 parser.add_option('--message-file', dest='message_file',
3755 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003756 parser.add_option('-t', dest='title',
3757 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003758 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003759 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003760 help='reviewer email addresses')
3761 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003762 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003763 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003764 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003765 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003766 parser.add_option('--emulate_svn_auto_props',
3767 '--emulate-svn-auto-props',
3768 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003769 dest="emulate_svn_auto_props",
3770 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003771 parser.add_option('-c', '--use-commit-queue', action='store_true',
3772 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003773 parser.add_option('--private', action='store_true',
3774 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003775 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003776 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003777 metavar='TARGET',
3778 help='Apply CL to remote ref TARGET. ' +
3779 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003780 parser.add_option('--squash', action='store_true',
3781 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003782 parser.add_option('--no-squash', action='store_true',
3783 help='Don\'t squash multiple commits into one ' +
3784 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003785 parser.add_option('--email', default=None,
3786 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003787 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3788 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003789 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3790 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003791 help='Send the patchset to do a CQ dry run right after '
3792 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003793 parser.add_option('--dependencies', action='store_true',
3794 help='Uploads CLs of all the local branches that depend on '
3795 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003796
rmistry@google.com2dd99862015-06-22 12:22:18 +00003797 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003798 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003799 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003800 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003801 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003802 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003803 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003804
sbc@chromium.org71437c02015-04-09 19:29:40 +00003805 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003806 return 1
3807
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003808 options.reviewers = cleanup_list(options.reviewers)
3809 options.cc = cleanup_list(options.cc)
3810
tandriib80458a2016-06-23 12:20:07 -07003811 if options.message_file:
3812 if options.message:
3813 parser.error('only one of --message and --message-file allowed.')
3814 options.message = gclient_utils.FileRead(options.message_file)
3815 options.message_file = None
3816
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003817 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3818 settings.GetIsGerrit()
3819
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003820 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003821 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003822
3823
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003824def IsSubmoduleMergeCommit(ref):
3825 # When submodules are added to the repo, we expect there to be a single
3826 # non-git-svn merge commit at remote HEAD with a signature comment.
3827 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003828 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003829 return RunGit(cmd) != ''
3830
3831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003833 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003835 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3836 upstream and closes the issue automatically and atomically.
3837
3838 Otherwise (in case of Rietveld):
3839 Squashes branch into a single commit.
3840 Updates changelog with metadata (e.g. pointer to review).
3841 Pushes/dcommits the code upstream.
3842 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 """
3844 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3845 help='bypass upload presubmit hook')
3846 parser.add_option('-m', dest='message',
3847 help="override review description")
3848 parser.add_option('-f', action='store_true', dest='force',
3849 help="force yes to questions (don't prompt)")
3850 parser.add_option('-c', dest='contributor',
3851 help="external contributor for patch (appended to " +
3852 "description and used as author for git). Should be " +
3853 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003854 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003855 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003856 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003857 auth_config = auth.extract_auth_config_from_options(options)
3858
3859 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003861 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3862 if cl.IsGerrit():
3863 if options.message:
3864 # This could be implemented, but it requires sending a new patch to
3865 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3866 # Besides, Gerrit has the ability to change the commit message on submit
3867 # automatically, thus there is no need to support this option (so far?).
3868 parser.error('-m MESSAGE option is not supported for Gerrit.')
3869 if options.contributor:
3870 parser.error(
3871 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3872 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3873 'the contributor\'s "name <email>". If you can\'t upload such a '
3874 'commit for review, contact your repository admin and request'
3875 '"Forge-Author" permission.')
3876 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3877 options.verbose)
3878
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003879 current = cl.GetBranch()
3880 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3881 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003882 print()
3883 print('Attempting to push branch %r into another local branch!' % current)
3884 print()
3885 print('Either reparent this branch on top of origin/master:')
3886 print(' git reparent-branch --root')
3887 print()
3888 print('OR run `git rebase-update` if you think the parent branch is ')
3889 print('already committed.')
3890 print()
3891 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003892 return 1
3893
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003894 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895 # Default to merging against our best guess of the upstream branch.
3896 args = [cl.GetUpstreamBranch()]
3897
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003898 if options.contributor:
3899 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003900 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003901 return 1
3902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003904 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905
sbc@chromium.org71437c02015-04-09 19:29:40 +00003906 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003907 return 1
3908
3909 # This rev-list syntax means "show all commits not in my branch that
3910 # are in base_branch".
3911 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3912 base_branch]).splitlines()
3913 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003914 print('Base branch "%s" has %d commits '
3915 'not in this branch.' % (base_branch, len(upstream_commits)))
3916 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 return 1
3918
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003919 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003920 svn_head = None
3921 if cmd == 'dcommit' or base_has_submodules:
3922 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3923 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003926 # If the base_head is a submodule merge commit, the first parent of the
3927 # base_head should be a git-svn commit, which is what we're interested in.
3928 base_svn_head = base_branch
3929 if base_has_submodules:
3930 base_svn_head += '^1'
3931
3932 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003934 print('This branch has %d additional commits not upstreamed yet.'
3935 % len(extra_commits.splitlines()))
3936 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3937 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003938 return 1
3939
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003940 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003941 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003942 author = None
3943 if options.contributor:
3944 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003945 hook_results = cl.RunHook(
3946 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003947 may_prompt=not options.force,
3948 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003949 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003950 if not hook_results.should_continue():
3951 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003953 # Check the tree status if the tree status URL is set.
3954 status = GetTreeStatus()
3955 if 'closed' == status:
3956 print('The tree is closed. Please wait for it to reopen. Use '
3957 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3958 return 1
3959 elif 'unknown' == status:
3960 print('Unable to determine tree status. Please verify manually and '
3961 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3962 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003963
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003964 change_desc = ChangeDescription(options.message)
3965 if not change_desc.description and cl.GetIssue():
3966 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003967
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003968 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003969 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003970 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003971 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003972 print('No description set.')
3973 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003974 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003975
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003976 # Keep a separate copy for the commit message, because the commit message
3977 # contains the link to the Rietveld issue, while the Rietveld message contains
3978 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003979 # Keep a separate copy for the commit message.
3980 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003981 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003982
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003983 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003984 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003985 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003986 # after it. Add a period on a new line to circumvent this. Also add a space
3987 # before the period to make sure that Gitiles continues to correctly resolve
3988 # the URL.
3989 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003991 commit_desc.append_footer('Patch from %s.' % options.contributor)
3992
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003993 print('Description:')
3994 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003996 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003998 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004000 # We want to squash all this branch's commits into one commit with the proper
4001 # description. We do this by doing a "reset --soft" to the base branch (which
4002 # keeps the working copy the same), then dcommitting that. If origin/master
4003 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4004 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004006 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4007 # Delete the branches if they exist.
4008 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4009 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4010 result = RunGitWithCode(showref_cmd)
4011 if result[0] == 0:
4012 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013
4014 # We might be in a directory that's present in this branch but not in the
4015 # trunk. Move up to the top of the tree so that git commands that expect a
4016 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004017 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004018 if rel_base_path:
4019 os.chdir(rel_base_path)
4020
4021 # Stuff our change into the merge branch.
4022 # We wrap in a try...finally block so if anything goes wrong,
4023 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004024 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004025 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004026 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004027 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004028 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004029 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004030 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004032 RunGit(
4033 [
4034 'commit', '--author', options.contributor,
4035 '-m', commit_desc.description,
4036 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004037 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004038 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004039 if base_has_submodules:
4040 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4041 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4042 RunGit(['checkout', CHERRY_PICK_BRANCH])
4043 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004044 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004045 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004046 mirror = settings.GetGitMirror(remote)
4047 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004048 pending_prefix = settings.GetPendingRefPrefix()
4049 if not pending_prefix or branch.startswith(pending_prefix):
4050 # If not using refs/pending/heads/* at all, or target ref is already set
4051 # to pending, then push to the target ref directly.
4052 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004053 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004054 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004055 else:
4056 # Cherry-pick the change on top of pending ref and then push it.
4057 assert branch.startswith('refs/'), branch
4058 assert pending_prefix[-1] == '/', pending_prefix
4059 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004060 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004061 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004062 if retcode == 0:
4063 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064 else:
4065 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004066 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004067 'svn', 'dcommit',
4068 '-C%s' % options.similarity,
4069 '--no-rebase', '--rmdir',
4070 ]
4071 if settings.GetForceHttpsCommitUrl():
4072 # Allow forcing https commit URLs for some projects that don't allow
4073 # committing to http URLs (like Google Code).
4074 remote_url = cl.GetGitSvnRemoteUrl()
4075 if urlparse.urlparse(remote_url).scheme == 'http':
4076 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004077 cmd_args.append('--commit-url=%s' % remote_url)
4078 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004079 if 'Committed r' in output:
4080 revision = re.match(
4081 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4082 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083 finally:
4084 # And then swap back to the original branch and clean up.
4085 RunGit(['checkout', '-q', cl.GetBranch()])
4086 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004087 if base_has_submodules:
4088 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004089
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004090 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004091 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004092 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004093
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004094 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004095 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004096 try:
4097 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4098 # We set pushed_to_pending to False, since it made it all the way to the
4099 # real ref.
4100 pushed_to_pending = False
4101 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004102 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004104 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004105 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004106 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004107 if not to_pending:
4108 if viewvc_url and revision:
4109 change_desc.append_footer(
4110 'Committed: %s%s' % (viewvc_url, revision))
4111 elif revision:
4112 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004113 print('Closing issue '
4114 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004115 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004117 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004118 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004119 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004120 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004121 if options.bypass_hooks:
4122 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4123 else:
4124 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004125 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004126
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004127 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004128 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004129 print('The commit is in the pending queue (%s).' % pending_ref)
4130 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4131 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004132
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004133 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4134 if os.path.isfile(hook):
4135 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004136
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004137 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138
4139
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004140def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004141 print()
4142 print('Waiting for commit to be landed on %s...' % real_ref)
4143 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004144 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4145 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004146 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004147
4148 loop = 0
4149 while True:
4150 sys.stdout.write('fetching (%d)... \r' % loop)
4151 sys.stdout.flush()
4152 loop += 1
4153
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004154 if mirror:
4155 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004156 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4157 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4158 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4159 for commit in commits.splitlines():
4160 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004162 return commit
4163
4164 current_rev = to_rev
4165
4166
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004167def PushToGitPending(remote, pending_ref, upstream_ref):
4168 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4169
4170 Returns:
4171 (retcode of last operation, output log of last operation).
4172 """
4173 assert pending_ref.startswith('refs/'), pending_ref
4174 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4175 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4176 code = 0
4177 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004178 max_attempts = 3
4179 attempts_left = max_attempts
4180 while attempts_left:
4181 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004182 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004183 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004184
4185 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004186 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004187 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004188 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004189 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004190 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004191 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004192 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004193 continue
4194
4195 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004197 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004198 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004199 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004200 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4201 'the following files have merge conflicts:' % pending_ref)
4202 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4203 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004204 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004205 return code, out
4206
4207 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004208 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004209 code, out = RunGitWithCode(
4210 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4211 if code == 0:
4212 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004213 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004214 return code, out
4215
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004217 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004218 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004219 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004220 print('Fatal push error. Make sure your .netrc credentials and git '
4221 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004222 return code, out
4223
vapiera7fbd5a2016-06-16 09:17:49 -07004224 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004225 return code, out
4226
4227
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004228def IsFatalPushFailure(push_stdout):
4229 """True if retrying push won't help."""
4230 return '(prohibited by Gerrit)' in push_stdout
4231
4232
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004233@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004234def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004235 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004237 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004238 # If it looks like previous commits were mirrored with git-svn.
4239 message = """This repository appears to be a git-svn mirror, but no
4240upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4241 else:
4242 message = """This doesn't appear to be an SVN repository.
4243If your project has a true, writeable git repository, you probably want to run
4244'git cl land' instead.
4245If your project has a git mirror of an upstream SVN master, you probably need
4246to run 'git svn init'.
4247
4248Using the wrong command might cause your commit to appear to succeed, and the
4249review to be closed, without actually landing upstream. If you choose to
4250proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004251 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004252 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004253 # TODO(tandrii): kill this post SVN migration with
4254 # https://codereview.chromium.org/2076683002
4255 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4256 'Please let us know of this project you are committing to:'
4257 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 return SendUpstream(parser, args, 'dcommit')
4259
4260
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004261@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004262def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004263 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004264 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265 print('This appears to be an SVN repository.')
4266 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004267 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004268 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004269 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270
4271
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004272@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004274 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275 parser.add_option('-b', dest='newbranch',
4276 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004277 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004278 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004279 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4280 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004281 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004282 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004283 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004284 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004286 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004287
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004288
4289 group = optparse.OptionGroup(
4290 parser,
4291 'Options for continuing work on the current issue uploaded from a '
4292 'different clone (e.g. different machine). Must be used independently '
4293 'from the other options. No issue number should be specified, and the '
4294 'branch must have an issue number associated with it')
4295 group.add_option('--reapply', action='store_true', dest='reapply',
4296 help='Reset the branch and reapply the issue.\n'
4297 'CAUTION: This will undo any local changes in this '
4298 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004299
4300 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004301 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004302 parser.add_option_group(group)
4303
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004304 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004305 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004306 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004307 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004308 auth_config = auth.extract_auth_config_from_options(options)
4309
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004310
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004311 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004312 if options.newbranch:
4313 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004314 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004315 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004316
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004317 cl = Changelist(auth_config=auth_config,
4318 codereview=options.forced_codereview)
4319 if not cl.GetIssue():
4320 parser.error('current branch must have an associated issue')
4321
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004322 upstream = cl.GetUpstreamBranch()
4323 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004324 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004325
4326 RunGit(['reset', '--hard', upstream])
4327 if options.pull:
4328 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004329
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004330 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4331 options.directory)
4332
4333 if len(args) != 1 or not args[0]:
4334 parser.error('Must specify issue number or url')
4335
4336 # We don't want uncommitted changes mixed up with the patch.
4337 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004338 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004340 if options.newbranch:
4341 if options.force:
4342 RunGit(['branch', '-D', options.newbranch],
4343 stderr=subprocess2.PIPE, error_ok=True)
4344 RunGit(['new-branch', options.newbranch])
4345
4346 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4347
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004348 if cl.IsGerrit():
4349 if options.reject:
4350 parser.error('--reject is not supported with Gerrit codereview.')
4351 if options.nocommit:
4352 parser.error('--nocommit is not supported with Gerrit codereview.')
4353 if options.directory:
4354 parser.error('--directory is not supported with Gerrit codereview.')
4355
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004356 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004357 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004358
4359
4360def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004361 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362 # Provide a wrapper for git svn rebase to help avoid accidental
4363 # git svn dcommit.
4364 # It's the only command that doesn't use parser at all since we just defer
4365 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004366
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004367 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004368
4369
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004370def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004371 """Fetches the tree status and returns either 'open', 'closed',
4372 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004373 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374 if url:
4375 status = urllib2.urlopen(url).read().lower()
4376 if status.find('closed') != -1 or status == '0':
4377 return 'closed'
4378 elif status.find('open') != -1 or status == '1':
4379 return 'open'
4380 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381 return 'unset'
4382
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384def GetTreeStatusReason():
4385 """Fetches the tree status from a json url and returns the message
4386 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004387 url = settings.GetTreeStatusUrl()
4388 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389 connection = urllib2.urlopen(json_url)
4390 status = json.loads(connection.read())
4391 connection.close()
4392 return status['message']
4393
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004394
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004395def GetBuilderMaster(bot_list):
4396 """For a given builder, fetch the master from AE if available."""
4397 map_url = 'https://builders-map.appspot.com/'
4398 try:
4399 master_map = json.load(urllib2.urlopen(map_url))
4400 except urllib2.URLError as e:
4401 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4402 (map_url, e))
4403 except ValueError as e:
4404 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4405 if not master_map:
4406 return None, 'Failed to build master map.'
4407
4408 result_master = ''
4409 for bot in bot_list:
4410 builder = bot.split(':', 1)[0]
4411 master_list = master_map.get(builder, [])
4412 if not master_list:
4413 return None, ('No matching master for builder %s.' % builder)
4414 elif len(master_list) > 1:
4415 return None, ('The builder name %s exists in multiple masters %s.' %
4416 (builder, master_list))
4417 else:
4418 cur_master = master_list[0]
4419 if not result_master:
4420 result_master = cur_master
4421 elif result_master != cur_master:
4422 return None, 'The builders do not belong to the same master.'
4423 return result_master, None
4424
4425
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004427 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004428 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004429 status = GetTreeStatus()
4430 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004431 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004432 return 2
4433
vapiera7fbd5a2016-06-16 09:17:49 -07004434 print('The tree is %s' % status)
4435 print()
4436 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437 if status != 'open':
4438 return 1
4439 return 0
4440
4441
maruel@chromium.org15192402012-09-06 12:38:29 +00004442def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004443 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004444 group = optparse.OptionGroup(parser, "Try job options")
4445 group.add_option(
4446 "-b", "--bot", action="append",
4447 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4448 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004449 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004450 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004451 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004452 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004453 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004454 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004455 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004456 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004457 "-r", "--revision",
4458 help="Revision to use for the try job; default: the "
4459 "revision will be determined by the try server; see "
4460 "its waterfall for more info")
4461 group.add_option(
4462 "-c", "--clobber", action="store_true", default=False,
4463 help="Force a clobber before building; e.g. don't do an "
4464 "incremental build")
4465 group.add_option(
4466 "--project",
4467 help="Override which project to use. Projects are defined "
4468 "server-side to define what default bot set to use")
4469 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004470 "-p", "--property", dest="properties", action="append", default=[],
4471 help="Specify generic properties in the form -p key1=value1 -p "
4472 "key2=value2 etc (buildbucket only). The value will be treated as "
4473 "json if decodable, or as string otherwise.")
4474 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004475 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004476 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004477 "--use-rietveld", action="store_true", default=False,
4478 help="Use Rietveld to trigger try jobs.")
4479 group.add_option(
4480 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4481 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004482 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004483 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004484 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004485 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004486
machenbach@chromium.org45453142015-09-15 08:45:22 +00004487 if options.use_rietveld and options.properties:
4488 parser.error('Properties can only be specified with buildbucket')
4489
4490 # Make sure that all properties are prop=value pairs.
4491 bad_params = [x for x in options.properties if '=' not in x]
4492 if bad_params:
4493 parser.error('Got properties with missing "=": %s' % bad_params)
4494
maruel@chromium.org15192402012-09-06 12:38:29 +00004495 if args:
4496 parser.error('Unknown arguments: %s' % args)
4497
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004498 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004499 if not cl.GetIssue():
4500 parser.error('Need to upload first')
4501
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004502 if cl.IsGerrit():
4503 parser.error(
4504 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4505 'If your project has Commit Queue, dry run is a workaround:\n'
4506 ' git cl set-commit --dry-run')
4507 # Code below assumes Rietveld issue.
4508 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4509
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004510 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004511 if props.get('closed'):
4512 parser.error('Cannot send tryjobs for a closed CL')
4513
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004514 if props.get('private'):
4515 parser.error('Cannot use trybots with private issue')
4516
maruel@chromium.org15192402012-09-06 12:38:29 +00004517 if not options.name:
4518 options.name = cl.GetBranch()
4519
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004520 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004521 options.master, err_msg = GetBuilderMaster(options.bot)
4522 if err_msg:
4523 parser.error('Tryserver master cannot be found because: %s\n'
4524 'Please manually specify the tryserver master'
4525 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004526
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004527 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004528 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004529 if not options.bot:
4530 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004531
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004532 # Get try masters from PRESUBMIT.py files.
4533 masters = presubmit_support.DoGetTryMasters(
4534 change,
4535 change.LocalPaths(),
4536 settings.GetRoot(),
4537 None,
4538 None,
4539 options.verbose,
4540 sys.stdout)
4541 if masters:
4542 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004543
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004544 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4545 options.bot = presubmit_support.DoGetTrySlaves(
4546 change,
4547 change.LocalPaths(),
4548 settings.GetRoot(),
4549 None,
4550 None,
4551 options.verbose,
4552 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004553
4554 if not options.bot:
4555 # Get try masters from cq.cfg if any.
4556 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4557 # location.
4558 cq_cfg = os.path.join(change.RepositoryRoot(),
4559 'infra', 'config', 'cq.cfg')
4560 if os.path.exists(cq_cfg):
4561 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004562 cq_masters = commit_queue.get_master_builder_map(
4563 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004564 for master, builders in cq_masters.iteritems():
4565 for builder in builders:
4566 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004567 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004568 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004569 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004570 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004571 else:
4572 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004573
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004574 if not options.bot:
4575 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004576
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004577 builders_and_tests = {}
4578 # TODO(machenbach): The old style command-line options don't support
4579 # multiple try masters yet.
4580 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4581 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4582
4583 for bot in old_style:
4584 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004585 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004586 elif ',' in bot:
4587 parser.error('Specify one bot per --bot flag')
4588 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004589 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004590
4591 for bot, tests in new_style:
4592 builders_and_tests.setdefault(bot, []).extend(tests)
4593
4594 # Return a master map with one master to be backwards compatible. The
4595 # master name defaults to an empty string, which will cause the master
4596 # not to be set on rietveld (deprecated).
4597 return {options.master: builders_and_tests}
4598
4599 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004600
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004601 for builders in masters.itervalues():
4602 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004603 print('ERROR You are trying to send a job to a triggered bot. This type '
4604 'of bot requires an\ninitial job from a parent (usually a builder).'
4605 ' Instead send your job to the parent.\n'
4606 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004607 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004608
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004609 patchset = cl.GetMostRecentPatchset()
4610 if patchset and patchset != cl.GetPatchset():
4611 print(
4612 '\nWARNING Mismatch between local config and server. Did a previous '
4613 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4614 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004615 if options.luci:
4616 trigger_luci_job(cl, masters, options)
4617 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004618 try:
4619 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4620 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004621 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004622 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004623 except Exception as e:
4624 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004625 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4626 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004627 return 1
4628 else:
4629 try:
4630 cl.RpcServer().trigger_distributed_try_jobs(
4631 cl.GetIssue(), patchset, options.name, options.clobber,
4632 options.revision, masters)
4633 except urllib2.HTTPError as e:
4634 if e.code == 404:
4635 print('404 from rietveld; '
4636 'did you mean to use "git try" instead of "git cl try"?')
4637 return 1
4638 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004639
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004640 for (master, builders) in sorted(masters.iteritems()):
4641 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004642 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004643 length = max(len(builder) for builder in builders)
4644 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004645 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004646 return 0
4647
4648
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004649def CMDtry_results(parser, args):
4650 group = optparse.OptionGroup(parser, "Try job results options")
4651 group.add_option(
4652 "-p", "--patchset", type=int, help="patchset number if not current.")
4653 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004654 "--print-master", action='store_true', help="print master name as well.")
4655 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004656 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004657 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004658 group.add_option(
4659 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4660 help="Host of buildbucket. The default host is %default.")
4661 parser.add_option_group(group)
4662 auth.add_auth_options(parser)
4663 options, args = parser.parse_args(args)
4664 if args:
4665 parser.error('Unrecognized args: %s' % ' '.join(args))
4666
4667 auth_config = auth.extract_auth_config_from_options(options)
4668 cl = Changelist(auth_config=auth_config)
4669 if not cl.GetIssue():
4670 parser.error('Need to upload first')
4671
4672 if not options.patchset:
4673 options.patchset = cl.GetMostRecentPatchset()
4674 if options.patchset and options.patchset != cl.GetPatchset():
4675 print(
4676 '\nWARNING Mismatch between local config and server. Did a previous '
4677 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4678 'Continuing using\npatchset %s.\n' % options.patchset)
4679 try:
4680 jobs = fetch_try_jobs(auth_config, cl, options)
4681 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004682 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004683 return 1
4684 except Exception as e:
4685 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004686 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4687 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004688 return 1
4689 print_tryjobs(options, jobs)
4690 return 0
4691
4692
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004693@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004695 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004696 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004697 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004698 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004700 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004701 if args:
4702 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004703 branch = cl.GetBranch()
4704 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004705 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004706 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004707
4708 # Clear configured merge-base, if there is one.
4709 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004710 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004711 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004712 return 0
4713
4714
thestig@chromium.org00858c82013-12-02 23:08:03 +00004715def CMDweb(parser, args):
4716 """Opens the current CL in the web browser."""
4717 _, args = parser.parse_args(args)
4718 if args:
4719 parser.error('Unrecognized args: %s' % ' '.join(args))
4720
4721 issue_url = Changelist().GetIssueURL()
4722 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004723 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004724 return 1
4725
4726 webbrowser.open(issue_url)
4727 return 0
4728
4729
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004730def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004731 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004732 parser.add_option('-d', '--dry-run', action='store_true',
4733 help='trigger in dry run mode')
4734 parser.add_option('-c', '--clear', action='store_true',
4735 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004736 auth.add_auth_options(parser)
4737 options, args = parser.parse_args(args)
4738 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004739 if args:
4740 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004741 if options.dry_run and options.clear:
4742 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4743
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004744 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004745 if options.clear:
4746 state = _CQState.CLEAR
4747 elif options.dry_run:
4748 state = _CQState.DRY_RUN
4749 else:
4750 state = _CQState.COMMIT
4751 if not cl.GetIssue():
4752 parser.error('Must upload the issue first')
4753 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004754 return 0
4755
4756
groby@chromium.org411034a2013-02-26 15:12:01 +00004757def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004758 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004759 auth.add_auth_options(parser)
4760 options, args = parser.parse_args(args)
4761 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004762 if args:
4763 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004764 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004765 # Ensure there actually is an issue to close.
4766 cl.GetDescription()
4767 cl.CloseIssue()
4768 return 0
4769
4770
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004771def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004772 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004773 auth.add_auth_options(parser)
4774 options, args = parser.parse_args(args)
4775 auth_config = auth.extract_auth_config_from_options(options)
4776 if args:
4777 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004778
4779 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004780 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004781 # Staged changes would be committed along with the patch from last
4782 # upload, hence counted toward the "last upload" side in the final
4783 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004784 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004785 return 1
4786
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004787 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004788 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004789 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004790 if not issue:
4791 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004792 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004793 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004794
4795 # Create a new branch based on the merge-base
4796 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004797 # Clear cached branch in cl object, to avoid overwriting original CL branch
4798 # properties.
4799 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004800 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004801 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004802 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004803 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004804 return rtn
4805
wychen@chromium.org06928532015-02-03 02:11:29 +00004806 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004807 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004808 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004809 finally:
4810 RunGit(['checkout', '-q', branch])
4811 RunGit(['branch', '-D', TMP_BRANCH])
4812
4813 return 0
4814
4815
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004816def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004817 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004818 parser.add_option(
4819 '--no-color',
4820 action='store_true',
4821 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004822 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004823 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004824 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004825
4826 author = RunGit(['config', 'user.email']).strip() or None
4827
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004828 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004829
4830 if args:
4831 if len(args) > 1:
4832 parser.error('Unknown args')
4833 base_branch = args[0]
4834 else:
4835 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004836 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004837
4838 change = cl.GetChange(base_branch, None)
4839 return owners_finder.OwnersFinder(
4840 [f.LocalPath() for f in
4841 cl.GetChange(base_branch, None).AffectedFiles()],
4842 change.RepositoryRoot(), author,
4843 fopen=file, os_path=os.path, glob=glob.glob,
4844 disable_color=options.no_color).run()
4845
4846
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004847def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004848 """Generates a diff command."""
4849 # Generate diff for the current branch's changes.
4850 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4851 upstream_commit, '--' ]
4852
4853 if args:
4854 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004855 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004856 diff_cmd.append(arg)
4857 else:
4858 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004859
4860 return diff_cmd
4861
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004862def MatchingFileType(file_name, extensions):
4863 """Returns true if the file name ends with one of the given extensions."""
4864 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004865
enne@chromium.org555cfe42014-01-29 18:21:39 +00004866@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004867def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004868 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004869 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004870 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004871 parser.add_option('--full', action='store_true',
4872 help='Reformat the full content of all touched files')
4873 parser.add_option('--dry-run', action='store_true',
4874 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004875 parser.add_option('--python', action='store_true',
4876 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004877 parser.add_option('--diff', action='store_true',
4878 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004879 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004880
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004881 # git diff generates paths against the root of the repository. Change
4882 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004883 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004884 if rel_base_path:
4885 os.chdir(rel_base_path)
4886
digit@chromium.org29e47272013-05-17 17:01:46 +00004887 # Grab the merge-base commit, i.e. the upstream commit of the current
4888 # branch when it was created or the last time it was rebased. This is
4889 # to cover the case where the user may have called "git fetch origin",
4890 # moving the origin branch to a newer commit, but hasn't rebased yet.
4891 upstream_commit = None
4892 cl = Changelist()
4893 upstream_branch = cl.GetUpstreamBranch()
4894 if upstream_branch:
4895 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4896 upstream_commit = upstream_commit.strip()
4897
4898 if not upstream_commit:
4899 DieWithError('Could not find base commit for this branch. '
4900 'Are you in detached state?')
4901
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004902 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4903 diff_output = RunGit(changed_files_cmd)
4904 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004905 # Filter out files deleted by this CL
4906 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004907
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004908 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4909 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4910 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004911 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004912
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004913 top_dir = os.path.normpath(
4914 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4915
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004916 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4917 # formatted. This is used to block during the presubmit.
4918 return_value = 0
4919
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004920 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004921 # Locate the clang-format binary in the checkout
4922 try:
4923 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004924 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004925 DieWithError(e)
4926
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004927 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004928 cmd = [clang_format_tool]
4929 if not opts.dry_run and not opts.diff:
4930 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004931 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004932 if opts.diff:
4933 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004934 else:
4935 env = os.environ.copy()
4936 env['PATH'] = str(os.path.dirname(clang_format_tool))
4937 try:
4938 script = clang_format.FindClangFormatScriptInChromiumTree(
4939 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004940 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004941 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004942
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004943 cmd = [sys.executable, script, '-p0']
4944 if not opts.dry_run and not opts.diff:
4945 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004946
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004947 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4948 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004949
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004950 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4951 if opts.diff:
4952 sys.stdout.write(stdout)
4953 if opts.dry_run and len(stdout) > 0:
4954 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004955
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004956 # Similar code to above, but using yapf on .py files rather than clang-format
4957 # on C/C++ files
4958 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004959 yapf_tool = gclient_utils.FindExecutable('yapf')
4960 if yapf_tool is None:
4961 DieWithError('yapf not found in PATH')
4962
4963 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004964 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004965 cmd = [yapf_tool]
4966 if not opts.dry_run and not opts.diff:
4967 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004968 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004969 if opts.diff:
4970 sys.stdout.write(stdout)
4971 else:
4972 # TODO(sbc): yapf --lines mode still has some issues.
4973 # https://github.com/google/yapf/issues/154
4974 DieWithError('--python currently only works with --full')
4975
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004976 # Dart's formatter does not have the nice property of only operating on
4977 # modified chunks, so hard code full.
4978 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004979 try:
4980 command = [dart_format.FindDartFmtToolInChromiumTree()]
4981 if not opts.dry_run and not opts.diff:
4982 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004983 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004984
ppi@chromium.org6593d932016-03-03 15:41:15 +00004985 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004986 if opts.dry_run and stdout:
4987 return_value = 2
4988 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4990 'found in this checkout. Files in other languages are still '
4991 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004992
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004993 # Format GN build files. Always run on full build files for canonical form.
4994 if gn_diff_files:
4995 cmd = ['gn', 'format']
4996 if not opts.dry_run and not opts.diff:
4997 cmd.append('--in-place')
4998 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004999 stdout = RunCommand(cmd + [gn_diff_file],
5000 shell=sys.platform == 'win32',
5001 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005002 if opts.diff:
5003 sys.stdout.write(stdout)
5004
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005005 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005006
5007
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005008@subcommand.usage('<codereview url or issue id>')
5009def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005010 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005011 _, args = parser.parse_args(args)
5012
5013 if len(args) != 1:
5014 parser.print_help()
5015 return 1
5016
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005017 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005018 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005019 parser.print_help()
5020 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005021 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005022
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005023 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005024 output = RunGit(['config', '--local', '--get-regexp',
5025 r'branch\..*\.%s' % issueprefix],
5026 error_ok=True)
5027 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005028 if issue == target_issue:
5029 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005030
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005031 branches = []
5032 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005033 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005034 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005035 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005036 return 1
5037 if len(branches) == 1:
5038 RunGit(['checkout', branches[0]])
5039 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005040 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005041 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005042 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005043 which = raw_input('Choose by index: ')
5044 try:
5045 RunGit(['checkout', branches[int(which)]])
5046 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005047 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005048 return 1
5049
5050 return 0
5051
5052
maruel@chromium.org29404b52014-09-08 22:58:00 +00005053def CMDlol(parser, args):
5054 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005055 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005056 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5057 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5058 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005059 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005060 return 0
5061
5062
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005063class OptionParser(optparse.OptionParser):
5064 """Creates the option parse and add --verbose support."""
5065 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005066 optparse.OptionParser.__init__(
5067 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005068 self.add_option(
5069 '-v', '--verbose', action='count', default=0,
5070 help='Use 2 times for more debugging info')
5071
5072 def parse_args(self, args=None, values=None):
5073 options, args = optparse.OptionParser.parse_args(self, args, values)
5074 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5075 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5076 return options, args
5077
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005078
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005079def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005080 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005081 print('\nYour python version %s is unsupported, please upgrade.\n' %
5082 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005083 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005084
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005085 # Reload settings.
5086 global settings
5087 settings = Settings()
5088
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005089 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005090 dispatcher = subcommand.CommandDispatcher(__name__)
5091 try:
5092 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005093 except auth.AuthenticationError as e:
5094 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005095 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005096 if e.code != 500:
5097 raise
5098 DieWithError(
5099 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5100 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005101 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005102
5103
5104if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005105 # These affect sys.stdout so do it outside of main() to simplify mocks in
5106 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005107 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005108 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005109 try:
5110 sys.exit(main(sys.argv[1:]))
5111 except KeyboardInterrupt:
5112 sys.stderr.write('interrupted\n')
5113 sys.exit(1)